DEV Community

Cover image for IAsyncEnumerable Explained — Async Streams in .NET
Libin Tom Baby
Libin Tom Baby

Posted on

IAsyncEnumerable Explained — Async Streams in .NET

IAsyncEnumerable, yield return, cancellation tokens, vs IEnumerable, real-world streaming

Most developers know IEnumerable<T> and async/await.

IEnumerable<T> — lets you iterate a sequence.

async/await — lets you do I/O without blocking threads.

But IAsyncEnumerable<T> — introduced in C# 8 — combines both, and it solves a problem that neither alone handles well.

It lets you iterate a sequence asynchronously — yielding items one at a time as they become available, without loading everything into memory first.


The Problem They Solve

Imagine reading 100,000 records from a database and returning them to the caller.

Option 1: Return everything at once

public async Task<List<Order>> GetAllOrdersAsync()
{
    return await db.Orders.ToListAsync(); // ❌ Loads everything into memory
}
Enter fullscreen mode Exit fullscreen mode

Memory spikes. The caller waits for the entire load.

Option 2: Synchronous yield

public IEnumerable<Order> GetAllOrders()
{
    foreach (var order in db.Orders) // ❌ Blocks the thread
        yield return order;
}
Enter fullscreen mode Exit fullscreen mode

Streams items one at a time, but blocks the thread on every read.

Option 3: IAsyncEnumerable — the right way

public async IAsyncEnumerable<Order> GetAllOrdersAsync()
{
    await foreach (var order in db.Orders.AsAsyncEnumerable())
        yield return order; // ✅ Non-blocking, streamed one at a time
}
Enter fullscreen mode Exit fullscreen mode

Items are produced and consumed one at a time, without blocking the thread, without loading everything into memory.


How It Works

IAsyncEnumerable<T> is the async version of IEnumerable<T>.

How to Produce an Async Stream

Use async, return IAsyncEnumerable<T>, and yield return each item.

// Producer
public async IAsyncEnumerable<int> GenerateNumbersAsync()
{
    for (int i = 0; i < 10; i++)
    {
        await Task.Delay(100); // Simulates async work
        yield return i;
    }
}
Enter fullscreen mode Exit fullscreen mode

The producer can await before yielding each item. Each call to yield return sends one item to the consumer, then pauses until the consumer requests the next one.


How to Consume an Async Stream

The consumer uses await foreach.

// Consumer
await foreach (var number in GenerateNumbersAsync())
{
    Console.WriteLine(number);
}
Enter fullscreen mode Exit fullscreen mode

This is the async equivalent of foreach.

Each iteration awaits the next item without blocking the thread.


Cancellation Support

Always support cancellation in long-running async streams.

Producer with cancellation

public async IAsyncEnumerable<Order> GetOrdersAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    await foreach (var order in db.Orders.AsAsyncEnumerable().WithCancellation(ct))
    {
        yield return order;
    }
}
Enter fullscreen mode Exit fullscreen mode

Consumer with cancellation

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

await foreach (var order in GetOrdersAsync().WithCancellation(cts.Token))
{
    Process(order);
}
Enter fullscreen mode Exit fullscreen mode

Without cancellation, a long-running stream can run indefinitely.


IAsyncEnumerable vs IEnumerable

IEnumerable<T> IAsyncEnumerable<T>
Execution Synchronous Asynchronous
Thread blocking Yes No
Use case In-memory data I/O-bound data sources
Iteration foreach await foreach
Cancellation No Yes (CancellationToken)
Memory Can load all at once Streams item by item

IAsyncEnumerable vs Task<List<T>>

Task<List<T>> IAsyncEnumerable<T>
Returns All items at once One at a time
Memory usage High (all in memory) Low (streaming)
Time to first item After all items loaded Immediately
Best for Small datasets Large or infinite streams

Real-World Scenarios

Scenario 1: Streaming database records

public async IAsyncEnumerable<Product> GetLowStockProductsAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    await foreach (var product in db.Products
        .Where(p => p.StockLevel < 10)
        .AsAsyncEnumerable()
        .WithCancellation(ct))
    {
        yield return product;
    }
}
Enter fullscreen mode Exit fullscreen mode

Scenario 2: Streaming an external API

public async IAsyncEnumerable<WeatherReading> StreamWeatherAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    while (!ct.IsCancellationRequested)
    {
        var reading = await _weatherApi.GetLatestAsync(ct);
        yield return reading;
        await Task.Delay(1000, ct);
    }
}
Enter fullscreen mode Exit fullscreen mode

Scenario 3: Reading a large file line by line

public async IAsyncEnumerable<string> ReadLinesAsync(
    string path,
    [EnumeratorCancellation] CancellationToken ct = default)
{
    await foreach (var line in File.ReadLinesAsync(path, ct))
    {
        yield return line;
    }
}
Enter fullscreen mode Exit fullscreen mode

Scenario 4: Exposing async streams from an API endpoint (ASP.NET Core)

[HttpGet("stream")]
public async IAsyncEnumerable<Order> StreamOrders(
    [EnumeratorCancellation] CancellationToken ct)
{
    await foreach (var order in _service.GetOrdersAsync(ct))
    {
        yield return order;
    }
}
Enter fullscreen mode Exit fullscreen mode

ASP.NET Core supports returning IAsyncEnumerable<T> directly from action methods.


Interview-Ready Summary

  • IAsyncEnumerable<T> lets you stream data asynchronously — one item at a time
  • Use yield return in an async method returning IAsyncEnumerable<T> to produce a stream
  • Use await foreach to consume it
  • Always use [EnumeratorCancellation] CancellationToken in producers
  • Use .WithCancellation(ct) in consumers
  • Prefer IAsyncEnumerable<T> over Task<List<T>> when datasets are large, unbounded, or I/O-bound
  • ASP.NET Core can return IAsyncEnumerable<T> directly from controller actions

A strong interview answer:

"IAsyncEnumerable allows asynchronous iteration — yielding items one at a time as they're available, instead of loading everything into memory first. It's ideal for large database queries, external API streams, and file processing. You produce it with yield return in an async method, and consume it with await foreach."


Add-On — LINQ and IAsyncEnumerable

As of .NET 9, LINQ supports IAsyncEnumerable<T> natively.

// .NET 9+
var filtered = await GetOrdersAsync()
    .Where(o => o.Amount > 100)
    .Take(50)
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

Before .NET 9, you needed the System.Linq.Async NuGet package for this.
Now it's built in — making async streams a first-class citizen alongside standard LINQ.


Top comments (0)