async Streams: await foreach
IAsyncEnumerable<T> for data that arrives over time, producing and consuming async sequences.
Producing: async iterator with yield return
async IAsyncEnumerable<int> PollAsync(CancellationToken ct = default)
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(10, ct);
yield return i;
}
}
yield return inside an async method makes an async iterator (.NET Core
3.0+). Items stream out as they're produced โ the consumer starts before
the producer finishes.
Consuming: await foreach
await foreach (int item in PollAsync(ct))
Console.WriteLine(item);
await foreach awaits each MoveNextAsync โ the loop suspends while waiting
for the next item instead of blocking a thread.
Why not Task<List<T>>?
Task<List<T>> says "everything, eventually" โ the caller waits for the
END before processing item one. IAsyncEnumerable<T> says "items over
time" โ process as they arrive, cancel mid-stream, keep memory flat. Choose
by arrival pattern, not style.
CancellationToken flows through
WithCancellation(ct) attaches the caller's token to the stream; inside
the producer, honor the token you were given. The
[EnumeratorCancellation] attribute on the default parameter merges caller
and local tokens โ the pattern used by the BCL.
Check your understanding
- Which type for a log tail? (IAsyncEnumerable โ items arrive over time.)
- What does await foreach await? (Each item's availability.)