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
}
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;
}
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
}
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;
}
}
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);
}
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;
}
}
Consumer with cancellation
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await foreach (var order in GetOrdersAsync().WithCancellation(cts.Token))
{
Process(order);
}
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;
}
}
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);
}
}
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;
}
}
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;
}
}
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 returnin anasyncmethod returningIAsyncEnumerable<T>to produce a stream - Use
await foreachto consume it - Always use
[EnumeratorCancellation] CancellationTokenin producers - Use
.WithCancellation(ct)in consumers - Prefer
IAsyncEnumerable<T>overTask<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();
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)