Async streams arrived in C# 8.0 and let asynchronous producers return a sequence of values over time. Instead of waiting for one large Task<List<T>> to complete, you can yield return values as they become available and consume them with await foreach.
Why it matters
Async streams improve throughput and responsiveness in data-heavy paths such as paged APIs, event feeds, and incremental ETL jobs. Consumers can begin work immediately, which reduces perceived latency and avoids unnecessary memory pressure from fully buffered collections.
Cautions
Always assume enumeration can fail midway through a stream and design processing to be retry-safe or idempotent. Also avoid mixing blocking calls into async stream pipelines—stick with asynchronous APIs end-to-end so await foreach can keep threads available while I/O is in flight.
From buffered lists to async streams
Async streams let callers process records as soon as each result arrives instead of waiting for an entire list to finish loading.
Valid since C# 8.0
Without var
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public sealed class PriceService
{
public async Task<List<decimal>> ReadPricesAsync()
{
var results = new List<decimal>();
foreach (var price in new[] { 9.99m, 11.25m, 10.5m })
{
await Task.Delay(20);
results.Add(price);
}
return results;
}
}
public static class Program
{
public static async Task Main()
{
var service = new PriceService();
List<decimal> prices = await service.ReadPricesAsync();
foreach (decimal price in prices)
{
Console.WriteLine($"Buffered price: {price:F2}");
}
}
}
With var
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public sealed class PriceService
{
public async IAsyncEnumerable<decimal> ReadPricesAsync()
{
foreach (var price in new[] { 9.99m, 11.25m, 10.5m })
{
await Task.Delay(20);
yield return price;
}
}
}
public static class Program
{
public static async Task Main()
{
var service = new PriceService();
await foreach (decimal price in service.ReadPricesAsync())
{
Console.WriteLine($"Streamed price: {price:F2}");
}
}
}
Consuming streamed updates with await foreach
Use await foreach to process each asynchronous result in order while naturally composing async validation and filtering logic.
Valid since C# 8.0
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public sealed class SensorReading
{
public SensorReading(DateTime timestampUtc, double temperatureCelsius)
{
TimestampUtc = timestampUtc;
TemperatureCelsius = temperatureCelsius;
}
public DateTime TimestampUtc { get; }
public double TemperatureCelsius { get; }
}
public static class SensorFeed
{
public static async IAsyncEnumerable<SensorReading> ReadingsAsync()
{
var values = new[] { 21.1, 21.4, 22.0, 22.7 };
foreach (double value in values)
{
await Task.Delay(15);
yield return new SensorReading(DateTime.UtcNow, value);
}
}
}
public static class Program
{
public static async Task Main()
{
await foreach (SensorReading reading in SensorFeed.ReadingsAsync())
{
if (reading.TemperatureCelsius >= 22.0)
{
Console.WriteLine($"Alert: {reading.TemperatureCelsius:F1}C at {reading.TimestampUtc:HH:mm:ss}");
}
}
}
}