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
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; }
}
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;
}
}
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);
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.
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);
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.
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();
}
}
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);
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.
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();
}
}
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);
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.
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();
}
}
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);
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.
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
Top comments (3)
I see that the article talks about the same thing as @elfocrash explained four days before in his YouTube channel, is it an article about it? VΓdeo here
It should already be like this by default π
I want one of the tasks to not complete or return an error code, what should I do to run that task again?