Matt McCorry's Blog Processing WhenAll Results
profile picture

Matthew McCorry

.Net and TypeScript Developer

Processing WhenAll Results

asynctaskwhenall

Matt McCorry, 2 months ago

I've been working on a codebase at work. One of the standards we are tying to implement is removing usage of Task.Result

We had some old code where we ran a number of tasks in parallel, got the result and then created a final array of the data.

var t1 = Task.Run(() => new[] { 1 });
var t2 = Task.Run(() => new[] { 2 });
await Task.WhenAll(t1, t2);
var finalArray = t1.Result.Concat(t2.Result);

Easy fix, just use the result of the WhenAll call, and unwrap the nested array.

var t1 = Task.Run(() => new[] { 1 });
var t2 = Task.Run(() => new[] { 2 });
var finalArray = (await Task.WhenAll(t1, t2)).SelectMany(a => a); // [1, 2]

If the types of the tasks vary, WhenAll returns a Task with no content.

var t1 = Task.Run(() => new[] { 1m });
var t2 = Task.Run(() => new[] { 2 });
var finalArray = await Task.WhenAll(t1, t2); // Build error

We can create a function to convert all the the Tasks to the same type, e.g. Task<decimal[]>

async Task<decimal[]> ToDecimal(Task<int[]> t) => (await t).Select(Convert.ToDecimal).ToArray();

Giving us:

var t1 = Task.Run(() => new[] { 1m });
var t2 = ToDecimal(Task.Run(() => new[] { 2 }));
var finalArray = (await Task.WhenAll(t1, t2)).SelectMany(a => a); // [1m, 2m]

It is possible to create generic converters to convert Task<Child> to Task<Parent> to simplify dealing with inherited task results.

In some cases, we want to process the results of the WhenAll individually. In TypeScript, I would write something equivalent to:

var t1 = Task.Run(() => new[] { 1 });
var t2 = Task.Run(() => new[] { 2 });
var [t1Result, t2Result] = await Task.WhenAll(t1, t2);
DoThingWith1(t1Result);
DoThingWith2(t2Result);

We can add a deconstructor like this:

public static void Deconstruct<T>(this T[] array, out T s1, out T s2)
{
    s1 = array[0];
    s2 = array[1];
}

Then split the result of the WhenAll

var t1 = Task.Run(() => new[] { 1 });
var t2 = Task.Run(() => new[] { 2 });
var (t1Result, t2Result) = await Task.WhenAll(t1, t2);
// deal with the results individually here
DoThingWith1(t1Result);
DoThingWith2(t2Result);

When using the deconstructor, we need to be careful that we don't missorder the results, as they will all be of the same type. Just make sure you put a unit test in to ensure the result mapping is correct.

I was hoping the AST for the deconstructor would be interesting, but there isn't much to see:

WhenAll AST