What’s so special about IAsyncEnumerable in .NET Core 3.0?

Translation of the article was prepared on the eve of the start of the course “C # Developer”


One of the most important features of .NET Core 3.0 and C # 8.0 is the new IAsyncEnumerable (aka asynchronous thread). But what is so special about him? What can we do now that was impossible before?

In this article, we will look at what tasks IAsyncEnumerable<T> intended to decide how to implement it in our own applications and why IAsyncEnumerable<T> will replace TaskT>> in many situations.

Check out all new features of .NET Core 3

Life before IAsyncEnumerable<T>

Perhaps the best way to explain why IAsyncEnumerable<T> so useful is to consider the problems that we faced before.

Imagine that we are creating a library for interacting with data, and we need a method that requests some data from a store or API. Usually this method returns Task<IEnumerable<T >>, like here:

public async Task> GetAllProducts()

To implement this method, we usually request data asynchronously and return it when it completes. The problem with this becomes more obvious when we need to make multiple asynchronous calls to get data. For example, our database or API can return data in entire pages, like this implementation using Azure Cosmos DB:

public async Task> GetAllProducts()
{
    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);
    var iterator = container.GetItemQueryIterator("SELECT * FROM c");
    var products = new List();
    while (iterator.HasMoreResults)
    {
        foreach (var product in await iterator.ReadNextAsync())
        {
            products.Add(product);
        }
    }
    return products;
}

Notice that we loop through all the results in a while loop, instantiate product objects, put them in a List, and finally return the whole thing. This is quite inefficient, especially on large datasets.

Perhaps we can create a more efficient implementation by modifying our method so that it returns results an entire page at a time:

public IEnumerable>> GetAllProducts()
{
    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);
    var iterator = container.GetItemQueryIterator("SELECT * FROM c");
    while (iterator.HasMoreResults)
    {
        yield return iterator.ReadNextAsync().ContinueWith(t => 
        {
            return (IEnumerable)t.Result;
        });
    }
}

The caller will use the method like this:

foreach (var productsTask in productsRepository.GetAllProducts())
{
    foreach (var product in await productsTask)
    {
        Console.WriteLine(product.Name);
    }
}

This implementation is more efficient, but the method now returns IEnumerable<Task<IEnumerable<Product >>>... As we can see from the calling code, calling the method and processing the data is not intuitive. More importantly, paging is a data access method implementation detail that the caller does not need to know about.

IAsyncEnumerable<T> hurrying to help

What we really want to do is fetch data from our database asynchronously and pass the results back to the caller as they are received.

In synchronous code, a method that returns IEnumerable can use a yield return statement to return each piece of data to the caller as it comes from the database.

public IEnumerable GetAllProducts()
{
    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);
    var iterator = container.GetItemQueryIterator("SELECT * FROM c");
    while (iterator.HasMoreResults)
    {
        foreach (var product in iterator.ReadNextAsync().Result)
        {
            yield return product;
        }
    }
}

However, NEVER DO THIS! The above code turns an asynchronous database call into a blocking call and does not scale.

If only we could use yield return with asynchronous methods! It was impossible ... until now.

IAsyncEnumerable<T> was introduced in .NET Core 3 (.NET Standard 2.1). It provides an enumerator which has a method MoveNextAsync()which might be expected. This means that the initiator can make asynchronous calls while (in the middle of) receiving the results.

Instead of returning Task<IEnumerable<T >> our method can now return IAsyncEnumerable<T> and use yield return to pass data.

public async IAsyncEnumerable GetAllProducts()
{
    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);
    var iterator = container.GetItemQueryIterator("SELECT * FROM c");
    while (iterator.HasMoreResults)
    {
        foreach (var product in await iterator.ReadNextAsync())
        {
            yield return product;
        }
    }
}

To use the results, we need to use the new syntax await foreach()available in C # 8:

await foreach (var product in productsRepository.GetAllProducts())
{
    Console.WriteLine(product);
}

This is much nicer. The method produces data as it comes in. The calling code uses the data at its own pace.

IAsyncEnumerable<T> and ASP.NET Core

Beginning with .NET Core 3 Preview 7ASP.NET can return an IAsyncEnumerable from an API controller action. This means that we can return the results of our method directly - effectively passing data from the database into the HTTP response.

[HttpGet]
public IAsyncEnumerable Get()
    => productsRepository.GetAllProducts();

Replacement Task<IEnumerable<T >> on the IAsyncEnumerable<T>

As time goes by as you become familiar with .NET Core 3 and .NET Standard 2.1, it is expected that IAsyncEnumerable<T> will be used in places where we usually used Task<IEnumerable>.

I look forward to seeing support IAsyncEnumerable<T> in libraries. In this article, we saw similar code to query data using the Azure Cosmos DB 3.0 SDK:

var iterator = container.GetItemQueryIterator("SELECT * FROM c");
while (iterator.HasMoreResults)
{
    foreach (var product in await iterator.ReadNextAsync())
    {
        Console.WriteLine(product.Name);
    }
}

As in our previous examples, the native Cosmos DB SDK also loads us with paging implementation details, making it difficult to process query results.

To see what it might look like if GetItemQueryIterator<Product> () instead returned IAsyncEnumerable<T>, we can create an extension method in FeedIterator:

public static class FeedIteratorExtensions
{
    public static async IAsyncEnumerable ToAsyncEnumerable(this FeedIterator iterator)
    {
        while (iterator.HasMoreResults)
        {
            foreach(var item in await iterator.ReadNextAsync())
            {
                yield return item;
            }
        }
    }
}

We can now handle the results of our queries in a much nicer way:

var products = container
    .GetItemQueryIterator("SELECT * FROM c")
    .ToAsyncEnumerable();
await foreach (var product in products)
{
    Console.WriteLine(product.Name);
}

Summary

IAsyncEnumerable<T> - is a welcome addition to .NET and in many cases will make your code more pleasant and efficient. You can learn more about this at these resources:


State design pattern


Read more:

  • Best Practices for Improving Performance in C #
  • Entity Framework Core

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *