Home > Software engineering >  How to access a variable that was available when an async method was called?
How to access a variable that was available when an async method was called?

Time:09-30

The animal names are fetched from an API that could return 404 if the animal is not found. But in order to properly log the error, we need to have access to the animal's country. Is it possible? I've read something from a guy called Stephen Cleary that made me think it is possible with lambdas, but I couldn't find anything.

var gettingNames = new List<Task<string>>();

foreach (var animal in animals)
{
    gettingNames.Add(this.zooApi.GetNameAsync(animal));
}

try
{
    await Task.WhenAll(gettingNames);
}
catch (Exception e)
{
    var exception = gettingNames.Where(task => task.IsFaulted)
        .SelectMany(x => x.Exception.InnerExceptions).First();

    this.logger.LogError("The animal name from {Country} was not found",
        animal.Country); // This is the goal
}

CodePudding user response:

One way to solve this problem is to project each Animal to a Task that contains more information than either a naked name or a naked error. For example you could project it to a Task<ValueTuple<Animal, string, Exception>> that contains three pieces of information: the animal, the animal's scientific name from the zooApi, and the error that may have happened while invoking the zooApi.GetScientificNameAsync method.

The easiest way to do this projection is the LINQ Select operator:

List<Task<(Animal, string, Exception)>> tasks = animals.Select(async animal =>
{
    try
    {
        return (animal, await this.zooApi.GetScientificNameAsync(animal),
            (Exception)null);
    }
    catch (Exception ex)
    {
        return (animal, null, ex);
    }
}).ToList();

(Animal, string, Exception)[] results = await Task.WhenAll(tasks);

foreach (var (animal, scientificName, error) in results)
{
    if (error != null)
        this.logger.LogError(error,
            $"The {animal.Name} from {animal.Country} was not found");
}

CodePudding user response:

You have almost nailed it. :)

Rather than having a List<Task<string>> you need a Dictionary<Task<string>, string> structure:

static async Task Main()
{
    var taskInputMapping = new Dictionary<Task<string>, string>();
    var inputs = new[] { "input", "fault", "error", "test"};
    foreach (var input in inputs)
    {
        taskInputMapping.Add(DelayEcho(input), input);
    }

    try
    {
        await Task.WhenAll(taskInputMapping.Keys);
    }
    catch
    {
        foreach (var pair in taskInputMapping.Where(t => t.Key.IsFaulted))
        {
            Console.WriteLine($"{pair.Value}: {pair.Key.Exception?.GetType().Name}");
        }
    }
}

static readonly ImmutableArray<string> wrongInputs = 
    ImmutableArray.Create("error", "fault");
static async Task<string> DelayEcho(string input)
{
    if (wrongInputs.Contains(input)) throw new ArgumentException();
    await Task.Delay(10);
    return input;
}
  • taskInputMapping.Add(DelayEcho(input), input): Stores the input next to the Task itself
  • taskInputMapping.Where(t => t.Key.IsFaulted): Iterates through the faulted tasks
  • $"{pair.Value}: {pair.Key.Exception?.GetType().Name}": Retrieves the input the related error

CodePudding user response:

I combined the answers and came up with this:

var tasks = animals.Select(async animal =>
{
    try
    {
        return await this.zooApi.GetNameAsync(animal);
    }
    catch (Exception ex)
    {
        this.logger.LogError(error,
            $"The {animal.Name} from {animal.Country} was not found");

        return null;
    }
});

var results = await Task.WhenAll(tasks);

foreach (var name in results.Where(x => x != null))...
  • Related