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

The translation of the article was prepared in anticipation 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<T>(aka asynchronous thread) feature . 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 it is IAsyncEnumerable<T>intended to solve, how to implement it in our own applications, and why it IAsyncEnumerable<T>will replace it in many situations. Check out all the new features in .NET Core 3Task<IEnumerable<T>>







Life before IAsyncEnumerable<T>



Perhaps the best way to explain why IAsyncEnumerable<T>it is so useful is to look at the problems we encountered 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 like this:Task<IEnumerable<T>>



public async Task<IEnumerable<Product>> 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<IEnumerable<Product>> GetAllProducts()
{
    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);
    var iterator = container.GetItemQueryIterator<Product>("SELECT * FROM c");
    var products = new List<Product>();
    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<Task<IEnumerable<Product>>> GetAllProducts()
{
    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);
    var iterator = container.GetItemQueryIterator<Product>("SELECT * FROM c");
    while (iterator.HasMoreResults)
    {
        yield return iterator.ReadNextAsync().ContinueWith(t => 
        {
            return (IEnumerable<Product>)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 . As we can see from the calling code, calling the method and processing the data is not intuitive. More importantly, paging is an implementation detail of a data access method that the caller does not need to know about.IEnumerable<Task<IEnumerable<Product>>>



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<Product> GetAllProducts()
{
    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);
    var iterator = container.GetItemQueryIterator<Product>("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 returnwith async methods! It was impossible ... until now.



IAsyncEnumerable<T>was introduced in .NET Core 3 (.NET Standard 2.1). It provides an enumerator that has a method MoveNextAsync()that 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<Product> GetAllProducts()
{
    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);
    var iterator = container.GetItemQueryIterator<Product>("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



Starting with .NET Core 3 Preview 7 , ASP.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<Product> Get()
    => productsRepository.GetAllProducts();


Replacement forTask<IEnumerable<T>>IAsyncEnumerable<T>



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



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



var iterator = container.GetItemQueryIterator<Product>("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 it GetItemQueryIterator<Product>()returned instead IAsyncEnumerable<T>, we can create an extension method in FeedIterator:



public static class FeedIteratorExtensions
{
    public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(this FeedIterator<T> 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<Product>("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 find out more about this on these resources:








State design pattern






Read more:






All Articles