Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async await using LINQ ForEach()

I have the following code that correctly uses async/await paradigm.

internal static async Task AddReferencseData(ConfigurationDbContext context)
{
    foreach (var sinkName in RequiredSinkTypeList)
    {
        var sinkType = new SinkType() { Name = sinkName };
        context.SinkTypeCollection.Add(sinkType);
        await context.SaveChangesAsync().ConfigureAwait(false);
    }
}

What is the equivalent way to write this if, instead of using foreach(), I want to use LINQ ForEach()? This one, for example, gives compile error.

internal static async Task AddReferenceData(ConfigurationDbContext context)
{
    RequiredSinkTypeList.ForEach(
        sinkName =>
        {
            var sinkType = new SinkType() { Name = sinkName };
            context.SinkTypeCollection.Add(sinkType);
            await context.SaveChangesAsync().ConfigureAwait(false);
        });
}

The only code I got to work without compile error is this.

internal static void AddReferenceData(ConfigurationDbContext context)
{
    RequiredSinkTypeList.ForEach(
        async sinkName =>
        {
            var sinkType = new SinkType() { Name = sinkName };
            context.SinkTypeCollection.Add(sinkType);
            await context.SaveChangesAsync().ConfigureAwait(false);
        });
}

I'm worried that this method has no async signature, only the body does. Is this the correct equivalent of my first block of code above?

like image 561
SamDevx Avatar asked May 15 '15 13:05

SamDevx


3 Answers

No. It isn't. This ForEach doesn't support async-await and requires your lambda to be async void which should only be used for event handlers. Using it will run all your async operations concurrently and won't wait for them to complete.

You can use a regular foreach as you did but if you want an extension method you need a special async version that iterates over the items, executes an async operation and awaits it.

However, you can create one:

Staring from .NET 6.0 you can use Parallel.ForEachAsync:

public async Task ForEachAsync<T>(this IEnumerable<T> enumerable, Func<T, Task> action)
{
    await Parallel.ForEachAsync(
        enumerable,
        async (item, _) => await action(item));
}

Or, avoid the extension method and just call it directly:

await Parallel.ForEachAsync(
    RequiredSinkTypeList,
    async (sinkName, _) =>
    {
        var sinkType = new SinkType() { Name = sinkName };
        context.SinkTypeCollection.Add(sinkType);
        await context.SaveChangesAsync().ConfigureAwait(false);
    });

On older platforms you need to use foreach:

public async Task ForEachAsync<T>(this IEnumerable<T> enumerable, Func<T, Task> action)
{
    foreach (var item in enumerable)
    {
        await action(item);
    }
}

Usage:

internal static async Task AddReferencseData(ConfigurationDbContext context)
{
    await RequiredSinkTypeList.ForEachAsync(async sinkName =>
    {
        var sinkType = new SinkType() { Name = sinkName };
        context.SinkTypeCollection.Add(sinkType);
        await context.SaveChangesAsync().ConfigureAwait(false);
    });
}

A different (and usually more efficient) implementation of ForEachAsync would be to start all the async operations and only then await all of them together but that's only possible if your actions can run concurrently which isn't always the case (e.g. Entity Framework):

public Task ForEachAsync<T>(this IEnumerable<T> enumerable, Func<T, Task> action)
{
    return Task.WhenAll(enumerable.Select(item => action(item)));
}

As was noted in the comments you probably don't want to use SaveChangesAsync in a foreach to begin with. Preparing your changes and then saving them all at once will probably be more efficient.

like image 56
i3arnon Avatar answered Oct 21 '22 09:10

i3arnon


.Net 6 now has

 Parallel.ForEachAsync

used like

await Parallel.ForEachAsync(userHandlers, parallelOptions, async (uri, token) =>
{
    var user = await client.GetFromJsonAsync<GitHubUser>(uri, token);
 
    Console.WriteLine($"Name: {user.Name}\nBio: {user.Bio}");
});

https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel.foreachasync https://www.hanselman.com/blog/parallelforeachasync-in-net-6

like image 3
Murdock Avatar answered Oct 21 '22 09:10

Murdock


The initial example with foreach effectively waits after each loop iteration. The last example invokes List<T>.ForEach() that takes an Action<T> meaning, that your async lambda will compile into a void delegate, as opposed to standard Task.

Effectively, the ForEach() method will invoke "iterations" one by one without waiting for each one to finish. This will also propagate to your method, meaning that AddReferenceData() might finish before the work is done.

So no, it's not an equivalent and behaves quite differently. In fact, assuming it's an EF context, it will blow up as it may not be used across multiple threads concurrently.

Also read http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx mentioned by Deepu for why it's probably better to stick to foreach.

like image 1
Jacek Gorgoń Avatar answered Oct 21 '22 07:10

Jacek Gorgoń