Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 15 – Asynchronous programming

Async streams with IAsyncEnumerable

Produce C# async streams with yield return, read them with await foreach, pass a token with EnumeratorCancellation, and query them with AsyncEnumerable.

  • Advanced
  • 25 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 25 minutes free
Free: An async iterator hands over each item as soon as it has it · await foreach pulls one item at a time, so the reader sets the pace · .NET 10 has LINQ for async streams built in. With a pass: 3 in-depth parts and 3 interview answers, about 17 minutes.

What you will learn

  • Write and consume async streams with yield return and await foreach
  • Predict how far an async iterator runs when its consumer stops early
  • Query async streams with the built-in System.Linq.AsyncEnumerable
  • Pass cancellation into async iterators with [EnumeratorCancellation]
  • Read JSON and NDJSON input one value at a time, without loading it whole

Before you start

A report reads 3 pages of orders from an API that needs 100 ms per page. Collected into a list first, the first order reaches the report after all 3 pages, 300 ms; read as an async stream, it arrives after the first page, 100 ms, and only one page is in memory at a time.

You will write async streams with yield return, read them with await foreach, see how far a stream runs when its reader stops early, and query streams with the LINQ operators that .NET 10 ships.

An async iterator hands over each item as soon as it has it

pages.cs reads 3 pages of 2 orders each, first as a stream, then collected into a list:

pages.cs: a paginated API as a stream and as a list C# · pages/pages.cs
// An async stream: an async method that returns IAsyncEnumerable<T> and uses yield return. Each order is handed to
// the loop as soon as its page has arrived, while the next page is still to be fetched.
await foreach (Order order in OrdersAsync())
{
    Console.WriteLine($"  the loop got order {order.Id} ({order.Amount})");
}

// The same orders collected into a list first: nothing reaches the caller before the last page has arrived.
List<Order> all = await AllOrdersAsync();
Console.WriteLine($"the list arrived with {all.Count} orders, after {Api.PagesFetched} pages");

static async IAsyncEnumerable<Order> OrdersAsync()
{
    for (int page = 1; page <= 3; page++)
    {
        Order[] orders = await Api.GetPageAsync(page);
        foreach (Order order in orders)
        {
            yield return order;
        }
    }
}

static async Task<List<Order>> AllOrdersAsync()
{
    Api.PagesFetched = 0;
    var all = new List<Order>();
    for (int page = 1; page <= 3; page++)
    {
        all.AddRange(await Api.GetPageAsync(page, quiet: true));
    }

    return all;
}

static class Api
{
    public static int PagesFetched;

    // A paginated API: page n holds orders n01 to n02, and every page takes 100 ms to arrive.
    public static async Task<Order[]> GetPageAsync(int page, bool quiet = false)
    {
        if (!quiet)
        {
            Console.WriteLine($"fetching page {page}");
        }

        await Task.Delay(100);
        PagesFetched++;
        return [new(page * 100 + 1, 250 * page), new(page * 100 + 2, 120 * page)];
    }
}

record Order(int Id, int Amount);

Output

fetching page 1
  the loop got order 101 (250)
  the loop got order 102 (120)
fetching page 2
  the loop got order 201 (500)
  the loop got order 202 (240)
fetching page 3
  the loop got order 301 (750)
  the loop got order 302 (360)
the list arrived with 6 orders, after 3 pages

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run pages.cs

OrdersAsync is an async iterator: An async method that returns IAsyncEnumerable<T> and uses yield return. Its body runs only as its reader asks for items, each time up to the next yield return.: an async method that returns IAsyncEnumerable<Order> and uses yield return. Its result is an async stream: A sequence whose items arrive asynchronously, typed IAsyncEnumerable<T>. An async iterator produces it with yield return, and await foreach reads it one item at a time., and await foreach reads it. The run interleaved the two: the loop printed orders 101 and 102 before page 2 was even fetched, while the list version handed over nothing until the last page had arrived. Step through the first orders:

