I am looking to create a Task<List>
that when invoked with a set of methods executes in parallel, runs and returns the result in the same order of the tasks in an array. The Tasks can return different types. I tried below. Not sure if I am heading in right direction.
private async Task<IList<object>> RunTasks<T>(IList<Task> taskList)
{
var allTasks = Task.WhenAll(taskList);
var ret = new object[taskList.Count];
await allTasks;
for (int i=0;i<taskList.Count;i++)
{
ret[i] = taskList[i].IsFaulted ?
default : ((Task<T>)taskList[i]).Result;
}
//otherPolicies.AppsPermissionsPolicy = teamsAppPermissionDocFromAAE
// .ToMTTeamsAppPermissionPolicy().ToMTPolicyDocument();
//Wrap AAE TeamsApp doc response into other Policies
return ret;
}
If Task1
& Task2
returns different types in taskList
do we need T
for RunTasks
? if so, What type do we pass in to Invoke RunTasks
?. If we dont need it, then how do we convert the Tasks return type to its corresponding object in the for loop immediately after tasks completed before we return the object array with results?
I think that converting a List<Task>
to a List<Task<object>>
cannot be done without reflection. Or without the dynamic
keyword, like in the implementation below:
public static Task<object[]> WhenAllToObject(IEnumerable<Task> tasks)
{
ArgumentNullException.ThrowIfNull(tasks);
return Task.WhenAll(tasks.Select(async task =>
{
// First await the task, to ensure that it ran successfully.
await task.ConfigureAwait(false);
// Then try to get its result, if it's a generic Task<T>.
try
{
return await (dynamic)task;
}
catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException)
{
throw new InvalidOperationException("Non-generic task found.");
}
}));
}
Usage example:
List<Task> heterogeneousListOfTasks = new()
{
Task.FromResult(13),
Task.FromResult("Hello"),
Task.FromResult(true),
};
object[] results = await WhenAllToObject(heterogeneousListOfTasks);
Alternative: This one is inspired by IS4's ingenious type-matching trick. The advantage over the above implementation is that the validation of the tasks
argument happens synchronously. That's because the pattern matching for the Task
case is a non-async
method (it elides async and await).
public static Task<object[]> WhenAllToObject(IEnumerable<Task> tasks)
{
ArgumentNullException.ThrowIfNull(tasks);
return Task.WhenAll(tasks.Select(task =>
{
if (task == null) throw new ArgumentException(
$"The {nameof(tasks)} argument included a null value.", nameof(tasks));
Task<object> taskOfObject = ToTaskOfObject((dynamic)task);
if (taskOfObject == null) throw new ArgumentException(
$"The {nameof(tasks)} argument included a non-generic Task.", nameof(tasks));
return taskOfObject;
}));
}
private static Task<object> ToTaskOfObject(Task task) // Not async
=> null;
private static async Task<object> ToTaskOfObject<T>(Task<T> task)
=> await task.ConfigureAwait(false);
Both implementations have similar behavior with the Task.WhenAll
method, but not identical. The Task.WhenAll
propagates all exceptions of all tasks. On the contrary the WhenAllToObject
propagates only the first exception of each task.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With