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?
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 await
s it.
However, you can create one:
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);
});
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.
.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
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With