4

Consider the following ideal code (which doesn't work). I'm essentially trying to create a list of Tasks that return a specific object, associate them with a string identifier, then execute all of them in bulk with Task.WhenAll. At the end of the execution, I need to have the results of those Tasks still associated with the string identifiers they were originally created with:

public async Task<SomeObject> DoSomethingAsync(string thing)
{
    // implementation elided
}

public async Task<SomeObject> DoSomethingElseAsync(string thing)
{
    // different implementation elided
}

public async Task<IEnumerable<(string, SomeObject)>>
    DoManyThingsAsync(IEnumerable<string> someListOfStrings, bool condition)
{
    var tasks = new List<(string, Task<SomeObject>)>();

    foreach (var item in someListOfStrings)
    {
        if (condition)
        {
            tasks.Add((item, DoSomethingAsync(item)));
        }
        else
        {
            tasks.Add((item, DoSomethingElseAsync(item)));
        }
    }

    // this doesn't compile, i'm just demonstrating what i want to achieve
    var results = await Task.WhenAll(tasks);

    return results;
}

This can be rewritten to the following:

public async Task<(string, SomeObject)> DoSomethingWrapperAsync(string thing)
    => (thing, await DoSomethingAsync(thing));

public async Task<(string, SomeObject)> DoSomethingElseWrapperAsync(string thing)
    => (thing, await DoSomethingElseAsync(thing));

public async Task<IEnumerable<(string, SomeObject)>>
    DoManyThingsAsync(IEnumerable<string> someListOfStrings, bool condition)
{
    var tasks = new List<Task<(string, SomeObject)>>();

    foreach (var thing in someListOfStrings)
    {
        if (condition)
        {
            tasks.Add(DoSomethingWrapperAsync(thing));
        }
        else
        {
            tasks.Add(DoSomethingElseWrapperAsync(thing));
        }
    }

    // this does compile
    var results = await Task.WhenAll(tasks);

    return results;
}

The problem is that I need an extra wrapper method for every possible discrete async function I'm going to call, which feels unnecessary and wasteful and is a lot of code (because there will be MANY of these methods). Is there a simpler way of achieving what I need?

I looked into implementing the awaitable/awaiter pattern, but can't see how I could get it to work with Task.WhenAll which requires a collection of Task or Task<TResult>, since the guidance seems to be "don't extend those classes".

Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
  • 2
    `await Tasks.WhenAll(tasks.Select(x => x.task))`? – canton7 Jul 13 '21 at 12:07
  • 2
    Why do you need the tuples? – Johnathan Barclay Jul 13 '21 at 12:08
  • FYI you don't need a wrapper for each method. You could do: `public async Task<(string, T)> WrapperAsync(string thing, Func> asyncFunc) => (thing, await asyncFunc(thing));` Then `tasks.Add(WrapperAsync(thing, DoSomethingAsync));` – Johnathan Barclay Jul 13 '21 at 12:12
  • @JohnathanBarclay Technically I don't, I just need a way to associate the relevant string with the result of the relevant method. (Assume that I cannot modify `SomeObject`'s definition to simply include this.) I suppose I could make a wrapper class, but that's much the same as the tuple IMO. – Ian Kemp Jul 13 '21 at 12:12
  • Also - I've been banging my head against this for a couple of hours so it's *very* likely I've missed something obvious - so please forgive me if this seems like a stupid question! – Ian Kemp Jul 13 '21 at 12:13
  • Why not keep the 'thing' in an entirely separate list? – JonasH Jul 13 '21 at 12:15
  • The first item in each tuple isn't used in the code you posted, so I'm not sure what the association is achieving? If it is necessary, then you could do: `await Task.WhenAll(tasks.Select(x => x.Item2));`. – Johnathan Barclay Jul 13 '21 at 12:20
  • Sorry all - I elided a bit too much information - question has been updated with what I'm actually looking to return. – Ian Kemp Jul 13 '21 at 12:27

2 Answers2

4

You can either do the zipping as you go:

public async Task<IEnumerable<(string, SomeObject)>>
    DoManyThingsAsync(IEnumerable<string> someListOfStrings, bool condition)
{
  var tasks = someListOfStrings
      .Select(async item =>
          condition ?
          (item, await DoSomethingAsync(item)) :
          (item, await DoSomethingElseAsync(item)))
      .ToList();
  return await Task.WhenAll(tasks);
}

Or, you can keep the input as a separate collection and zip it later:

