Managing Tasks with different outputs

Viewed 91

I have a .NET Core 2.1 project that has a BackgroundService and I want its responsibility to just handle logging the result from a group of different tasks that can return different values. I want to group all of their output into a Task Manager class to log their output. Is it possible to have one List<Task> that will contain all the Task objects from these async methods?

I don't want to have multiple Task fields for each method I want to await on. I'd rather have them be put into a List of some sort because there could be the possibility to have more than these three async methods I want this manager to manage.

I was thinking of doing something like:

public class MyTaskManager : BackgroundService
{
    private readonly ILogger<MyTaskManager> _logger;
    private APIInvoker _invoker;

    public MyTaskManager (ILogger<MyTaskManager> logger, APIInvoker invoker)
    {
        _logger = logger;
        _invoker= invoker;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        List<Task<object>> tasks = new List<Task<object>>();

        tasks.Add(_invoker.GetImportWarningsAsync("1"));
        tasks.Add(_invoker.GetImportErrorsAsync("2"));
        tasks.Add(_invoker.GetImportStatusAsync("3"));
    }

Where GetImportWarningsAsync, GetImportErrorsAsync, GetImportStatusAsync are defined as:

internal async Task<string> GetImportWarningsAsync(...)
internal async Task<string> GetImportErrorsAsync(...)
internal async Task<ImportResponse> GetImportLeadStatusAsync(...)

I'm fuzzy on if I can do tasks.Add(...) if they return different types and I am adding them to a List<Task<object>>. I don't think that is possible. How can I achieve something like that?

Ultimately, I want to run a method for each Task in tasks when any of them execute.

eg.

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    List<Task<object>> tasks = new List<Task<object>>();

    tasks.Add(_invoker.GetImportWarningsAsync("1"));
    tasks.Add(_invoker.GetImportErrorsAsync("2"));
    tasks.Add(_invoker.GetImportStatusAsync("3"));


    Task<object> finishedTask = await Task.WhenAny(tasks);
    tasks.Remove(finishedTask);

    HandleTask(finishedTask, await finishedTask);
}

private void HandleTask(Task task, object value)
{
    if (value is ImportResponse)
    {
        _logger.LogInformation((value as ImportResponse).someProp); // Log something
    }
    else
    {
        // Any other object type will be logged here - In this case string.
        _logger.LogInformation(value.ToString());
    }
}
3 Answers

Tasks aren't covariant like that, but nothing is stopping you from casting the result as needed on your own:

    var tasks = new List<Task<object>>();

    tasks.Add(((Func<Task<object>>)(async () => (object)await _invoker.GetImportWarningsAsync("1")))());
    tasks.Add(((Func<Task<object>>)(async () => (object)await _invoker.GetImportErrorsAsync("2")))());
    tasks.Add(((Func<Task<object>>)(async () => (object)await _invoker.GetImportStatusAsync("3")))());

This is most probably not the nicest approach but it does work as expected.

public static class TaskExtensions
{
    public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> t1, Task<T2> t2)
    {
        return (await t1, await t2);
    }

    public static async Task<(T1, T2, T3)> WhenAll<T1, T2, T3>(Task<T1> t1, Task<T2> t2, Task<T3> t3)
    {
        return (await t1, await t2, await t3);
    }

    public static async Task<(T1, T2, T3, T4)> WhenAll<T1, T2, T3, T4>(Task<T1> t1, Task<T2> t2, Task<T3> t3, Task<T4> t4)
    {
        return (await t1, await t2, await t3, await t4);
    }
    
    //etc.
}

Here we are taking advantage of ValueTuple.

Usage sample:

var (warnings, errors, status) = await TaskExtensions.WhenAll(
   _invoker.GetImportWarningsAsync("1"),
   _invoker.GetImportErrorsAsync("2"),
   _invoker.GetImportStatusAsync("3") 
);

Here we are taking advantage of C# 7's deconstruction capabilities.

The types of the deconstructed variables:

  • warnings: string
  • errors: string
  • status: ImportResponse

If you are doing it a lot, you could use the extension method ToObjectAsync shown below:

public static async Task<object> ToObjectAsync<T>(this Task<T> task)
{
    return await task;
}

Usage example:

var tasks = new List<Task<object>>();

tasks.Add(_invoker.GetImportWarningsAsync("1").ToObjectAsync());
tasks.Add(_invoker.GetImportErrorsAsync("2").ToObjectAsync());
tasks.Add(_invoker.GetImportStatusAsync("3").ToObjectAsync());

It is essentially equivalent to Blindy's answer, just a bit more convenient.

Related