Interactive · message by message The first orders of pages.cs, step by step Step through the stream. The await foreach loop is on the left, the async iterator in the middle, the API on the right.
The first orders of pages.cs, step by step, step 7 of 7. Messages so far: await foreach to OrdersAsync: MoveNextAsync(); OrdersAsync to Paginated API: GetPageAsync(1); Paginated API to OrdersAsync: 2 orders; OrdersAsync to await foreach: order 101, pause; OrdersAsync to await foreach: order 102; OrdersAsync to Paginated API: GetPageAsync(2); OrdersAsync to await foreach: order 201, pause.await foreachthe consumerOrdersAsyncasked for page 2Paginated API100 ms a pageMoveNextAsync()GetPageAsync(1)2 ordersorder 101, pauseorder 102GetPageAsync(2)order 201, pause

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The loop asks. await foreach calls GetAsyncEnumerator, then MoveNextAsync. Only now does the body of OrdersAsync start: calling it ran none of its code.
  2. Page 1 is fetched. The iterator prints "fetching page 1" and awaits the API, so MoveNextAsync returns a task that is not complete. Nothing waits on a thread for the 100 ms.
  3. Page 1 arrives. The page arrives with orders 101 and 102. The iterator resumes after its await.
  4. yield return 101. At yield return the iterator hands order 101 to the loop and stops where it is, with its page and its place kept. The loop prints "the loop got order 101".
  5. The next item is ready. The next MoveNextAsync resumes the iterator after its yield. Order 102 is already in the page, so it comes back at once, without a call to the API.
  6. Page 2, only when asked. Only when the loop asks for a third order does the iterator fetch page 2, while the orders of page 1 have already been used.
  7. Order 201. Page 2 arrives and the iterator yields order 201. The pattern repeats until page 3 is used up and MoveNextAsync answers false.

The interfaces mirror the ones a foreach uses: IAsyncEnumerable<T> hands out an IAsyncEnumerator<T>, whose MoveNextAsync returns a ValueTask<bool> that the loop awaits, and the enumerator is disposed asynchronously when the loop ends. Async streams came with C# 8 and .NET Core 3.0.

Checkpoint

Check: Async iterators and await foreach

  1. Question 1 of 2 When does the body of the async iterator OrdersAsync() start to run?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: When await foreach asks for the first item, with MoveNextAsync

    An async iterator runs a piece at a time: from the start, or the last yield return, to the next yield return, each time the reader asks.

  2. Question 2 of 2 An API takes 100 ms per page and the report needs 5 pages. Read as an async stream, after about how many milliseconds does the first order reach the loop?

    Type a number.

    A hint

    How many pages must arrive before the iterator can yield its first order?

    Show the answer: Question 2 of 2

    Answer: 100 ms

    The first order is yielded as soon as page 1 has arrived, at 100 ms. Collected into a list first, it would arrive after all 5 pages, 500 ms.

await foreach pulls one item at a time, so the reader sets the pace

early.cs reads a stream of 5 pages and stops after 3 orders:

early.cs: a reader that stops after 3 orders C# · early/early.cs
// await foreach asks for one item at a time, so the producer runs only as far as the consumer reads. Breaking out
// of the loop disposes the stream: the iterator's finally block runs, and the pages after it are never fetched.
int pagesFetched = 0;
int seen = 0;
await foreach (int id in OrderIdsAsync())
{
    Console.WriteLine($"  the loop got order {id}");
    if (++seen == 3)
    {
        Console.WriteLine("the loop breaks after 3 orders");
        break;
    }
}

Console.WriteLine($"pages fetched: {pagesFetched} of 5");

async IAsyncEnumerable<int> OrderIdsAsync()
{
    try
    {
        for (int page = 1; page <= 5; page++)
        {
            Console.WriteLine($"fetching page {page}");
            await Task.Delay(100);
            pagesFetched++;
            yield return page * 100 + 1;
            yield return page * 100 + 2;
        }
    }
    finally
    {
        Console.WriteLine("the iterator's finally block runs: connection closed");
    }
}

Output

fetching page 1
  the loop got order 101
  the loop got order 102
fetching page 2
  the loop got order 201
the loop breaks after 3 orders
the iterator's finally block runs: connection closed
pages fetched: 2 of 5

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run early.cs

The iterator fetched page 2 only when the loop asked for a third order, and when the loop broke out, the iterator’s finally block ran, printing “connection closed”, because leaving await foreach disposes the enumerator. Pages 3, 4 and 5 were never fetched: 2 pages of 5. A stream runs exactly as far as its reader asks, so a fast producer cannot bury a slow consumer under items. This built-in back-pressure: A consumer setting the pace of its producer. An async stream has it built in: the producer runs only when the consumer asks for the next item, so items never pile up between them. is the difference from a buffer: when the producer should run ahead of its reader, as when messages arrive whether or not anyone reads them, a channel holds them in between, a tool of the concurrency module.

Cleanup after the last yield return

Code after the loop of an iterator runs only if the reader reads to the end. Put cleanup, such as closing a connection, in a finally block or a using declaration, which also run when the reader stops early.

Checkpoint

