Loading...

Asynchronous Streams in C# (IAsyncEnumerable)

Learn asynchronous streams in C# with IAsyncEnumerable to process data step by step using modern async iteration patterns.

In modern .NET applications, data often arrives in chunks and over time: streaming records from the network, log lines, sensor data… IAsyncEnumerable<T> lets you consume such streams in an asynchronous and lazy manner. On the producer side you use async + yield return, and on the consumer side you use await foreach.


What Is IAsyncEnumerable?

IAsyncEnumerable<T> allows you to produce and consume a sequence of data asynchronously. Unlike synchronous IEnumerable<T>, each step may involve awaiting (e.g., due to I/O latency). To consume, write await foreach; at each step the loop awaits until the next element is ready.


// Simple consumption
await foreach (var item in GetNumbersAsync())
{
    Console.WriteLine(item);
}

Async Iterator: async + yield return

When writing an asynchronous producer method, return type is IAsyncEnumerable<T>; inside the body you can await and produce items with yield return.


using System;
using System.Collections.Generic;
using System.Threading.Tasks;

static async IAsyncEnumerable<int> GetNumbersAsync()
{
    for (int i = 1; i <= 5; i++)
    {
        await Task.Delay(500); // simulate I/O or time-consuming work
        yield return i;        // a new number every 500 ms
    }
}

// Consumption
await foreach (var n in GetNumbersAsync())
{
    Console.WriteLine($"Arrived: {n}");
}

Consuming with await foreach

await foreach pulls items from asynchronous streams without blocking the UI or the worker thread. On each iteration it waits until data is ready; once it arrives, the loop resumes.


await foreach (var entry in ReadLogEntriesAsync())
{
    // Process as soon as the entry arrives
    Process(entry);
}

Error Handling and using

You can use standard try/catch when consuming async streams. If an error occurs in the stream or during consumption, it surfaces while executing await foreach.


try
{
    await foreach (var item in GetNumbersAsync())
        Console.WriteLine(item);
}
catch (Exception ex)
{
    Console.WriteLine("Error: " + ex.Message);
}

For asynchronous resources, use IAsyncDisposable and await using.


await using var source = await OpenResourceAsync();
await foreach (var x in source.StreamAsync())
{
    // ...
}

Cancellation Support (CancellationToken)

To stop long-running streams, you can cancel consumption. On the producer side, capture the token with [EnumeratorCancellation] and check it periodically.


using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

static async IAsyncEnumerable<int> CountAsync(
    int start, int count, [EnumeratorCancellation] CancellationToken ct = default)
{
    for (int i = 0; i < count; i++)
    {
        ct.ThrowIfCancellationRequested();
        await Task.Delay(300, ct);
        yield return start + i;
    }
}

// Consumption
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); // cancel after 1s
await foreach (var n in CountAsync(10, 100, cts.Token))
{
    Console.WriteLine(n);
}

Comparison: IEnumerable<Task<T>> vs IAsyncEnumerable<T>


Async Line-by-Line File Read (Wrapper Example)

The following example produces a stream that reads a file asynchronously, line by line. Each line is emitted to the consumer via yield return as soon as it becomes available.


using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

static async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
    using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read,
                                  bufferSize: 4096, useAsync: true);
    using var sr = new StreamReader(fs);

    while (!sr.EndOfStream)
    {
        var line = await sr.ReadLineAsync(); // when each line arrives
        if (line is not null)
            yield return line;
    }
}

// Consumption
await foreach (var line in ReadLinesAsync("log.txt"))
{
    Console.WriteLine(line);
}

Simple Filtering over a Stream (Pipeline)

Asynchronous streams can be chained to build a pipeline: produce → filter → transform → consume.


static async IAsyncEnumerable<int> FilterAsync(IAsyncEnumerable<int> source)
{
    await foreach (var n in source)
    {
        if (n % 2 == 0)          // pass even numbers
            yield return n * 10; // transform
    }
}

// Usage
await foreach (var x in FilterAsync(GetNumbersAsync()))
{
    Console.WriteLine(x); // 20, 40, ...
}

UI Scenario: await foreach in WPF

In WPF, long-running streams won’t freeze the UI; you can update the interface as each item arrives. If UI updates are required, marshal back to the Dispatcher on the UI thread.


// Example: read sensor values from a stream and write to the UI
await foreach (var v in ReadSensorAsync())
{
    Dispatcher.Invoke(() => txtStatus.Text = $"Latest value: {v}");
}

Tips and Considerations


TL;DR

  • IAsyncEnumerable<T> is for asynchronous, lazy data streams.
  • Producer: async + yield return; Consumer: await foreach.
  • Use [EnumeratorCancellation] and CancellationToken for cancellation.
  • IAsyncEnumerable<T> can be more efficient than IEnumerable<Task<T>> in memory and timing.
  • If the UI needs updates, switch to the UI thread with Dispatcher.

Related Articles