public async Task<IEnumerable<(string, SomeObject)>>
    DoManyThingsAsync(IEnumerable<string> someListOfStrings, bool condition)
{
  // Reify input so we don't enumerate twice.
  var input = someListOfStrings.ToList();

  var tasks = input
      .Select(item =>
          condition ?
          DoSomethingAsync(item) :
          DoSomethingElseAsync(item))
      .ToList();
  var taskResults = await Task.WhenAll(tasks);

  return input.Zip(taskResults, (item, taskResult) => ((item, taskResult)));
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    I like the first approach. The second not so much, because it feels more error prone, and also because it elides async and await. – Theodor Zoulias Jul 13 '21 at 13:40
0

From what I can gather you are using condition to determine which method (of the exact same signature) is being called. Why not pass a callback that is called for each item instead of doing the logic inside the foreach loop?

public async Task<SomeObject> DoSomethingAsync(string thing)
{
    // ...
}

public async Task<SomeObject> DoSomethingElseAsync(string thing)
{
    // ...
}

public async Task<IEnumerable<(string, SomeObject)>> DoManyThingsAsync(IEnumerable<string> someListOfStrings, bool condition)
{
    Func<string, Task<SomeObject>> callback = condition ? DoSomethingAsync : DoSomethingElseAsync;

    var results = await Task.WhenAll(
        someListOfStrings.Select(async thing => (thing, await callback(thing)))
    );
    return results;
}

Furthermore, you can extract this as an extension method.

public static class AsyncExtensions
{
    public static async Task<IEnumerable<(T, TResult)>> WhenAllAsync(this IEnumerable<T> collection, Func<T, Task<TResult>> selector)
    {
        var results = await Task.WhenAll(
            collection.Select(async item => (item, await selector(item)))
        );
        return results;
    }
}

public async Task MyMethodAsync()
{
    // ...

    var results = await myListOfStrings.WhenAllAsync(condition ? DoSomethingAsync : DoSomethingElseAsync);

    // ...
}

Or do the mapping in the callback.

public static class AsyncExtensions
{
    public static Task<IEnumerable<TResult>> WhenAllAsync(this IEnumerable<T> collection, Func<T, Task<TResult>> selector)
        => Task.WhenAll(collection.Select(selector)));
}

public async Task MyMethodAsync()
{
    // ...

    var results = await myListOfStrings.WhenAllAsync(async thing => (thing, await (condition ? DoSomethingAsync(thing) : DoSomethingElseAsync(thing))));

    // ...
}
Andrei15193
  • 655
  • 5
  • 8
  • 1
    Combining `await` and `ContinueWith` is not a good idea. You are using two mechanisms that have slightly different semantics in order to accomplish the same thing. The `ContinueWith` is a [primitive](https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html) method, and it should be avoided in general. – Theodor Zoulias Jul 13 '21 at 13:35
  • 1
    @TheodorZoulias fair point, I've updated my answer. – Andrei15193 Jul 13 '21 at 13:39
  • OK, I revoked my downvote. But honestly I am not inclined to upvote, because the `Process`/`ProcessAsync` method looks overly abstract to me. It is unlikely that this method will be used twice in the same project, and its name communicates poorly what this method is doing. – Theodor Zoulias Jul 13 '21 at 13:49
  • @TheodorZoulias I wouldn't know if a method like that will be used twice or not, I don't know the project. As to being overly abstract, well, it's an easy way towards an extension method that may be used multiple times if that's the case. – Andrei15193 Jul 13 '21 at 14:00
  • 1
    `WhenAllAsync` is a better name than `ProcessAsync`. You could compare your extension method with the new API [`Parallel.ForEachAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel.foreachasync?view=net-6.0), that is going to be introduced in .NET 6. – Theodor Zoulias Jul 13 '21 at 18:59
  • 1
    @TheodorZoulias yeah, the main challenge I see in the question is actually determining the callback that does the processing and then using that to map both input and output together. Once you have the callback you can go a number of ways to do that mapping which may or may not require an extension method in the first place. – Andrei15193 Jul 14 '21 at 10:06
  • 1
    My main concern is that an extension method like the `WhenAllAsync` could be implemented in all sorts of ways, and have all sorts of signatures, and all these implementations and signatures would be valid in some context, and would fulfill a useful role. Regarding the implementation: should the tasks be generated and awaited one by one, or all together, or with some specific level of concurrency? Regarding the signature: should it return a `Task>` or a `Task<(TSource, TResult)[]>` or an `IAsyncEnumerable<(TSource, TResult, Exception)>`? So many options... – Theodor Zoulias Jul 14 '21 at 10:36