IAsyncEnumerable and await foreach

Return results one at a time as they arrive, using yield return in an async iterator and await foreach on the other end.

Syntaxasync IAsyncEnumerable<T> NameAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)

What an async stream is

A method returning Task<List<Row>> hands back every row at once. The caller waits for the last row before it can look at the first, and all the rows sit in memory together.

IAsyncEnumerable<T> is the other shape. It hands back items one at a time, as each becomes available. The caller can work on item one while item two is still being fetched, and only one item needs to be in memory. That is an async stream.

Producing one

A method marked async, returning IAsyncEnumerable<T> and using yield return, is an async iterator. yield return gives one item to the caller and pauses the method on that line. When the caller asks for the next item, the method continues from there. The compiler rewrites all of this for you.

The body does not run when you call the method. It starts on the first turn of the loop, and stops for good if the loop breaks.

Consuming one

await foreach is foreach that is allowed to wait between items. It is shorthand. The longhand is GetAsyncEnumerator, a while (await MoveNextAsync()) loop reading Current, and await DisposeAsync() in a finally. Same thing.

Cancellation

Give the iterator a CancellationToken parameter marked [EnumeratorCancellation], from System.Runtime.CompilerServices. The caller then supplies a token with .WithCancellation(token) and the compiler routes it into that parameter. Passing the token as a plain argument also works when you control the call site.

What trips people up

A stream can only be read once per enumeration, and await foreach over it a second time runs the whole method again.

Example

Example · csharp
using System.Runtime.CompilerServices;

using CancellationTokenSource cts = new CancellationTokenSource();
int lastSeen = 0;

try
{
    // WithCancellation feeds cts.Token into the marked parameter below.
    await foreach (int value in Counter.CountAsync(5).WithCancellation(cts.Token))
    {
        Console.WriteLine($"got {value}");
        lastSeen = value;

        if (value == 2)
        {
            cts.Cancel();   // stop the producer while it is mid-stream
        }
    }
}
catch (OperationCanceledException)
{
    Console.WriteLine($"cancelled after {lastSeen}");
}

static class Counter
{
    // An async iterator: async, returns IAsyncEnumerable<int>, uses yield return.
    public static async IAsyncEnumerable<int> CountAsync(
        int count,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        for (int i = 1; i <= count; i++)
        {
            await Task.Delay(10, cancellationToken);   // stands in for real I/O
            yield return i;
        }
    }
}

// Output:
// got 1
// got 2
// cancelled after 2

When to use it

  • Return paged API results to a caller who can start work on page one while page two is fetched.
  • Read a multi-gigabyte log file line by line without holding it in memory.
  • Stream rows from a database query instead of materialising the whole result set.
  • Wrap a stream with a stage that filters or batches items without buffering them first.

More examples

One stream feeding another

An async iterator that both consumes and produces a stream is a pipeline stage written as a single method. Each item passes through the whole chain before the next one is fetched, so a chain of ten stages still holds one item at a time. This is how you add filtering, batching or retry logic to a stream without first collecting it into a list.

Example · csharp
await foreach (int value in Streams.EvenAsync(Streams.SourceAsync()))
{
    Console.WriteLine(value);
}

static class Streams
{
    public static async IAsyncEnumerable<int> SourceAsync()
    {
        int[] values = [1, 2, 3, 4, 5, 6];

        foreach (int value in values)
        {
            await Task.Yield();   // gives the rest of the program a turn
            yield return value;
        }
    }

    // A stage: reads one stream, yields a filtered one. Nothing is buffered.
    public static async IAsyncEnumerable<int> EvenAsync(IAsyncEnumerable<int> source)
    {
        await foreach (int value in source)
        {
            if (value % 2 == 0)
            {
                yield return value;
            }
        }
    }
}

// Output:
// 2
// 4
// 6

Buffering compared with streaming

The two methods do the same fetching; only the return type differs. Pick Task<List<T>> when the result is small and the caller needs a count, an index or more than one pass over it. Pick IAsyncEnumerable<T> when the result could be large or unbounded, or when the caller benefits from starting early. A list can always be built from a stream afterwards; a stream cannot be recovered from a list.

Example · csharp
// Buffering: the caller sees nothing until every row is ready.
List<string> buffered = await Rows.GetAllAsync();
Console.WriteLine($"buffered {buffered.Count} rows");

// Streaming: the caller sees row 1 while row 2 is still being fetched.
await foreach (string row in Rows.StreamAllAsync())
{
    Console.WriteLine(row);
}

static class Rows
{
    public static async Task<List<string>> GetAllAsync()
    {
        List<string> rows = [];

        for (int i = 1; i <= 3; i++)
        {
            await Task.Delay(5);
            rows.Add($"row {i}");
        }

        return rows;
    }

    public static async IAsyncEnumerable<string> StreamAllAsync()
    {
        for (int i = 1; i <= 3; i++)
        {
            await Task.Delay(5);
            yield return $"row {i}";
        }
    }
}

// Output:
// buffered 3 rows
// row 1
// row 2
// row 3

The longhand of await foreach

Write this out once and await foreach stops being magic: GetAsyncEnumerator takes the cancellation token, MoveNextAsync returns false at the end of the stream, and DisposeAsync lets the paused iterator run its finally blocks and close files or connections. Use the short form in real code. The long form is worth knowing when you need to pull exactly one item, or peek before deciding whether to continue.

Example · csharp
// await foreach is shorthand. This is the loop it stands for.
IAsyncEnumerable<int> stream = Numbers.ThreeAsync();
IAsyncEnumerator<int> enumerator = stream.GetAsyncEnumerator(CancellationToken.None);

try
{
    while (await enumerator.MoveNextAsync())
    {
        Console.WriteLine(enumerator.Current);
    }
}
finally
{
    // The finally matters: it runs the iterator's own cleanup.
    await enumerator.DisposeAsync();
}

static class Numbers
{
    public static async IAsyncEnumerable<int> ThreeAsync()
    {
        for (int i = 1; i <= 3; i++)
        {
            await Task.Yield();
            yield return i;
        }
    }
}

// Output:
// 1
// 2
// 3

Discussion

  • Be the first to comment on this lesson.