Skip to main content
๐Ÿ“œ WAYPOINT LESSON

Async streams and IAsyncDisposable

โญโญโญ advancedโณ 15 min read๐Ÿ“ Lesson 195 of 265

IAsyncEnumerable pipelines, await using, and backpressure between async producers and consumers.

Async streams and IAsyncDisposable

IAsyncEnumerable<T> is the async IEnumerable: items arrive over time, and await foreach consumes them. An async iterator combines yield return with await:

static async IAsyncEnumerable<Trade> StreamAsync(
    IAsyncEnumerable<string> lines, [EnumeratorCancellation] CancellationToken ct = default)
{
    await foreach (var line in lines.WithCancellation(ct).ConfigureAwait(false))
        if (Trade.TryParse(line, out var t)) yield return t;
}

Backpressure is inherent: the consumer pulls; the producer only produces when pulled (unlike Task-based fan-out, which produces as fast as it can). For bounded buffering semantics, compose streams with Channel<T> (Module 13).

IAsyncDisposable + await using close the lifecycle for resources whose release is itself asynchronous (network connections, streams, DB readers):

await using var conn = await pool.RentAsync(ct);
// DisposeAsync runs on scope exit โ€” even on exception

The trap: DisposeAsync is NOT awaited if you use using instead of await using โ€” the compiler inserts a sync-over-async wait, or worse, fire-and-forget semantics depending on the type. And ConfigureAwait(false) belongs on the await foreach line, applied to the enumerator, not the items.