2

Let's say that I have the following sample code:

private static async Task Main(string[] args)
{
    var result = Enumerable.Range(0, 3).Select(x => TestMethod(x)).ToArray();
    Console.ReadKey();
}

private static int TestMethod(int param)
{
    Console.WriteLine($"{param} before");
    Thread.Sleep(50);
    Console.WriteLine($"{param} after");
    return param;
}

The TestMethod will run to completion 3 times, so I'll see 3 pairs of before and after:

0 before
0 after
1 before
1 after
2 before
2 after

Now, I need to make TestMethod asynchronous:

private static async Task<int> TestMethod(int param)
{
    Console.WriteLine($"{param} before");
    await Task.Delay(50);
    Console.WriteLine($"{param} after");
    return param;
}

How can I write a similar Select expression for this async method? If I just use an async lambda Enumerable.Range(0, 3).Select(async x => await TestMethod(x)).ToArray();, that won't work because it won't wait for completion, so before parts will be called first:

0 before
1 before
2 before
2 after
0 after
1 after

Note that I don't want to run all 3 calls in parallel - I need them to execute one by one, starting the next one only when the previous one has fully finished and returned the value.

Aleksey Shubin
  • 1,960
  • 2
  • 20
  • 39
  • What about just using `foreach`? – JSteward Feb 24 '19 at 15:29
  • @JSteward definitely an option, but LINQ-style is just much shorter and elegant. I'll make an extension method wrapping `foreach`, I just hoped there is a solution in the standard library already. – Aleksey Shubin Feb 24 '19 at 15:50
  • @Aleksey Shubin Do you consider my answer as correct or not? – Alan Turing Feb 24 '19 at 17:00
  • So you're saying that other requirements stipulate that `TestMethod` be `async` but in this particular case you want to keep it running synchronously? If not, I see no reason to make the method `async`. If yes, you could return `TestMethod(x).Result`. – Gert Arnold Feb 24 '19 at 17:40
  • @ArturMustafin no, in your answer you block the thread, losing all advantages of async execution. Calling `TestMethod(x).Result` would be the same and easier. – Aleksey Shubin Feb 27 '19 at 09:35
  • 1
    @GertArnold I want to run it *asynchronously* but not *in parallel* - a bit different things. In my real task, I wanted that to make multiple calls to Entity Framework's `DbContext.SaveChangesAsync` which doesn't allow a parallel execution, but still there is an advantage of asynchronous call - it doesn't block the calling thread. – Aleksey Shubin Feb 27 '19 at 09:39
  • @AlekseyShubin "Note that I don't want to run all 3 calls in parallel - I need them to execute one by one, starting the next one only when the previous one has fully finished and returned the value." – Alan Turing Feb 28 '19 at 20:44
  • @ArturMustafin I don't see your point. Yes, I don't want it in parallel, but I still want it asynchronous - see [this comment](https://stackoverflow.com/questions/54853352/linq-select-analogue-for-async-method/54888004?noredirect=1#comment96571750_54853352) for details. Your answer makes it synchronous. – Aleksey Shubin Mar 02 '19 at 13:08

6 Answers6

5

I run into this requirement regularly, and I'm not aware of any built-in solution for addressing it as at C# 7.2. I generally just fall back to using await on each asynchronous operation within a foreach, but you could go for your extension method:

public static class EnumerableExtensions
{
    public static async Task<IEnumerable<TResult>> SelectAsync<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, Task<TResult>> asyncSelector)
    {
        var results = new List<TResult>();
        foreach (var item in source)
            results.Add(await asyncSelector(item));
        return results;
    }
}

You would then call await on the SelectAsync:

static async Task Main(string[] args)
{
    var result = (await Enumerable.Range(0, 3).SelectAsync(x => TestMethod(x))).ToArray();
    Console.ReadKey();
}

The disadvantage of this approach is that the SelectAsync is eager, not lazy. C# 8 promises to introduce async streams, which will allow this to be lazy again.

Douglas
  • 53,759
  • 13
  • 140
  • 188
  • Nice solution! The return type `Task>` could create false expectations of lazy evaluation though. Returning `Task` could be more suitable. – Theodor Zoulias Oct 30 '20 at 12:41
3

In C# 8 (the next major version as of the time of writing) there will be support for IAsyncEnumrable<T> where you can write async for each loops:

await foreach (var item in GetItemsAsync())
    // Do something to each item in sequence

I expect there will be a Select extension method to do projection but if not then it is not difficult to write your own. You will also be able to create IAsyncEnumrable<T> iterator blocks with yield return.

Martin Liversage
  • 104,481
  • 22
  • 209
  • 256
1

I guess this is what OP actually want, since I've spent a whole day solving the same question as OP wondered. But I want it to be done in fluent mehtod.

By Martin Liversage's answer, I found the correct way: IAsyncEnumerable and System.Linq.Async pacakge, which are available in C# 8.0, is what I want.

Example:

private static async Task Main(string[] args)
{
    var result = await Enumerable.Range(0, 3)
        .ToAsyncEnumerable()
        .SelectAwait(x => TestMethod(x))
        .ToArrayAsync();
    Console.ReadKey();
}

