2

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());
    }
}
Jimenemex
  • 3,104
  • 3
  • 24
  • 56
  • What do you need to do with `value`? – Johnathan Barclay Oct 09 '20 at 15:50
  • 1
    Please [edit] post to clarify what exactly you want to achieve - `_logger.LogInformation(/$"Log task value {value}")` is clearly a solution but it I doubt you are interested in one. Also make sure to clarify why it is *not a duplicate* of https://stackoverflow.com/questions/17197699/awaiting-multiple-tasks-with-different-results – Alexei Levenkov Oct 09 '20 at 16:26

3 Answers3

3

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")))());
Blindy
  • 65,249
  • 10
  • 91
  • 131
  • Wouldn't I need to wrap the `async () => ...` inside a `Task.Run()`? Therefore not need the `await` & `async` also? – Jimenemex Oct 09 '20 at 20:38
  • No, there’s no need to use yet another thread, all you want is to cast the result of a finished thread. – Blindy Oct 11 '20 at 02:52
  • I thought lambda expressions `async () =>` are not allowed to be cast into an `Task` in the `.Add()` because they aren't delegate types. – Jimenemex Oct 13 '20 at 14:17
  • What do you mean, it is a delegate type. It's been 4 days, just type it in and try it lol – Blindy Oct 13 '20 at 14:58
  • I did try this, and long behold: `Cannot convert lambda expression to type Task because it is not a delegate type.` :] – Jimenemex Oct 13 '20 at 15:01
  • Ah, I see. Okay, I updated the code, the idea is the same, but you need to actually call the result converter lambda if you want to store the tasks in a list. Alternatively, you can run my previous version and store lambda functions to start at a later time. – Blindy Oct 13 '20 at 15:17
1

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
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
1

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.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104