Sometimes, you have multiple async calls to make, and you want to do that asynchronously and get the results afterward. Let's build a fluent API to do that.
The problem
Imagine we have three API calls that can be done independently, and we want the results of each of the calls. We could do something like this:
var result1 = CallAPI1();
var result2 = CallAPI2();
var result3 = CallAPI3();
var result1Value = await result1;
var result2Value = await result2;
var result3Value = await result3;
That works, but it's not very elegant. Let's build a fluent API to do that. We want something like this:
var results = await TaskHelper.StartWith(() => CallAPI1())
.And(() => CallAPI2())
.And(() => CallAPI3())
.WaitAllAsync();
Where results
is a ValueTuple
of the returned types, so we can also do this instead:
var (resultOne, resultTwo, resultThree) = await TaskHelper.StartWith(() => CallAPI1())
.And(() => CallAPI2())
.And(() => CallAPI3())
.WaitAllAsync();
The solution
Let's build a structure that can fulfill the requirement. The idea is that each call needs its own object so that we are statically and strongly typed.
public static class TaskHelper
{
public static TaskHelper<T> StartWith<T>(Func<Task<T>> task)
{
return new TaskHelper<T>(task);
}
}
public class TaskHelper<T>
{
private readonly List<Func<Task<object>>> _tasks;
public TaskHelper(Func<Task<T>> initialTask)
{
_tasks = [() => initialTask().ContinueWith(t => (object)t.Result)];
}
public TaskHelper<T, TNext> And<TNext>(Func<Task<TNext>> nextTask)
{
return new TaskHelper<T, TNext>(_tasks, nextTask);
}
public async Task<T> WaitAllAsync()
{
var results = await Task.WhenAll(_tasks.Select(t => t()));
return (T)results[0];
}
}
public class TaskHelper<T1, T2>
{
private readonly List<Func<Task<object>>> _tasks;
public TaskHelper(List<Func<Task<object>>> existingTasks, Func<Task<T2>> nextTask)
{
_tasks = [..existingTasks, () => nextTask().ContinueWith(t => (object)t.Result)];
}
public TaskHelper<T1, T2, T3> And<T3>(Func<Task<T3>> nextTask)
{
return new TaskHelper<T1, T2, T3>(_tasks, nextTask);
}
public async Task<(T1, T2)> WaitAllAsync()
{
var results = await Task.WhenAll(_tasks.Select(t => t()));
return ((T1)results[0], (T2)results[1]);
}
}
public class TaskHelper<T1, T2, T3>
{
private readonly List<Func<Task<object>>> _tasks;
public TaskHelper(List<Func<Task<object>>> existingTasks, Func<Task<T3>> nextTask)
{
_tasks = [..existingTasks, () => nextTask().ContinueWith(t => (object)t.Result)];
}
public async Task<(T1, T2, T3)> WaitAllAsync()
{
var results = await Task.WhenAll(_tasks.Select(t => t()));
return ((T1)results[0], (T2)results[1], (T3)results[2]);
}
}
This way, we can chain as many calls as we want, and we will get the results in a strongly typed way. To support more And
calls we would need another TaskHelper
class with more generic parameters.
But the result looks like this:
We can see that the compiler knows exactly which type will be returned! And that all thanks to generics. We basically chain calls with one higher generic parameter each time, and the compiler can infer the types.
Thanks to @toupswork for the initial challenge and thought!