Shaul Behr Shaul Behr - 4 years ago 86
C# Question

Asynchronously calling asynchronous delegate?

Here's a dumbed-down version of what I want to do:

private static int Inc(int input)
{
return input + 1;
}

private static async Task<int> IncAsync(int input)
{
await Task.Delay(200);
return input + 1;
}

private static async Task<IEnumerable<TResult>> GetResultsAsync<TInput, TResult>(Func<TInput, TResult> func, IEnumerable<TInput> values)
{
var tasks = values.Select(value => Task.Run(() => func(value)))
.ToList();
await Task.WhenAll(tasks);
return tasks.Select(t => t.Result);
}

public async void TestAsyncStuff()
{
var numbers = new[] { 1, 2, 3, 4 };
var resultSync = await GetResultsAsync(Inc, numbers); // returns IEnumerable<int>
Console.WriteLine(string.Join(",", resultSync.Select(n => $"{n}")));
// The next line is the important one:
var resultAsync = await GetResultsAsync(IncAsync, numbers); // returns IEnumerable<Task<int>>
}


So basically,
GetResultsAsync()
is intended to be a generic method that will get the results of a function for a set of input values. In
TestAsyncStuff()
you can see how it would work for calling a synchronous function (
Inc()
).

The trouble comes when I want to call an asynchronous function (
IncAsync()
). The result I get back is of type
IEnumerable<Task<int>>
. I could do a
Task.WhenAll()
on that result, and that works:

var tasksAsync = (await GetResultsAsync(IncAsync, numbers)).ToList();
await Task.WhenAll(tasksAsync);
var resultAsync = tasksAsync.Select(t => t.Result);
Console.WriteLine(string.Join(",", resultAsync.Select(n => $"{n}")));


But I'd like to tighten up the code and do the
await
inline. It should look something like this:

var resultAsync = await GetResultsAsync(async n => await IncAsync(n), numbers);


But that also returns an
IEnumerable<Task<int>>
! I could do this:

var resultAsync = await GetResultsAsync(n => IncAsync(n).GetAwaiter().GetResult(), numbers);


And that works... but from what I've seen, use of
Task.GetAwaiter().GetResult()
or
Task.Result
is not encouraged.

So what is the correct way to do this?

Answer Source

You should create two overloads of GetResultsAsync. One should accept a 'synchronous' delegate which returns TResult. This method will wrap each delegate into a task, and run them asynchronously:

private static async Task<IEnumerable<TResult>> GetResultsAsync<TInput, TResult>(
   Func<TInput, TResult> func, IEnumerable<TInput> values)
{
    var tasks = values.Select(value => Task.Run(() => func(value)));
    return await Task.WhenAll(tasks);
}

The second overload will accept an 'asynchronous' delegate, which returns Task<TResult>. This method doesn't need to wrap each delegate into a task, because they are already tasks:

private static async Task<IEnumerable<TResult>> GetResultsAsync<TInput, TResult>(
   Func<TInput, Task<TResult>> func, IEnumerable<TInput> values)
{
    var tasks = values.Select(value => func(value));
    return await Task.WhenAll(tasks);
}

You even can call the second method from the first one to avoid code duplication:

private static async Task<IEnumerable<TResult>> GetResultsAsync<TInput, TResult>(
   Func<TInput, TResult> func, IEnumerable<TInput> values)
{
    return await GetResultsAsync(x => Task.Run(() => func(x)), values);
}

NOTE: These methods don't simplify your life a lot. The same results can be achieved with

var resultSync = await Task.WhenAll(numbers.Select(x => Task.Run(() => Inc(x))));
var resultAsync = await Task.WhenAll(numbers.Select(IncAsync));
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download