await foreach asynchronously processes items as they become available, avoiding the need to build an entire collection before iteration. It suits sequences generated or retrieved through asynchronous operations. section 14.0
Understand the compiler-level shape behind the loop:
- Treat foreach as syntax for GetEnumerator, MoveNext, Current, and disposal; await foreach applies the same shape asynchronously. section 14.1
- Use IAsyncEnumerable<T>.GetAsyncEnumerator to obtain an IAsyncEnumerator<T>; retrieve each item with MoveNextAsync, which returns ValueTask<bool>. section 14.1
- Let the compiler combine the yield return and await transformations, generating an asynchronous state machine whose steps advance through MoveNextAsync. section 14.2
Generate and consume delayed values
private async IAsyncEnumerable<int> AsyncYieldDemo() { yield return 1; await Task.Delay(1000); yield return 2; } public async Task UseAsyncYieldDemo() { await foreach (var current in AsyncYieldDemo()) { Console.WriteLine($"Got {current}"); } } section 14.2
Apply practical safeguards:
- Pass cancellation with collection.WithCancellation(token), and mark an iterator parameter with [EnumeratorCancellation] when the compiler should forward that token. section 14.3
- Remember that cancellation is only a flag: the iterator must observe the token and stop, typically by passing it to an asynchronous operation. section 14.8
- Avoid converting asynchronous sequences to blocking IEnumerable<T> unless necessary for non-asynchronous callers; blocking can cause performance problems and deadlocks. section 14.8
Listing 14.2 Async yield return example
private async IAsyncEnumerable<int> AsyncYieldDemo() #1
{
yield return 1;
await Task.Delay(1000); #2
yield return 2;
}
public async Task UseAsyncYieldDemo()
{
await foreach(var current in AsyncYieldDemo()) #3
{
Console.WriteLine($"Got {current}");
}
}
Inspect a complete async iterator that combines yield return, Task.Delay, and await foreach. section 14.2