Check: How far a stream runs for its reader

  1. Question 1 of 2 A stream yields 2 orders per page from 5 pages. The loop breaks after its 5th order. How many pages were fetched?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 3: the 5th order is the first of page 3, and nothing after it was asked for

    An async stream pulls: each MoveNextAsync runs the iterator to its next yield, so the work done matches what the reader takes.

  2. Question 2 of 2 The loop over OrderIdsAsync() in early.cs breaks after 3 orders. What printed "connection closed"?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The iterator's finally block, run when the break disposed the enumerator

    Put the cleanup of an iterator in finally or using: both run when the reader stops early.

.NET 10 has LINQ for async streams built in

linq.cs filters, maps and totals a stream with the System.Linq.AsyncEnumerable class:

linq.cs: Where, Select, ToListAsync and SumAsync on a stream C# · linq/linq.cs
// .NET 10 has LINQ for async streams in the shared framework: System.Linq.AsyncEnumerable. Its operators
// read an IAsyncEnumerable<T> and return one, and the ...Async methods (ToListAsync, SumAsync) end the query.
IAsyncEnumerable<int> squares = AsyncEnumerable.Range(1, 10)
    .Where(n => n % 2 == 0)
    .Select(n => n * n);

List<int> list = await squares.ToListAsync();
Console.WriteLine($"even squares: {string.Join(",", list)}");
Console.WriteLine($"their sum: {await squares.SumAsync()}");
Console.WriteLine($"AsyncEnumerable is in the assembly {typeof(AsyncEnumerable).Assembly.GetName().Name}");

// The operators also take async lambdas, which get the stream's cancellation token.
IAsyncEnumerable<string> labels = AsyncEnumerable.Range(1, 3)
    .Select(async (int n, CancellationToken cancellationToken) =>
    {
        await Task.Delay(10, cancellationToken); // a lookup per item
        return $"order {n}";
    });
Console.WriteLine($"labels: {string.Join("; ", await labels.ToListAsync())}");

Output

even squares: 4,16,36,64,100
their sum: 220
AsyncEnumerable is in the assembly System.Linq.AsyncEnumerable
labels: order 1; order 2; order 3

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run linq.cs

Where and Select return new streams, and nothing runs until ToListAsync or SumAsync reads them: the even squares 4, 16, 36, 64 and 100, with the sum 220. The operators also take async lambdas, which receive the stream’s cancellation token. AsyncEnumerable came from the assembly System.Linq.AsyncEnumerable, which is part of .NET 10.

New in .NET 10

Before .NET 10 these operators came from the community System.Linq.Async package. Microsoft lists the change as a breaking change: a project that references that package gets ambiguous calls on .NET 10, and removes the reference or moves to version 7.0.0 of the package.

Two more tools complete the set. [EnumeratorCancellation] marks the iterator parameter that receives the token of WithCancellation. It goes on the iterator’s CancellationToken parameter, written last with default as its value, as in Microsoft’s tutorial. JsonSerializer.DeserializeAsyncEnumerable reads a JSON array, or one JSON value per line, as an async stream.

Checkpoint

Check: LINQ for async streams in .NET 10

  1. One question squares = AsyncEnumerable.Range(1, 10).Where(n => n % 2 == 0).Select(n => n * n). When does the filtering run?

    Choose one answer.

    Show the answer: One question

    Answer: When something reads squares, such as ToListAsync or SumAsync

    LINQ over async streams is deferred, like LINQ over IEnumerable: the query runs when a terminal method such as ToListAsync reads it, and again for every read.

Cancelling an async stream: [EnumeratorCancellation] and WithCancellation Pro: opens with a Pro, Premium or Ultimate pass

A token can reach an async iterator in two ways, and one of them fails without an error. This part runs all four combinations side by side and shows the compiler warning that catches the silent one.

In this part:

  • Four ways a token can reach an iterator, run side by side
  • The warning that says a token goes nowhere: CS8425

