The elegant way to await multiple tasks in .NET.

Serhii Korol - Nov 12 '23 - - Dev Community

Hello everyone. In this article, we'll talk about tasks. I'm sure each of you is using async methods, and there are some cases when you need to synchronize tasks. I'll show you how to handle it more elegantly. Sit down comfortably, and we are beginning.
First of all, I ask you to create a simple console application.

dotnet new console -n AwaitHandleSample
Enter fullscreen mode Exit fullscreen mode

For further work, we need to create a simple logic. We'll call third-party API, parse, and return it. Let's create a response model:

public record AstroForecast
{
    [JsonPropertyName("sign")]
    public string? Sign { get; set; }

    [JsonPropertyName("date")]
    public DateTimeOffset Date { get; set; }

    [JsonPropertyName("horoscope")]
    public string? Horoscope { get; set; }
    public TimeSpan Time { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

This model returns the zodiac sign that was inquired, the current date of the astrological forecast, and the text to the forecast. The last property doesn't belong to the response API. I added this for tracking tasks. Please also add this service:

public class HoroService
{
    public enum ZodiacSign
    {
        Aries,
        Taurus,
        Gemini,
        Cancer,
        Leo,
        Virgo,
        Libra,
        Scorpio,
        Sagittarius,
        Capricorn,
        Aquarius,
        Pisces
    }

    public async Task<AstroForecast?> MakeDailyHoroscopeBySign(ZodiacSign sign)
    {
        var client = new HttpClient();
        var response = await client.GetAsync($"https://ohmanda.com/api/horoscope/{sign.ToString().ToLower()}");
        response.EnsureSuccessStatusCode();
        var json = await response.Content.ReadAsStringAsync();

        var model = JsonConvert.DeserializeObject<AstroForecast>(json);
        model!.Time = DateTime.Now.TimeOfDay;
        return model;
    }
}
Enter fullscreen mode Exit fullscreen mode

Before we start implementing calling our service, I want to mention that I divided the demonstration into several stages. It allows step-by-step modification of the code.

Stage 1

Please add this code to your Program.cs file and run it:

//Stage 1 
var horo = new HoroService();
var taurus = await horo.MakeDailyHoroscopeBySign(HoroService.ZodiacSign.Taurus);
var scorpio = await horo.MakeDailyHoroscopeBySign(HoroService.ZodiacSign.Scorpio);

Console.WriteLine(taurus);
Console.WriteLine(scorpio); 
Enter fullscreen mode Exit fullscreen mode

You should receive the result in something like this:

/Users/serhiikorol/RiderProjects/AwaitHandleSample/AwaitHandleSample/bin/Debug/net7.0/AwaitHandleSample
AstroForecast { Sign = taurus, Date = 12.11.2023 00:00:00 +02:00, Horoscope = Get ready for even more volatile tension that will be felt today and tomorrow, , Time = 12:09:47.7103010 }
AstroForecast { Sign = scorpio, Date = 12.11.2023 00:00:00 +02:00, Horoscope = Today and tomorrow could be one of the most important periods of your entire life, , Time = 12:09:49.1569490 }

Process finished with exit code 0.
Enter fullscreen mode Exit fullscreen mode

Pay attention to the last property Time. The two tasks worked with 2 seconds difference. It is because each task runs in the standalone thread and doesn't depend on another task. So are working asynchronous calls.

Stage 2

Let's change this code:

//Stage 2
var horo = new HoroService();
var taurus = horo.MakeDailyHoroscopeBySign(HoroService.ZodiacSign.Taurus);
var scorpio = horo.MakeDailyHoroscopeBySign(HoroService.ZodiacSign.Scorpio);

await Task.WhenAll(taurus, scorpio);
Console.WriteLine(taurus.Result);
Console.WriteLine(scorpio.Result);
Enter fullscreen mode Exit fullscreen mode

If you run it, you'll get a similar result:

AstroForecast { Sign = taurus, Date = 12.11.2023 00:00:00 +02:00, Horoscope = Get ready for even more volatile tension that will be felt today and tomorrow, , Time = 12:24:40.2863970 }
AstroForecast { Sign = scorpio, Date = 12.11.2023 00:00:00 +02:00, Horoscope = Today and tomorrow could be one of the most important periods of your entire life, , Time = 12:24:40.4238450 }

Process finished with exit code 0.
Enter fullscreen mode Exit fullscreen mode

As you might detect, the time difference was narrowed. It's caused by the reason that tasks were synchronized.

Stage 3

Let's modify and improve the previous sample. Please add this code:

public static class TaskExt
{
    public static async Task<Task<T>[]> WhenAll<T>(params Task<T>[] tasks)
    {
        try
        {
            await Task.WhenAll(tasks);
        }
        catch (InvalidOperationException e)
        {
            var faultedTasks = tasks.Where(task => task.Exception != null).ToArray();
            return faultedTasks;
        }

        return tasks.Where(task => task.Status == TaskStatus.RanToCompletion).ToArray();
    }
}
Enter fullscreen mode Exit fullscreen mode

This class is a wrapper for the Task.WhenAll(). Why does it need to, you'll ask me. This wrapper is required to handle exceptions and return successful and faulted tasks.

Let's call it:

//Stage 3
 var horo = new HoroService();
 var taurus = horo.MakeDailyHoroscopeBySign(HoroService.ZodiacSign.Taurus);
 var scorpio = horo.MakeDailyHoroscopeBySign(HoroService.ZodiacSign.Scorpio);

 await TaskExt.WhenAll(taurus, scorpio);
 Console.WriteLine(taurus.Result);
 Console.WriteLine(scorpio.Result);
Enter fullscreen mode Exit fullscreen mode

The result:

/Users/serhiikorol/RiderProjects/AwaitHandleSample/AwaitHandleSample/bin/Debug/net7.0/AwaitHandleSample
AstroForecast { Sign = taurus, Date = 12.11.2023 00:00:00 +02:00, Horoscope = Get ready for even more volatile tension that will be felt today and tomorrow, , Time = 13:08:11.3602040 }
AstroForecast { Sign = scorpio, Date = 12.11.2023 00:00:00 +02:00, Horoscope = Today and tomorrow could be one of the most important periods of your entire life, , Time = 13:08:11.3601650 }

Process finished with exit code 0.
Enter fullscreen mode Exit fullscreen mode

Stage 4

Let's simplify the previous code, and please add this code:

public static class TaskHelper
{
    public static TaskAwaiter<Task<T>[]> GetAwaiter<T>(this (Task<T>, Task<T>) tasks)
    {
        return TaskExt.WhenAll(tasks.Item1, tasks.Item2).GetAwaiter();
    }
}
Enter fullscreen mode Exit fullscreen mode

This helper allows you to get out from TaskExt.WhenAll() on each call. We implemented it in the helper.

var horo = new HoroService();
var taurus = horo.MakeDailyHoroscopeBySign(HoroService.ZodiacSign.Taurus);
var scorpio = horo.MakeDailyHoroscopeBySign(HoroService.ZodiacSign.Scorpio);

await (taurus, scorpio);
Console.WriteLine(taurus.Result);
Console.WriteLine(scorpio.Result);
Enter fullscreen mode Exit fullscreen mode

The result:

/Users/serhiikorol/RiderProjects/AwaitHandleSample/AwaitHandleSample/bin/Debug/net7.0/AwaitHandleSample
AstroForecast { Sign = taurus, Date = 12.11.2023 00:00:00 +02:00, Horoscope = Get ready for even more volatile tension that will be felt today and tomorrow, , Time = 13:11:47.9693390 }
AstroForecast { Sign = scorpio, Date = 12.11.2023 00:00:00 +02:00, Horoscope = Today and tomorrow could be one of the most important periods of your entire life, , Time = 13:11:47.9693090 }

Process finished with exit code 0.
Enter fullscreen mode Exit fullscreen mode

Stage 5

However, it's not it all. I want to modify more. Let's extend the existing helper and remake GetAwaiter.

public static class TaskHelper
{
    public static TaskAwaiter<(T, T)> GetAwaiter<T>(this (Task<T>, Task<T>) tasks)
    {
        async Task<(T, T)> MergeTasks()
        {
            var (task1, task2) = tasks;
            await TaskExt.WhenAll(task1, task2);

            return (task1.Result, task2.Result);
        }

        return MergeTasks().GetAwaiter();
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's call it:

//Stage 5
var horo = new HoroService();
var taurus = horo.MakeDailyHoroscopeBySign(HoroService.ZodiacSign.Taurus);
var scorpio = horo.MakeDailyHoroscopeBySign(HoroService.ZodiacSign.Scorpio);

var (forecast1, forecast2) = await (taurus, scorpio);
Console.WriteLine(forecast1);
Console.WriteLine(forecast2);
Enter fullscreen mode Exit fullscreen mode

The result:

/Users/serhiikorol/RiderProjects/AwaitHandleSample/AwaitHandleSample/bin/Debug/net7.0/AwaitHandleSample
AstroForecast { Sign = taurus, Date = 12.11.2023 00:00:00 +02:00, Horoscope = Get ready for even more volatile tension that will be felt today and tomorrow, , Time = 13:26:24.9281830 }
AstroForecast { Sign = scorpio, Date = 12.11.2023 00:00:00 +02:00, Horoscope = Today and tomorrow could be one of the most important periods of your entire life, , Time = 13:26:24.9282190 }

Process finished with exit code 0.
Enter fullscreen mode Exit fullscreen mode

Conclution.

This approach can help you reuse repeatable code and reduce the amount of code. You also can make a method that handles many more tasks, three or four, et cetera.

I hope this article was helpful for you and see you next week. You can find the source code by the link below. Happy coding!

The source code >>>>> LINK

Buy Me A Beer

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .