Summary: in this tutorial, you will learn how to handle async streams using the IAsyncEnumerable<T>
interface.
Introduction to the IAsyncEnumerable interface
Suppose you have an async method GetMessage()
that returns a message after one second:
async Task<string> GetMessage(int n)
{
await Task.Delay(1000);
return $"Message #{n}";
}
Code language: C# (cs)
To define a method that returns a sequence of messages based on the GetMessage()
method, you use the IAsyncEnumerable<T>
interface:
async IAsyncEnumerable<string> GetMessages(int max)
{
for (var i = 1; i <= max; i++)
{
var message = await GetMessage(i);
yield return message;
}
}
Code language: C# (cs)
The GetMessages()
method calls the GetMessage()
method to generate a sequence of messages. It returns an IAsyncEnumerable
<T> interface that iterates the sequences asynchronously.
The IAsyncEnumerable<T>
has the GetAsyncEnumerator()
method that returns an enumerator which iterates asynchronously through the collection:
IAsyncEnumerator GetAsyncEnumerator(
CancellationToken cancellationToken = default
);
Code language: C# (cs)
The GetAsyncEnumerator()
method has a parameter CancellationToken
that allows you to cancel the asynchronous iteration. It returns an IAsyncEnumerator<T>
interface that has the Current
and MoveNextAsync()
methods:
Member | IEnumerable<T> | IAsyncEnumerator<T> |
---|---|---|
Method | GetEnumerator | GetAsyncEnumerator |
Member | IEnumerator<T> | IAsyncEnumerator<T> |
---|---|---|
Property | Current | Current |
Method | MoveNext() | MoveNextAsync() |
To consume an IAsyncEnumerator
, you use the await foreach
statement instead of the foreach
statement:
await foreach (var message in GetMessages(5))
{
WriteLine(message);
}
Code language: C# (cs)
Output:
Message #1
Message #2
Message #3
Message #4
Message #5
Code language: C# (cs)
The program displays five messages after every second.
Put it all together.
using static System.Console;
await foreach (var message in GetMessages(5))
{
WriteLine(message);
}
async IAsyncEnumerable<string> GetMessages(int max)
{
for (var i = 1; i <= max; i++)
{
var message = await GetMessage(i);
yield return message;
}
}
async Task<string> GetMessage(int n)
{
await Task.Delay(1000);
return $"Message #{n}";
}
Code language: C# (cs)
Using the IAsyncEnumerable interface with CancellationToken
The following example shows how to use the CancellationToken
to request the cancellation of an operation:
using System.Runtime.CompilerServices;
using static System.Console;
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(3));
try
{
await foreach (var message in GetMessages(5, cts.Token))
{
WriteLine(message);
}
}
catch(TaskCanceledException ex)
{
WriteLine(ex.Message);
}
async IAsyncEnumerable<string> GetMessages(int max, [EnumeratorCancellation] CancellationToken token = default)
{
for (var i = 1; i <= max; i++)
{
var message = await GetMessage(i, token);
yield return message;
}
}
async Task<string> GetMessage(int n, CancellationToken token=default)
{
await Task.Delay(1000, token);
return $"Message #{n}";
}
Code language: C# (cs)
Output:
Message #1
Message #2
A task was canceled.
Code language: C# (cs)
How it works.
First, create a CancellationTokenSource
object and schedule a cancel operation after 3 seconds:
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(3));
Code language: C# (cs)
Second, add the CancellationToken
to both GetMessage()
and GetMessages()
methods. In the GetMessage()
method, pass the token to the Task.Delay
() method.
Practical use of the IAsyncEnumerable interface
The following program illustrates how to retrieve the content of a list of URLs
and writes the length of each downloaded text to the console using the IAsyncEnumerable
and HttpClient
:
using static System.Console;
var urls = new List<string>()
{
"https://datatracker.ietf.org/doc/html/rfc791",
"https://datatracker.ietf.org/doc/html/rfc1180",
"https://datatracker.ietf.org/doc/html/rfc2616"
};
await foreach (var result in GetTexts(urls))
{
WriteLine(result?.Length);
}
static async IAsyncEnumerable<string?> GetTexts(List<string> urls)
{
foreach (var url in urls)
{
var text = await Download(url);
yield return text;
}
}
static async Task<string?> Download(string url)
{
try
{
using var client = new HttpClient();
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
var text = await response.Content.ReadAsStringAsync();
return text;
}
catch (HttpRequestException ex)
{
WriteLine(ex.Message);
return null;
}
}
Code language: C# (cs)
Output:
126891
102930
580146
Code language: C# (cs)
Summary
- Use
IAsyncEnumerable<T>
andawait foreach
statement to iterate over a sequence of items asynchronously.