skip to content
Khalid Abuhakmeh logo Khalid Abuhakmeh

IAsyncEnumerable and await foreach in C#

Learn how IAsyncEnumerable and await foreach work in C#: consume and produce async streams, convert to a List, and avoid the List.ForEach async/await bug.

6 min read Updated

IAsyncEnumerable<T> is a C# interface for a sequence whose items arrive asynchronously, and await foreach is the loop that consumes it. Use them when you want to process data as it becomes available instead of waiting for the whole collection to load.

I started thinking about this after scrolling the Mastodon timeline, where fellow .NET developer Bill Seipel had an unexpected experience with List.ForEach and async/await. At first glance, I thought he was modifying the collection he was iterating over, but the issue was much more subtle. We’ll cover that mistake, then the right tools for async iteration.

What is IAsyncEnumerable in C#?

IAsyncEnumerable<T> arrived with C# 8 and .NET Core 3.0 (it’s in .NET Standard 2.1 too). It’s the asynchronous cousin of IEnumerable<T>: instead of a blocking MoveNext(), the enumerator exposes MoveNextAsync(), which returns a ValueTask<bool>. Each item can be awaited.

How does it differ from the alternatives?

Return type Behavior Time to first item
IEnumerable<T> Synchronous; blocks the thread while waiting on each item Fast, but blocks
Task<List<T>> Asynchronous, but buffers everything before returning Slowest; waits for all
IAsyncEnumerable<T> Asynchronous and streaming; items arrive one at a time Fast, no blocking

That’s the key idea: Task<List<T>> buffers, IAsyncEnumerable<T> streams. I covered a practical web angle in How to use IAsyncEnumerable with Blazor Stream Rendering.

How do I consume an async stream with await foreach?

Put await in front of foreach inside an async method.

await foreach (var number in RangeAsync(1, 5))
{
Console.WriteLine(number);
}

Each iteration awaits the next item. The thread is free while you wait, and you handle each element the moment it shows up. You can also pass a token with .WithCancellation(token) and configure the awaiter with .ConfigureAwait(false).

How do I produce an IAsyncEnumerable?

Write an async method that returns IAsyncEnumerable<T>, and mix await with yield return.

static async IAsyncEnumerable<int> RangeAsync(
int start,
int count,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (var i = 0; i < count; i++)
{
await Task.Delay(100, cancellationToken);
yield return start + i;
}
}

The [EnumeratorCancellation] attribute (from System.Runtime.CompilerServices) tells the compiler to flow the token passed to WithCancellation(...) into your method’s parameter. Without it, a token given through WithCancellation is never seen by your code.

What is a common async iteration mistake with List.ForEach?

For those unaware, the List type has a ForEach method, which allows the user to pass an Action<T>. The method passes the iteration’s item, allowing you to execute something similar to a foreach method. In a sense, it’s syntactic sugar. In another, it’s a relic before async/await. Let’s see an example code that can get you in trouble.

List<string> projects = new();
List<string> measures = ["cm", "m", "km"];
measures.ForEach(async x =>
{
var result = await GetResultOfMeasure(x);
projects.Add(result);
});
Console.WriteLine(projects.Count);
async Task<string> GetResultOfMeasure(string s)
{
await Task.Delay(100);
return $"Measured as {s}";
}

Looking at the code, we see an async/await, so what’s the problem? Well, let’s run the code and see what the result is.

0

What?! Why did we get a result of 0?

If you’re using a tool like ReSharper or JetBrains Rider you’ll have seen a warning around the async keyword.

Avoid using 'async' lambda when delegate type returns 'void'

Oops! While the Action uses async/await semantics, nothing awaits the iterative process. This means our iterations may or may not be complete by the time we reach the Console.WriteLine statement. Since we use Task.Delay, it’s likely we won’t.

If you’re using something that fluctuates in performance, you will likely pull out your hair due to indeterminate behavior.

So, what’s the fix?

Fixing List.ForEach with ForEachAsync

The most straightforward solution is not to use ForEach but to use a simple for each iteration instead.

List<string> projects = new();
List<string> measures = ["cm", "m", "km"];
foreach (var measure in measures)
{
var result = await GetResultOfMeasure(measure);
projects.Add(result);
}
Console.WriteLine(projects.Count);

But honestly, where’s the fun in that?! Another approach is to write a ForEachAsync extension method.

public static class ListExtension
{
public static async Task ForEachAsync<T>(
this List<T> collection,
Func<T, Task> action)
{
foreach (var i in collection) await action(i);
}
}

From here, you can tweak the original sample code to be sure to await the entire iterative process.

List<string> projects = new();
List<string> measures = ["cm", "m", "km"];
await measures.ForEachAsync(async x =>
{
var result = await GetResultOfMeasure(x);
projects.Add(result);
});
Console.WriteLine(projects.Count);
async Task<string> GetResultOfMeasure(string s)
{
await Task.Delay(100);
return $"Measured as {s}";
}

There you have it. Pretty cool, right?! That fixes a list you already have in memory. When the data arrives piece by piece, there’s a better tool.

How do I convert an IAsyncEnumerable to a List?

There’s no built-in ToListAsync() in the base class library. You have two options. The first is a manual await foreach:

var numbers = new List<int>();
await foreach (var number in RangeAsync(1, 5))
{
numbers.Add(number);
}

The second is the System.Linq.Async NuGet package, which adds LINQ operators for async streams, including ToListAsync():

var numbers = await RangeAsync(1, 5).ToListAsync();

(In .NET 10, async LINQ operators ship in the box via System.Linq.AsyncEnumerable; I’d check your target framework before adding the package.) Note that converting to a list throws away the streaming benefit, since you wait for everything.

When should you use IAsyncEnumerable?

Reach for it when:

  • You’re streaming large or remote data, like paged API results, files, or message queues.
  • You want a fast time to first item instead of waiting on a full buffer.
  • You’re using EF Core, where AsAsyncEnumerable() lets you await foreach over query results as they’re read.
  • Items are produced over time and the consumer shouldn’t block.

Skip it when you need the whole set anyway (counts, sorting, multiple passes). A plain Task<List<T>> is simpler there.

Conclusion

await foreach and IAsyncEnumerable<T> give C# proper asynchronous iteration, and they’re the right answer when a collection’s items arrive over time. The List.ForEach trap compiles and looks “correct,” and with good tooling like JetBrains’, you get a hint about the problem, but it doesn’t scream at you. It might make sense for .NET to include a ForEachAsync method on List, but it might also make sense to Obsolete ForEach entirely since it likely does more harm than good.

What do you think? Let me know if you’ve ever run into this issue. You can see the full thread on Mastodon here, along with debugging videos describing what is happening.

Thanks for reading and sharing my work with friends and colleagues. Cheers.

Comments

Related Posts