Behavioral Beginner 6 min read

Iterator Pattern in C#: yield, IEnumerable, IAsyncEnumerable

Iterator pattern in C# / .NET 10: how yield return and IEnumerable are this pattern, and when IAsyncEnumerable lets you stream remote data lazily.

Table of contents
  1. What problem does the Iterator pattern solve in C#?
  2. How does yield return give you Iterator for free?
  3. When should you use IAsyncEnumerable in .NET 10?
  4. When does it pay to implement IEnumerable manually?
  5. How does Iterator compare to Composite and Visitor?
  6. What does a real .NET 10 example look like?
  7. Where should you read next in this series?

The order-fulfilment job needs to walk every order placed in the last 24 hours, ship the ones that are paid, and email the ones that are unpaid for too long. The "walk every order" part has three common shapes: load them all into memory and foreach; page through them in chunks of 1,000; stream them as they arrive from a remote service. All three shapes have one thing in common — the consumer just wants to write foreach (var order in source) and not care about pagination, allocation, or async details.

The Iterator pattern is the answer the language has built in. Define a uniform "give me the next element" surface; let the source hide its internals (array, tree, paged API, database cursor). Modern C# has been doing this since version 1 with IEnumerable<T> and since C# 8 with IAsyncEnumerable<T>. This article spends its time on the modern version of the pattern, because the textbook hand-written iterator class is the version you will almost never write.

What problem does the Iterator pattern solve in C#?

The pattern earns its place when callers want a uniform "one element at a time" surface but the source's internal layout varies or is hidden. Three concrete shapes:

  1. Hide internal layout. The cart is a tree (Composite); the catalogue is a list; the order log is a paginated API. Callers should write the same foreach against all three.
  2. Lazy evaluation. Reading 5 GB of orders into a List<> crashes the box. An iterator yields one row at a time and the GC reclaims it.
  3. Composable transformations. orders.Where(...).Take(100) is composed lazily — none of the source is evaluated until the consumer asks for the first element. The pattern enables this.

What is not an Iterator problem: "I want to fan out work to multiple workers" — that is closer to Mediator or message buses. Iterator is single-consumer, sequential.

How does yield return give you Iterator for free?

The C# yield keyword turns a regular method into a state-machine that implements IEnumerable<T>:

public IEnumerable<Order> RecentOrders()
{
    foreach (var page in _api.PagedOrders(pageSize: 1000))
    {
        foreach (var order in page.Items)
            yield return order;
    }
}

The caller writes:

foreach (var order in RecentOrders())
{
    if (order.IsPaid) await _shipping.ShipAsync(order);
}

What the compiler generates: a class <RecentOrders>d__0 implementing IEnumerator<Order> with a _state field, a MoveNext that resumes from each yield, and Dispose to clean up the inner foreach. You wrote ten lines of straight-line code; the compiler wrote the textbook Iterator pattern.

The structural picture (what foreach actually does):

sequenceDiagram
    participant C as Caller (foreach)
    participant E as Enumerator
    C->>E: GetEnumerator()
    loop until done
        C->>E: MoveNext()
        E-->>C: true / false
        C->>E: Current
        E-->>C: Order #N
    end
    C->>E: Dispose()

MoveNext runs your method up to the next yield return; Current returns the yielded value. The loop ends when MoveNext returns false (the method returned).

When should you use IAsyncEnumerable in .NET 10?

IAsyncEnumerable<T> adds the same shape over async sources. The keyword combination is await foreach plus await yield return:

public async IAsyncEnumerable<Order> RecentOrdersAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    var pageToken = (string?)null;
    do
    {
        var page = await _api.GetOrdersPageAsync(pageToken, ct);
        foreach (var order in page.Items)
            yield return order;
        pageToken = page.NextToken;
    }
    while (pageToken is not null);
}

Caller:

await foreach (var order in RecentOrdersAsync().WithCancellation(ct))
{
    if (order.IsPaid) await _shipping.ShipAsync(order, ct);
}

