How to use IAsyncEnumerable in C#

By FoxLearn 1/6/2025 8:14:49 AM   135
IAsyncEnumerable is a feature introduced in C# 8.0 for working with asynchronous sequences of data.

IAsyncEnumerable uses a pull-based approach, meaning the next item is retrieved or created only when requested by the consumer. Unlike IEnumerable, which waits for the next element, IAsyncEnumerable returns an awaitable object that can be resumed later, enabling asynchronous iteration.

Benefits of IAsyncEnumerable

Key Benefits of IAsyncEnumerable:

  1. Asynchronous Streaming: Unlike traditional collections, IAsyncEnumerable streams items as they become available, which is ideal for large datasets or real-time data feeds (e.g., stock quotes, social media updates).

  2. Efficient Resource Use: It allows asynchronous processing of large data sets, optimizing resource usage and avoiding unnecessary waiting for all data to be loaded.

  3. Enhanced Performance: By not loading all data at once, IAsyncEnumerable reduces memory usage and improves system performance.

  4. Improved Responsiveness: It helps maintain application responsiveness when handling large data streams, freeing up resources while processing data.

  5. Simpler Code: IAsyncEnumerable reduces the need for complex synchronization (e.g., locks), minimizing the risk of deadlocks and simplifying code.

Filtering with IAsyncEnumerable

For example, how to filter an asynchronous sequence of data to retrieve only the strings that start with the letter "A":

public async IAsyncEnumerable<string> GetStringsStartingWithAAsync(IEnumerable<string> strings)
{
    foreach (var str in strings)
    {
        await Task.Delay(500); // Simulate async operation (e.g., fetching data)
        if (str.StartsWith("A", StringComparison.OrdinalIgnoreCase))
        {
            yield return str;
        }
    }
}

In this example, the method filters the input sequence of strings, yielding only those that start with the letter "A" after asynchronously processing each element.

Aggregating with IAsyncEnumerable

Here’s an example that demonstrates how to use IAsyncEnumerable to calculate the total length of a sequence of strings asynchronously:

public async Task<int> GetTotalLengthAsync(IEnumerable<string> strings)
{
    int totalLength = 0;
    await foreach (var str in GetStringsAsync(strings))
    {
        totalLength += str.Length;
    }
    return totalLength;
}

In this example, the method iterates through an asynchronous sequence of strings, calculating the total length of all the strings by adding their lengths asynchronously as they are processed.

Asynchronous projection with IAsyncEnumerable

For example, how you can use projections with IAsyncEnumerable to asynchronously transform a sequence of strings to their uppercase equivalents:

public async IAsyncEnumerable<string> GetUppercaseStringsAsync(IEnumerable<string> strings)
{
    foreach (var str in strings)
    {
        await Task.Delay(500); // Simulate async operation
        yield return await Task.Run(() => str.ToUpper());
    }
}

In this example, the method takes an asynchronous sequence of strings and projects each string to its uppercase form, returning the transformed values asynchronously.

Transforming a sequence with IAsyncEnumerable

For example, how to use IAsyncEnumerable to transform a sequence of dates into their formatted string representations:

public async IAsyncEnumerable<string> FormatDatesAsync(IEnumerable<DateTime> dates)
{
    foreach (var date in dates)
    {
        await Task.Delay(500); // Simulate async operation
        yield return await Task.Run(() => date.ToString("yyyy-MM-dd"));
    }
}

In this example, the method takes an asynchronous sequence of DateTime objects and transforms each date into a string formatted as "yyyy-MM-dd", yielding the transformed values asynchronously.

Batch processing with IAsyncEnumerable

For example, how to process items in batches asynchronously using IAsyncEnumerable to handle large sets of strings and process them in chunks:

public static async IAsyncEnumerable<IEnumerable<string>> ProcessStringBatchAsync(IAsyncEnumerable<string> source, int batchSize, 
    CancellationToken cancellationToken = default)
{
    var batch = new List<string>();
    await foreach (var str in source)
    {
        if (cancellationToken.IsCancellationRequested)
            yield break;

        batch.Add(str);
        if (batch.Count >= batchSize)
        {
            yield return batch;
            batch = new List<string>(); // Clear the batch for the next set
        }
    }

    // Yield the remaining items if they exist
    if (batch.Count > 0)
    {
        yield return batch;
    }
}

In this example, the method processes an asynchronous sequence of strings and groups them into batches of the specified size, yielding each batch asynchronously. If the cancellation token is triggered, it stops processing and returns early. Any remaining items that don't fill an entire batch are yielded at the end.

Buffering with IAsyncEnumerable

For example, how to use buffering with IAsyncEnumerable to process a sequence of numbers more efficiently by buffering a fixed number of items before processing them asynchronously:

public static async IAsyncEnumerable<int> ProcessBufferedNumbersAsync(this IAsyncEnumerable<int> source, int bufferSize,
    CancellationToken cancellationToken = default)
{
    var buffer = new List<int>();
    await foreach (var number in source)
    {
        if (cancellationToken.IsCancellationRequested)
            yield break;

        buffer.Add(number);
        if (buffer.Count >= bufferSize)
        {
            foreach (var bufferedItem in buffer)
            {
                yield return bufferedItem; // Process each item in the buffer
            }
            buffer.Clear(); // Clear the buffer for the next set
        }
    }

    // Process any remaining items in the buffer
    foreach (var remainingItem in buffer)
    {
        yield return remainingItem;
    }
}

In this example, the method buffers a sequence of numbers until the buffer reaches the specified size. Once the buffer is full, it processes (yields) the items and clears the buffer for the next batch. If there are any remaining items in the buffer after all items are processed, they are yielded as well.

IAsyncEnumerable offers powerful features for working with asynchronous sequences, including filtering, transforming, and aggregating large datasets. It can be combined with LINQ operators for efficient data manipulation. Additionally, you can handle runtime errors, implement retries, and use logging to ensure errors are managed gracefully during asynchronous operations.