Understanding IAsyncEnumerable in C#: A Guide to Async Streams
An introduction to IAsyncEnumerable<T> and async streams in C#. Learn how to use `yield return` with `async` methods to create and consume streams of data asynchronously, improving performance and reducing memory usage.
Since C# 5, async
and await
have revolutionized how we write asynchronous code. However, they were primarily designed for single, asynchronous operations that return a single result (Task<T>
). What if you need to work with a sequence of results that are generated asynchronously? This is the problem that async streams, represented by the IAsyncEnumerable<T>
interface, were introduced to solve in C# 8.
The Problem: Asynchronous Sequences
Imagine you have a method that needs to fetch a large number of items from a database or a remote API. The API might support paging, so you have to make multiple requests to get all the data.
The Old Way (before async streams):
You had two main options, neither of which was ideal.
Return
Task<List<T>>
: You could wait for all the asynchronous operations to complete, collect all the results into a list, and then return the entire list at once.public async Task<List<Item>> GetItemsAsync() { var allItems = new List<Item>(); var page = 0; while (true) { var items = await _apiClient.GetPageAsync(page++); if (!items.Any()) break; allItems.AddRange(items); } return allItems; // Returns everything at once }
The problem here is that the caller has to wait for all the data to be fetched before it can process the first item. It also requires loading the entire dataset into memory, which can be a problem for large result sets.
Use Callbacks: You could use a callback-based approach, but this often leads to more complex and less readable code.
The Solution: IAsyncEnumerable<T>
and await foreach
Async streams provide a much more elegant solution. A method can now return an IAsyncEnumerable<T>
, which allows it to yield return
items as they become available asynchronously. The caller can then consume this stream using an await foreach
loop.
The New Way (with async streams):
public async IAsyncEnumerable<Item> GetItemsAsync()
{
var page = 0;
while (true)
{
var items = await _apiClient.GetPageAsync(page++);
if (!items.Any()) break;
foreach (var item in items)
{
yield return item; // Yield each item as it becomes available
}
}
}
Now, the calling code can start processing items as soon as the first page is fetched, without waiting for the entire collection.
Consuming the async stream:
await foreach (var item in GetItemsAsync())
{
// Process each item as it arrives
Console.WriteLine($"Processing item: {item.Name}");
}
This code is not only more responsive and memory-efficient, but it's also remarkably clean and readable. It looks almost identical to a regular synchronous foreach
loop.
How it Works
When you await foreach
, the compiler generates the complex state machine code needed to:
- Call the async iterator method (
GetItemsAsync
). await
the next item from the stream.- If an item is available, execute the body of the loop.
- Repeat until the stream is exhausted.
- Properly dispose of the enumerator.
This allows you to pull items from the source one at a time, asynchronously, without blocking the thread.
Where is it Useful?
Async streams are incredibly useful in any scenario where you are working with a sequence of data that is fetched asynchronously:
- Paging through a remote API: As shown in our example.
- Reading data from a database: EF Core 6 and later support returning
IAsyncEnumerable<T>
from LINQ queries, allowing you to stream results from the database without loading them all into memory.await foreach (var user in db.Users.AsAsyncEnumerable()) { // ... }
- Processing data from a message queue or a real-time data source.
- Reading a large file from disk or a network stream.
Conclusion
IAsyncEnumerable<T>
and async streams are a powerful addition to the C# language that elegantly solve the problem of working with asynchronous sequences of data. By allowing you to produce and consume items one at a time, asynchronously, they enable you to write code that is more responsive, more memory-efficient, and more readable. For any I/O-bound operation that returns a collection of items, async streams should be your go-to solution.