Three things this gives you:

When does it pay to implement IEnumerable manually?

Almost never. Two cases where it does:

For everything else — paged APIs, in-memory transformations, streamed file reads — yield is the right answer.

How does Iterator compare to Composite and Visitor?

Pattern What is uniform? Direction
Iterator One element at a time, externally driven Caller pulls
Composite Operations work on leaves and groups Recursive (often combined with Iterator)
Visitor Operations added to fixed node types Visitor pushes

The split: Iterator pulls one element at a time; Visitor walks the whole structure performing an operation; Composite gives them both a uniform tree shape to work on. A typical .NET app uses all three: a Composite cart, an Iterator that yields its leaves, and a Visitor that computes tax across them.

What does a real .NET 10 example look like?

Combining yield, IAsyncEnumerable, and IEnumerable for the same domain:

public sealed record Order(string Id, decimal Total, bool IsPaid);

public interface IOrderSource
{
    // Synchronous, in-memory
    IEnumerable<Order>      AllInMemory();
    // Async, paginated remote
    IAsyncEnumerable<Order> StreamRecent(CancellationToken ct = default);
}

public sealed class HybridOrderSource : IOrderSource
{
    private readonly List<Order> _local = LoadFromCsv();
    private readonly IPaginatedApi _remote;

    public HybridOrderSource(IPaginatedApi remote) => _remote = remote;

    public IEnumerable<Order> AllInMemory()
    {
        foreach (var order in _local)
            yield return order;
    }

    public async IAsyncEnumerable<Order> StreamRecent(
        [EnumeratorCancellation] CancellationToken ct = default)
    {
        string? token = null;
        do
        {
            var page = await _remote.GetPageAsync(token, ct);
            foreach (var order in page.Items)
                yield return order;
            token = page.NextToken;
        }
        while (token is not null);
    }

    private static List<Order> LoadFromCsv() => /* ... */ new();
}

// Caller A — synchronous
foreach (var order in source.AllInMemory().Where(o => o.IsPaid))
    Console.WriteLine($"local paid: {order.Id}");

// Caller B — async
await foreach (var order in source.StreamRecent(ct))
    if (order.IsPaid)
        await _shipping.ShipAsync(order, ct);

The consumer code is identical in shape (foreach / await foreach). The producer hides whether the source is in memory, on disk, or across the network. That uniformity is the value of the Iterator pattern.

A pragmatic note: yield return is the most-used design pattern in .NET that nobody calls a design pattern. Every LINQ query you chain, every IEnumerable<T> you iterate, every async stream is the Iterator pattern in action. The pattern's success is exactly why nobody talks about it any more — it has dissolved into the language.

Frequently asked questions

How does yield return implement the Iterator pattern?
When the C# compiler sees yield return in a method that returns IEnumerable<T>, it generates a state-machine class behind the scenes — exactly the Iterator pattern's enumerator. Each yield return becomes a paused state; each MoveNext resumes from where the last yield stopped. You write straight-line code; the compiler writes the iterator class.
When should I use IAsyncEnumerable instead of IEnumerable?
Use IAsyncEnumerable<T> when each element is produced by an async operation — paginated API calls, streamed database queries, message-bus consumers. You get backpressure, cancellation, and await foreach syntax. Use IEnumerable<T> when each step is synchronous and the source is in-memory or a local file.
When should I implement IEnumerator manually instead of using yield?
Almost never in application code. The compiler-generated iterator handles state, exceptions, and IDisposable correctly. Hand-rolled IEnumerator<T> is only worth it for custom collection types that need to expose multiple parallel iterators with shared state, or for edge-case performance work where the generated state machine adds measurable overhead.
How does the Iterator pattern interact with the Composite pattern?
Composite defines a tree; Iterator visits the tree's elements one at a time. The two compose: a cart.Flatten() method that yields every leaf is Iterator on top of Composite. The Composite.Flatten() extension method in our Composite chapter is exactly this combination.