.NET Async Streams
Introduction
Async streams, introduced in C# 8.0 and .NET Core 3.0, provide a way to work with asynchronous sequences of data. Traditional IEnumerable<T>
collections work well for synchronous data access, but what if you need to asynchronously produce or consume sequences? That's where async streams come in.
Async streams allow you to:
- Process sequences of data that arrive over time
- Handle data that's produced asynchronously
- Yield results one at a time while performing asynchronous operations
In this tutorial, you'll learn how to work with async streams using IAsyncEnumerable<T>
and async iterators in C#.
Understanding Async Streams
The Problem Async Streams Solve
Before async streams, developers faced challenges when dealing with asynchronous data sequences:
- Using
Task<IEnumerable<T>>
meant waiting for the entire collection before processing any items - Creating custom async enumeration patterns was complex and error-prone
- Combining asynchronous operations with enumeration required workarounds
Async streams provide a natural way to handle these scenarios with language-level support.
Key Components
The async streams feature consists of:
IAsyncEnumerable<T>
: An interface representing an asynchronous sequenceIAsyncEnumerator<T>
: An interface for enumerating async sequencesasync
iterator methods: Methods that useyield return
withasync
/await
await foreach
statement: For consuming async streams
Creating Async Streams
Let's look at how to create async streams:
// Method that returns an async stream
public static async IAsyncEnumerable<int> GenerateNumbersAsync(int count, int delay)
{
for (int i = 0; i < count; i++)
{
// Simulate asynchronous work
await Task.Delay(delay);
yield return i;
}
}
The key elements here:
- The return type is
IAsyncEnumerable<T>
- The method is marked with both
async
and usesyield return
- We can use
await
within the iterator
Consuming Async Streams
To consume an async stream, use the await foreach
statement:
// Consuming an async stream
await foreach (var number in GenerateNumbersAsync(5, 1000))
{
Console.WriteLine($"Received: {number}");
}
Output:
Received: 0
Received: 1
Received: 2
Received: 3
Received: 4
Notice that each number is printed as it becomes available, with a 1-second delay between them.
Cancellation Support
Async streams support cancellation via a special attribute:
public static async IAsyncEnumerable<string> GetDataAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 0; i < 10; i++)
{
// Check cancellation before doing work
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(500, cancellationToken);
yield return $"Data {i}";
}
}
To use cancellation with await foreach
:
using var cts = new CancellationTokenSource();
cts.CancelAfter(2000); // Cancel after 2 seconds
try
{
await foreach (var item in GetDataAsync(cts.Token))
{
Console.WriteLine(item);
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled");
}
ConfigureAwait Support
You can configure how the async stream is awaited:
// Use ConfigureAwait(false) with async streams
await foreach (var item in asyncStream.ConfigureAwait(false))
{
// Process item
}
Real-World Examples
Let's look at some practical applications of async streams:
Example 1: Reading File Lines Asynchronously
public static async IAsyncEnumerable<string> ReadLinesAsync(string filePath)
{
using var fileStream = new FileStream(filePath, FileMode.Open);
using var reader = new StreamReader(fileStream);
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (line != null)
yield return line;
}
}
// Usage
await foreach (var line in ReadLinesAsync("largeFile.txt"))
{
// Process each line as it's read, without loading the entire file
ProcessLine(line);
}
Example 2: API Pagination
public static async IAsyncEnumerable<Product> GetAllProductsAsync(HttpClient client)
{
int page = 1;
bool hasMore = true;
while (hasMore)
{
var response = await client.GetAsync($"api/products?page={page}");
response.EnsureSuccessStatusCode();
var pageResult = await response.Content.ReadFromJsonAsync<PagedResponse<Product>>();
if (pageResult?.Items == null || pageResult.Items.Count == 0)
{
hasMore = false;
}
else
{
foreach (var product in pageResult.Items)
{
yield return product;
}
page++;
}
}
}
// Usage
await foreach (var product in GetAllProductsAsync(httpClient))
{
Console.WriteLine($"Processing product: {product.Name}");
}
Example 3: Database Streaming with Entity Framework Core
public static async IAsyncEnumerable<Customer> StreamCustomersAsync(DbContext context)
{
// AsAsyncEnumerable is available in EF Core
await foreach (var customer in context.Customers
.AsAsyncEnumerable())
{
// Maybe do some enrichment or filtering
if (customer.IsActive)
yield return customer;
}
}
// Usage
await foreach (var customer in StreamCustomersAsync(dbContext))
{
await ProcessCustomerDataAsync(customer);
}
Performance Considerations
When working with async streams:
- Lazy Evaluation: Items are produced only as needed
- Memory Efficiency: Process large data sets without loading everything into memory
- Responsiveness: UI or service remains responsive while processing
- Overhead: Be aware that async operations have some overhead
When to Use Async Streams
Async streams are ideal for:
- Processing data that becomes available over time
- Working with large datasets that should be streamed rather than loaded entirely
- APIs that naturally return data in pages or chunks
- Scenarios where both asynchronous operations and enumeration are needed
They might not be necessary when:
- All data is available synchronously
- The collection is small and can be processed in memory
- You need the entire collection before processing any items
Summary
Async streams provide an elegant way to work with asynchronous sequences of data in .NET. They offer:
- A natural programming model for producing and consuming asynchronous data
- Built-in language support with
async
,await foreach
, andIAsyncEnumerable<T>
- Efficient handling of asynchronous sequences with lazy evaluation
- Proper cancellation and configuration support
By using async streams, you can write cleaner, more efficient code when dealing with sequences that require asynchronous operations to produce or process.
Additional Resources
- Microsoft Docs on Async Streams
- .NET API documentation for IAsyncEnumerable
- GitHub sample repository on Async Streams
Exercises
- Create an async stream that simulates reading sensor data at regular intervals
- Implement a method that returns weather data from an API as an async stream
- Write a program that processes a large CSV file using async streams
- Create an async stream that monitors a directory for new files and yields their contents
- Implement pagination with async streams for a database query using Entity Framework Core
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)