1. Introduction
You're already familiar with async and await. They work great for "one-off" async actions, like downloading a single file. But what if data arrives as a stream, or a resource requires asynchronous cleanup?
The issue with async "collections": Imagine you need to fetch millions of records from a database. If a method returns Task<List<T>>, you wait until all data is loaded into memory. That's inefficient and causes delays. A synchronous IEnumerable<T> also doesn't fit when each element must be obtained asynchronously.
The issue with async disposal: IDisposable and using handle synchronous cleanup well. But what if closing a network connection or flushing buffers to disk is itself asynchronous? You can't use await inside a synchronous Dispose(), which leads to blocking the thread or incorrect cleanup.
To solve these problems, IAsyncEnumerable<T> and IAsyncDisposable were introduced.
2. Asynchronous data streams
IAsyncEnumerable<T> is the async counterpart of IEnumerable<T>. It lets you produce sequence elements asynchronously one by one, without waiting for all data to be ready.
When do you need it?
- Reading big files line-by-line asynchronously: e.g., gigabyte logs.
- Streaming data from the network or database: API results that arrive in chunks.
- Implementing server streaming APIs: e.g., gRPC Streaming.
- Any scenario where data is generated or arrives asynchronously and should be processed incrementally.
How does it work?
- IAsyncEnumerable<T>: an interface with the method GetAsyncEnumerator(CancellationToken cancellationToken). The cancellation token is important!
- IAsyncEnumerator<T>: an interface with a ValueTask<bool> MoveNextAsync() (move to next) and Current (current element). It also implements IAsyncDisposable.
- await foreach: conveniently iterates over IAsyncEnumerable<T>. The compiler calls MoveNextAsync() and accesses Current for you. Crucially, await foreach guarantees calling DisposeAsync() on the enumerator after iteration completes, even on errors.
Creating IAsyncEnumerable<T> with async yield return
You can use yield return inside an async method that returns IAsyncEnumerable<T>. This lets you build async generators. Your method can use await to pause generation, wait for an async operation, and then resume.
Example: Simple async generator
async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 0; i < 3; i++)
{
Console.WriteLine($"Generating: {i}");
await Task.Delay(100); // Imitation of async work
yield return i;
}
}
// Usage:
async Task ConsumeAsyncNumbers()
{
await foreach (var number in GenerateNumbersAsync())
{
Console.WriteLine($"Received: {number}");
}
}
// Call: await ConsumeAsyncNumbers();
Example: Read file lines asynchronously
async IAsyncEnumerable<string> ReadFileLinesAsync(string filePath)
{
using var reader = new StreamReader(filePath); // 'using' here (StreamReader implements IAsyncDisposable)
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
yield return line;
}
}
// Usage:
async Task ProcessFileAsync()
{
await File.WriteAllLinesAsync("data.txt", new[] { "String 1", "String 2", "String 3" });
await foreach (var line in ReadFileLinesAsync("data.txt"))
{
Console.WriteLine($"Processed stroka: {line}");
}
}
// Call: await ProcessFileAsync();
Example: Async generator with cancellation (CancellationToken)
async IAsyncEnumerable<int> GetCancelableSequence(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken token = default)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested(); // Check cancellation
await Task.Delay(200, token); // Task.Delay also supports cancellation via token
yield return i;
}
}
// Usage:
async Task ConsumeAndCancel()
{
var cts = new CancellationTokenSource(500); // Cancel after 500ms
try
{
await foreach (var num in GetCancelableSequence(cts.Token))
{
Console.WriteLine($"Received: {num}");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Generation is cancelled!");
}
}
// Call: await ConsumeAndCancel();
The attribute [EnumeratorCancellation] allows passing a CancellationToken into the async generator. This makes it possible to cancel the iteration if the caller requests cancellation via CancellationTokenSource. Without this attribute the token won't be automatically passed to GetAsyncEnumerator.
3. Asynchronous resource management
The problem with synchronous IDisposable
The Dispose() method in IDisposable is synchronous (void Dispose()). You can't use await inside it. If closing a DB connection or flushing buffers to disk are long async operations, a synchronous Dispose() will block the thread, which is bad for async applications.
Solution: IAsyncDisposable
IAsyncDisposable fixes this. It contains a single method: ValueTask DisposeAsync() — an async cleanup method.
await using
This is the async counterpart of using. It's meant for objects that implement IAsyncDisposable.
- await using guarantees that DisposeAsync() will be called when the code block where the resource is declared completes (or when exiting due to an exception).
- Allows correct async resource cleanup, avoiding blocking.
Example: Basic IAsyncDisposable and await using
class MyAsyncResource : IAsyncDisposable
{
public MyAsyncResource() => Console.WriteLine("Resource is opened.");
public async ValueTask DisposeAsync()
{
Console.WriteLine("Starting asynchronous cleanup...");
await Task.Delay(200); // Imitation of async cleanup
Console.WriteLine("Asynchronous cleanup completed.");
}
}
// Using await using
async Task UseAndDisposeResource()
{
await using var resource = new MyAsyncResource();
Console.WriteLine("Resource is used...");
} // resource.DisposeAsync() is called automatically here
// Call: await UseAndDisposeResource();
Example: Multiple await using blocks
async Task UseMultipleResources()
{
await using var res1 = new MyAsyncResource();
await using var res2 = new MyAsyncResource();
Console.WriteLine("Using both resources...");
} // resources are disposed in LIFO order (Last In, First Out): res2.DisposeAsync() is called first, then res1.DisposeAsync().
// Call: await UseMultipleResources();
Compatibility of IAsyncEnumerable<T> with IAsyncDisposable
Important: IAsyncEnumerator<T> (used by await foreach) itself implements IAsyncDisposable. This means that if your async generator uses resources (like StreamReader in the example above) that can be disposed asynchronously, await foreach will take care of it. It will call DisposeAsync() on the enumerator when iteration completes.
GO TO FULL VERSION