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?
CodePudding user response:
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.
CodePudding user response:
If the number of tasks is 2 (you said task1 and task2) you may consider doing this without a loop:
var t1 = DoOneThingAsync();
var t2 = DoAnotherThingAsync();
await Task.WhenAll(t1, t2); // Without this you can miss exceptions
var result1 = await t1;
var result2 = await t2;
For a dynamic number of tasks I would still suggest separating them by type:
List<Task<int>> xs = ...;
List<Task<string>> ys = ...;
var groupX = await Task.WhenAll(xs);
var groupY = await Task.WhenAll(ys);
CodePudding user response:
I'd argue that in most cases, you shouldn't need this, as once you get the result as object
, you'll most likely want to cast it back to some concrete type anyway, so why not work with the tasks directly until you are sure what the types are?
Anyway, the non-generic Task
class doesn't expose the Result
property like Task<T>
does, and there aren't any simple workarounds. We'll have to use some tricks!
By far my favourite way is to use dynamic
combined with overloads, resulting in a sort of runtime type-based pattern matching:
public static async Task<IEnumerable<object>> TasksResults(IEnumerable<Task> tasks)
{
await Task.WhenAll(tasks);
return tasks.Select(t => TaskResult((dynamic)t));
}
static object TaskResult<T>(Task<T> task)
{
return task.Result;
}
static object TaskResult(Task task)
{
return null;
}
Using (dynamic)
makes the expression behave as if t
had actually the most concrete type of the object inside. If it is actually Task<T>
(with a result), the first overload is chosen, otherwise the second overload is picked. No exception handling necessary.
I've also taken the liberty to use IEnumerable
for the argument and return type. Feel free to use .ToList()
if you need to.