Click here to Skip to main content
15,887,083 members
Articles / Programming Languages / C#
Tip/Trick

Awaiting a Tuple of Tasks in C#

Rate me:
Please Sign up or sign in to vote.
5.00/5 (19 votes)
4 Sep 2023CPOL2 min read 26.2K   29   14
Elegant replacement for awaiting a limited set of tasks
This tip introduces a more concise way to await and extract results from multiple asynchronous tasks in C# by creating custom awaitable types using extension methods.

Introduction

Consider this code:

C#
Task<int> fetchIntFromApiTask = FetchIntFromApi();
Task<string> fetchStringFromApiTask = FetchStringFromApi();
await Task.WhenAll(fetchIntFromApiTask, fetchStringFromApiTask);
string stringResponse = fetchStringFromApiTask.Result;
int intResponse = fetchIntFromApiTask.Result;

What we are trying to accomplish in the snippet above is just awaiting two asynchronous methods concurrently and assign their results to two different variables. There is a lot of boilerplate.

On the other hand, consider this one-liner:

C#
var (stringResponse, intResponse) = await (FetchStringFromApi(), FetchIntFromApi());

Here, we are trying to await a tuple of Tasks, with the goal to "extract" the Result from them. This is by far more readable, more maintainable and more intuitive. The main problem is that this is not standard C#, but the good news is that this is fairly easy to implement!

How Can We Await a Tuple of Tasks?

First of all, we have to remember what is an awaitable. An awaitable is basically a type on which we can apply the await operator. For example, Task, Task<T>, ValueTask, ValueTask<T> are all awaitables. The good thing is that we can create our own awaitables!

This is the rigorous definition of an awaitable:

A type is said to be an awaitable, if it has an instance method or an extension method called GetAwaiter() that returns a valid awaiter type.

A type is said to be an awaiter if it:

  1. implements the INotifyCompletion interface
  2. has a property bool IsCompleted { get; }
  3. has a method GetResult() that can either return something or void. This is the value returned by the await operator.

For example, a Task<T>, has an instance method with this signature TaskAwaiter<T> GetAwaiter(). The TaskAwaiter<T> follows the awaiter pattern and has a Method T GetResult(), that blocks the caller thread until the result is ready, before returning it back.

So the trick is to create an extension method over our tuple (the type ValueTuple<T1,T2>) called GetAwaiter() that returns a valid awaiter. In our scenario, we want awaiting the tuple to be equivalent to call Task.WhenAll on the elements of the tuple, so we can directly return the TaskAwaiter returned back from a Task that extracts the results. So, in practice, this is our solution:

C#
public static class TaskExtensions
{
    public static TaskAwaiter<(T1, T2)> GetAwaiter<T1, T2>(this (Task<T1>, Task<T2>) tuple)
    {
        async Task<(T1, T2)> Core()
        {
            var (task1, task2) = tuple;
            await Task.WhenAll(task1, task2);
            return (task1.Result, task2.Result);
        }

        return Core().GetAwaiter();
    }
}

Now the one-liner compiles and works perfectly:

C#
var (stringResponse, intResponse) = await (FetchStringFromApi(), FetchIntFromApi());

We can, of course, create overloads for Tuples with more elements, using the same strategy.

If we want also to implement ConfigureAwait(), we can implement it similarly as an extension method over the tuple that returns a ConfiguredTaskAwaitable<(T1,T2)>.

C#
public static ConfiguredTaskAwaitable<(T1, T2)> ConfigureAwait<T1, T2>
       (this (Task<T1>, Task<T2>) tuple, bool continueOnCapturedContext)
{
    async Task<(T1, T2)> Core()
    {
        var (task1, task2) = tuple;
        await Task.WhenAll(task1, task2).ConfigureAwait(continueOnCapturedContext);
        return (task1.Result, task2.Result);
    }

    return Core().ConfigureAwait(continueOnCapturedContext);
}

var (stringResponse, intResponse) = 
     await (FetchStringFromApi(), FetchIntFromApi()).ConfigureAwait(false);

History

  • 3rd September, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Italy Italy
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
tbayart6-Sep-23 5:25
professionaltbayart6-Sep-23 5:25 
Questionimho title change needed Pin
BillWoodruff4-Sep-23 2:53
professionalBillWoodruff4-Sep-23 2:53 
AnswerRe: imho title change needed Pin
Federico Alterio4-Sep-23 7:10
Federico Alterio4-Sep-23 7:10 
QuestionAn Alternative Approach Pin
George Swan3-Sep-23 22:34
mveGeorge Swan3-Sep-23 22:34 
AnswerRe: An Alternative Approach Pin
Federico Alterio3-Sep-23 22:56
Federico Alterio3-Sep-23 22:56 
GeneralRe: An Alternative Approach Pin
George Swan4-Sep-23 1:00
mveGeorge Swan4-Sep-23 1:00 
GeneralRe: An Alternative Approach Pin
Federico Alterio4-Sep-23 1:09
Federico Alterio4-Sep-23 1:09 
GeneralRe: An Alternative Approach Pin
George Swan4-Sep-23 2:39
mveGeorge Swan4-Sep-23 2:39 

Your observations are perfectly valid and appreciated. I just wanted to highlight an alternative approach that follows the pattern of avoiding blocking Task calls. Thanks again for the well written and informative piece.


GeneralRe: An Alternative Approach Pin
Federico Alterio4-Sep-23 2:47
Federico Alterio4-Sep-23 2:47 
BugTypo Pin
Сергій Ярошко3-Sep-23 20:55
professionalСергій Ярошко3-Sep-23 20:55 
GeneralRe: Typo Pin
Federico Alterio3-Sep-23 21:03
Federico Alterio3-Sep-23 21:03 
GeneralRe: Typo Pin
Сергій Ярошко3-Sep-23 21:18
professionalСергій Ярошко3-Sep-23 21:18 
GeneralRe: Typo Pin
Federico Alterio3-Sep-23 21:29
Federico Alterio3-Sep-23 21:29 
GeneralRe: Typo Pin
tbayart6-Sep-23 5:24
professionaltbayart6-Sep-23 5:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.