private static async Task<int> TestMethod(int param)
{
    Console.WriteLine($"{param} before");
    await Task.Delay(50);
    Console.WriteLine($"{param} after");
    return param;
}
TomCW
  • 51
  • 6
  • Nice. But did you forgot an await? `var result = await Enumerable...` – Theodor Zoulias Oct 30 '20 at 12:55
  • 1
    Yes, I am. Sorry about that. – TomCW Oct 31 '20 at 17:04
  • Nice solution! Alas, async enumerables are only supported for .NET Core, not for .NET Framework. – Aleksey Shubin Nov 01 '20 at 08:23
  • 1
    @AlekseyShubin you can use `IAsyncEnumerable`s on .NET Framework too. First you need to install the Microsoft.Bcl.AsyncInterfaces package, and then add the line 8 in the .csproj file of your project. [Here](https://bartwullems.blogspot.com/2020/01/asynchronous-streams-using.html) are the detailed instructions. – Theodor Zoulias Nov 02 '20 at 20:24
0

You'll have to be aware what an Enumerable object is. An Enumerable object, isn't the enumeration itself. It gives you the possibility to get an Enumerator object. If you've got an Enumerator object, you can ask for the first elements of a sequence, and once you've got an enumerated element you can ask for the next one.

So creating an object that enables you to enumerate over a sequence, has nothing to do with the enumerating itself.

Your Select function only returns an Enumerable object, it does not start the enumeration. The enumeration is started by ToArray.

At it's lowest level, enumeration is done as follows:

IEnumerable<TSource> myEnumerable = ...
IEnumerator<TSource> enumerator = myEnumerable.GetEnumerator();
while (enumerator.MoveNext())
{
     // there is still an element in the enumeration
     TSource currentElement = enumerator.Current;
     Process(currentElement);
}

ToArray will internally call GetEnumerator and MoveNext. So to make your LINQ statement async, you will need a ToArrayAsync.

The code is fairly simple. I'll show you ToListAsync as an extension method, which is a little bit easier to make.

static class EnumerableAsyncExtensions
{
    public static async Task<List<TSource>> ToListAsync<TSource>(
       this IEnumerable<Task<TSource>> source)
    {
        List<TSource> result = new List<TSource>();
        var enumerator = source.GetEnumerator()
        while (enumerator.MoveNext())
        {
            // in baby steps. Feel free to do this in one step
            Task<TSource> current = enumerator.Current;
            TSource awaitedCurrent = await current;
            result.Add(awaitedCurrent);
        }
        return result;
    } 
}

You'll only have to create this once. You can use it for any ToListAsync where you'll have to await for each element:

 var result = Enumerable.Range(0, 3)
     .Select(i => TestMethod(i))
     .ToListAsync();

Note that the return of Select is IEnumerable<Task<int>>: an object that makes it possible to enumerate a sequence of Task<int> objects. In a foreach, every cycle you get a Task<int>

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • I wonder what I did wrong to deserve a downvote. If you don't comment when you downvote, I can't improve my answer and will never learn about what is wrong – Harald Coppoolse Feb 27 '19 at 07:23
  • I wonder too why someone downvoted. Very interesting information about `Enumerable`, I didn't realize it. I came to a very similar `ToListAsync` extension method, except that I used `foreach` instead of working with Enumerable directly (see the answer I added). My understanding is that `foreach` does basically the same under the hood. Is it correct, or do I miss something? – Aleksey Shubin Feb 27 '19 at 09:45
  • 1
    foreach does internally calls `GetEnumerator()` / `MoveNext() ` / `Current`. So usually it is much easier to use foreach and `yield return` if you want to return an `IEnumerable<...>` instead of a `List<...>`. I only use `GetEnumerator` and `MoveNext` if the enumeration depends on what I read. A good example is `Any`. Try to code that using a foreach and compare that with the simple `return enumerator.MoveNext();` – Harald Coppoolse Feb 27 '19 at 12:37
  • This solution is a bit dangerous, because it depends on the source `IEnumerable>` being non-materialized. The caller could easily make the mistake to materialize the deferred enumerable of tasks, by calling `ToList` for example, and this would change completely the behavior by starting all tasks at once. For this reason I would suggest adding a check that the `source` cannot be cast to a materialized collection: `if (source is ICollection>) throw new ArgumentException();` – Theodor Zoulias Oct 30 '20 at 12:51
-2

You can use syncronzation primitives

    class Program
    {
        static async Task Main(string[] args)
        {
            var waitHandle = new AutoResetEvent(true);
            var result = Enumerable
                .Range(0, 3)
                .Select(async (int param) => {
                    waitHandle.WaitOne();
                    await TestMethod(param);
                    waitHandle.Set();
                }).ToArray();
            Console.ReadKey();
        }
        private static async Task<int> TestMethod(int param)
        {
            Console.WriteLine($"{param} before");
            await Task.Delay(50);
            Console.WriteLine($"{param} after");
            return param;
        }
    }
Alan Turing
  • 2,482
  • 17
  • 20
-2

Looks like there is no other solution currently (until C# 8.0) except enumerating it manually. I made an extension method for that (returns a List instead of Array as it's simpler):

public static async Task<List<T>> ToListAsync<T>(this IEnumerable<Task<T>> source)
{
    var result = new List<T>();
    foreach (var item in source)
    {
        result.Add(await item);
    }
    return result;
}
Aleksey Shubin
  • 1,960
  • 2
  • 20
  • 39