0Pricing
C# Academy · Lesson

IAsyncEnumerable & await foreach

Stream data asynchronously with IAsyncEnumerable, yield in async iterators, and consume with await foreach.

IAsyncEnumerable & await foreach is a free C# Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why IAsyncEnumerable?

IAsyncEnumerable<T> (introduced in C# 8 and .NET Core 3) enables asynchronous streaming — you yield items one at a time as they become available, without loading everything into memory first. Think: reading a database cursor row by row, or streaming API responses.

Writing an Async Iterator

Declare a method returning IAsyncEnumerable<T> and use yield return inside an async method. The await and yield return keywords coexist naturally.

public async IAsyncEnumerable<int> GenerateNumbersAsync(int count)
{
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(100); // simulate async work per item
        yield return i;
    }
}

Consuming with await foreach

Consume an IAsyncEnumerable<T> with await foreach. Each iteration waits for the next item asynchronously — the consumer and producer are naturally paced.

await foreach (var number in GenerateNumbersAsync(10))
{
    Console.WriteLine(number);
}

// With cancellation:
var cts = new CancellationTokenSource();
await foreach (var n in GenerateNumbersAsync(100)
    .WithCancellation(cts.Token))
{
    if (n > 50) { cts.Cancel(); break; }
    Console.WriteLine(n);
}

Streaming from a Database

EF Core supports streaming query results with AsAsyncEnumerable(). This reads rows one at a time without loading the full result set into memory — critical for large tables.

public async IAsyncEnumerable<Product> StreamProductsAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    await foreach (var product in _db.Products
        .Where(p => p.IsActive)
        .AsAsyncEnumerable()
        .WithCancellation(ct))
    {
        yield return product;
    }
}

Streaming HTTP Responses

Stream large responses to clients without buffering the entire payload. The client receives data progressively while the server continues processing.

app.MapGet("/stream/products", (ProductRepository repo) =>
{
    // Returns an IAsyncEnumerable — ASP.NET Core streams it as JSON array
    return repo.StreamAllAsync();
});

// Or with custom serialization:
app.MapGet("/stream/events", async (HttpResponse response, EventStore store) =>
{
    response.ContentType = "application/json";
    await foreach (var e in store.StreamAsync())
    {
        await response.WriteAsJsonAsync(e);
        await response.Body.FlushAsync();
    }
});

The EnumeratorCancellation Attribute

Apply [EnumeratorCancellation] to the CancellationToken parameter of an async iterator so WithCancellation() on the consumer side flows the token into the producer.

public async IAsyncEnumerable<LogEntry> TailLogAsync(
    string logFile,
    [EnumeratorCancellation] CancellationToken ct = default)
{
    while (!ct.IsCancellationRequested)
    {
        var newLines = await ReadNewLinesAsync(logFile, ct);
        foreach (var line in newLines)
            yield return new LogEntry(line);
        await Task.Delay(500, ct);
    }
}

// Consumer:
await foreach (var entry in TailLogAsync("/var/log/app.log")
    .WithCancellation(cts.Token))
{
    Console.WriteLine(entry.Message);
}

LINQ over IAsyncEnumerable

The System.Linq.Async NuGet package provides async LINQ operators (WhereAwait, SelectAwait, ToListAsync) for IAsyncEnumerable.

// dotnet add package System.Linq.Async

var expensiveProducts = await repo.StreamAllAsync()
    .WhereAwait(async p => await pricing.IsExpensiveAsync(p.Id))
    .SelectAwait(async p => await enricher.EnrichAsync(p))
    .Take(10)
    .ToListAsync();

// Or use built-in Where/Select that don't need async:
var names = repo.StreamAllAsync()
    .Where(p => p.IsActive)
    .Select(p => p.Name);

Merging Multiple Async Streams

To process multiple async streams concurrently, use Channel as a fan-in point — write from multiple producers and read from a single consumer.

async IAsyncEnumerable<T> Merge<T>(
    params IAsyncEnumerable<T>[] streams)
{
    var channel = Channel.CreateUnbounded<T>();

    var producers = streams.Select(async stream =>
    {
        await foreach (var item in stream)
            await channel.Writer.WriteAsync(item);
    });

    _ = Task.WhenAll(producers)
            .ContinueWith(_ => channel.Writer.Complete());

    await foreach (var item in channel.Reader.ReadAllAsync())
        yield return item;
}

Performance: Buffering vs Streaming

Always stream when processing large datasets. Loading everything into a list first (ToListAsync()) holds all data in memory simultaneously — streaming processes one item at a time.

// BAD: loads all 1M records into memory
var allOrders = await _db.Orders.ToListAsync();
foreach (var order in allOrders)
    await ProcessAsync(order);

// GOOD: processes one record at a time
await foreach (var order in _db.Orders.AsAsyncEnumerable())
    await ProcessAsync(order);
// Memory usage: O(1) vs O(n)

Error Handling in Async Iterators

Wrap the entire await foreach in a try/catch to handle errors from the producer. Errors in the iterator propagate to the consumer at the current iteration point.

try
{
    await foreach (var item in RiskyStreamAsync())
    {
        await ProcessAsync(item);
    }
}
catch (HttpRequestException ex)
{
    Console.Error.WriteLine($"Stream error: {ex.Message}");
}
catch (OperationCanceledException)
{
    Console.WriteLine("Stream cancelled.");
}

Quick Check

What is the primary memory advantage of IAsyncEnumerable over returning a List?

Recap: IAsyncEnumerable & await foreach

Key takeaways:

  • IAsyncEnumerable<T>: stream items asynchronously, one at a time
  • yield return in async methods produces async iterators
  • await foreach: consume the stream with cancellation via .WithCancellation()
  • [EnumeratorCancellation]: propagates consumer's token into the iterator
  • EF Core: use AsAsyncEnumerable() for memory-efficient large queries
  • Processing large datasets: stream instead of ToListAsync()

Frequently asked questions

Is the “IAsyncEnumerable & await foreach” lesson free?

Yes — the full text of “IAsyncEnumerable & await foreach” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.

What will I learn in “IAsyncEnumerable & await foreach”?

Stream data asynchronously with IAsyncEnumerable, yield in async iterators, and consume with await foreach. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start C# Academy?

No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “IAsyncEnumerable & await foreach” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this C# Academy lesson?

Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. IAsyncEnumerable & await foreach
  2. System.Threading.Channels
  3. ValueTask & Avoiding Allocations
  4. ConfigureAwait & Synchronization Context
← Back to C# Academy