3 runnable examples (C#, Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Streaming JSON without loading it: DeserializeAsyncEnumerable, measured Pro: opens with a Pro, Premium or Ultimate pass

JsonSerializer.DeserializeAsyncEnumerable turns a JSON array, or a stream of JSON values, into an async stream. This part measures how early the first item arrives, reads NDJSON-style values, and reproduces what one broken value does.

In this part:

  • A JSON array read 64 bytes at a time
  • One value per line, and what a broken line does

1 runnable example (C#) · about 4 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

A lab: read NDJSON orders lazily, skip bad lines and total them Pro: opens with a Pro, Premium or Ultimate pass

The lab combines the three ideas of this lesson: an async iterator that yields as it reads, a token that stops it, and an AsyncEnumerable query that totals what it yields. Five checks say when it is right.

In this part:

  • The rules and the five checks
  • Building it: a line loop, a guarded parse, a token

1 runnable example (C#) · about 4 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise

The self-check covers the first three sections: when an async iterator runs, how far it runs for a reader that stops early, and what the AsyncEnumerable operators return. Your weakest topic gets the first question, and each choice comes with its reason:

Practice that adapts to you

Practise this lesson

6 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 A method returns Task<List<Order>> after reading 50 pages of 1,000 orders each. What does changing it to an async iterator returning IAsyncEnumerable<Order> change?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: The first order reaches the caller after one page, and memory holds a page instead of 50,000 orders

    An async stream improves the time to the first item and the memory held, not the total time of a sequential producer.

  2. Question 2 of 6 Which signature declares an async iterator?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: async IAsyncEnumerable<Order> OrdersAsync() with yield return in its body

    The async and the yield return together, with IAsyncEnumerable<T> as the return type, make an async iterator.

  3. Question 3 of 6 A sensor pushes a reading every 10 ms whether or not anyone reads, and the consumer writes each one to a database in 50 ms. Is an async iterator enough on its own?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: No: the iterator can only produce when asked, so readings that arrive meanwhile need a buffer such as a channel

    Async streams pull. When items are pushed at their own pace, put a channel between producer and consumer, the subject of the concurrency module.

  4. Question 4 of 6 A reader takes the first 3 orders of a stream with 2 orders per page and stops. How many pages are fetched?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: 2

    The work a stream does is the work its reader asks for, page by page.

  5. Question 5 of 6 A project moves to .NET 10 and keeps a reference to the community System.Linq.Async package. What does Microsoft's breaking-change note say can happen?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: Calls such as Where become ambiguous, so the reference is removed or the package moved to 7.0.0

    .NET 10's System.Linq.AsyncEnumerable replaces the community package. Remove the direct reference when upgrading.

  6. Question 6 of 6 What does AsyncEnumerable.Range(1, 10).Where(n => n % 2 == 0).Select(n => n * n).SumAsync() return?

    Type a number.

    Show the answer: Question 6 of 6

    Answer: 220

    The even numbers 2, 4, 6, 8 and 10 squared are 4, 16, 36, 64 and 100, whose sum is 220, the second line of linq.cs.

Interview questions

Fresher · What is the difference between IEnumerable<T> and IAsyncEnumerable<T>?

Both are sequences read one item at a time, and both can be written as iterators with yield return. With IEnumerable<T>, MoveNext returns a bool at once, so getting an item that needs I/O would block the thread. With IAsyncEnumerable<T>, MoveNextAsync returns a ValueTask<bool> that the reader awaits, so the producer can await a page, a row or a message between items without holding a thread. You read it with await foreach, which also disposes the enumerator asynchronously when the loop ends or breaks. Choose IAsyncEnumerable<T> when items arrive over time from I/O, and IEnumerable<T> when they are computed or already in memory.

  1. Mid to senior

    An export endpoint loads 2 million order rows into a List<Order>, serialises the list and sends it. At peak the service runs out of memory. Rewrite its data path with async streams, and say what changes for memory, the time to the first byte and cancellation.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A dashboard stops reading a sensor stream with WithCancellation(token), but the sensor connection stays open and keeps polling. The async iterator takes a CancellationToken parameter and passes it to every call. What is wrong, how do you prove it, and how do you fix it?

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    A worker reads messages from a queue faster than it can write them to a database. Would you connect the reading and the writing with an IAsyncEnumerable<T> or a Channel<T>? Explain back-pressure in both, and what happens when the writer fails.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An async iterator, an async method that returns IAsyncEnumerable<T> with yield return, hands over each item as soon as it has it, and await foreach reads it.
  • The iterator runs only as far as its reader asks, and leaving the loop early disposes it, which runs its finally blocks; that is back-pressure without a buffer.
  • .NET 10 ships LINQ for async streams in System.Linq.AsyncEnumerable, whose operators run only when a method such as ToListAsync reads the result.

Terms in this lesson

async iterator
An async method that returns IAsyncEnumerable<T> and uses yield return. Its body runs only as its reader asks for items, each time up to the next yield return.
async stream
A sequence whose items arrive asynchronously, typed IAsyncEnumerable<T>. An async iterator produces it with yield return, and await foreach reads it one item at a time.
back-pressure
A consumer setting the pace of its producer. An async stream has it built in: the producer runs only when the consumer asks for the next item, so items never pile up between them.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.