Что же такого особенного в IAsyncEnumerable в .NET Core 3.0?

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Перевод статьи подготовлен в преддверии старта курса «Разработчик C#».





Одной из наиболее важных функций .NET Core 3.0 и C# 8.0 стал новый IAsyncEnumerable<T> (он же асинхронный поток). Но что в нем такого особенного? Что же мы можем сделать теперь, что было невозможно раньше?

В этой статье мы рассмотрим, какие задачи IAsyncEnumerable<T> предназначен решать, как реализовать его в наших собственных приложениях и почему IAsyncEnumerable<T> заменит Task<IEnumerable<T>> во многих ситуациях.

Ознакомьтесь со всеми новыми функциями .NET Core 3

Жизнь до IAsyncEnumerable<T>


Возможно, лучший способ объяснить, почему IAsyncEnumerable<T> так полезен — это рассмотреть проблемы, с которыми мы сталкивались до него.

Представьте, что мы создаем библиотеку для взаимодействия с данным, и нам нужен метод, который запрашивает некоторые данные из хранилища или API. Обычно этот метод возвращает Task<IEnumerable<T>>, как здесь:

public async Task<IEnumerable<Product>> GetAllProducts()

Чтобы реализовать этот метод, мы обычно запрашиваем данные асинхронно и возвращаем их, когда он завершается. Проблема с этим становится более очевидной, когда для получения данных нам нужно сделать несколько асинхронных вызовов. Например, наша база данных или API могут возвращать данные целыми страницами, как, например, эта реализация, использующая 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;
}

Обратите внимание, что мы пролистываем все результаты в цикле while, создаем экземпляры объектов product, помещаем их в List, и, наконец, возвращаем все целиком. Это довольно неэффективно, особенно на больших наборах данных.

Возможно, мы сможем создать более эффективную реализацию, изменив наш метод так, чтобы он возвращал результаты по целой странице за раз:

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;
        });
    }
}

Вызывающий объект будет использовать метод следующим образом:

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

Эта реализация более эффективна, но метод теперь возвращает IEnumerable<Task<IEnumerable<Product>>>. Как мы видим из вызывающего кода, вызов метода и обработка данных не интуитивны. Что еще более важно, подкачка страниц — это деталь реализации метода доступа к данным, о которой вызывающая сторона не должна ничего знать.

IAsyncEnumerable<T> спешит на помощь


Что бы мы действительно хотели сделать, так это асинхронно извлекать данные из нашей базы данных и передавать результаты обратно вызывающей стороне по мере их получения.

В синхронном коде метод, который возвращает IEnumerable, может использовать оператор yield return для возврата каждой части данных вызывающей стороне, когда она приходит из базы данных.

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;
        }
    }
}

Однако, НИКОГДА ТАК НЕ ДЕЛАЙТЕ! Приведенный выше код превращает асинхронный вызов базы данных в блокирующий и не масштабируется.

Если только мы могли бы использовать yield return с асинхронными методами! Это было невозможно… до сих пор.

IAsyncEnumerable<T> был представлен в .NET Core 3 (.NET Standard 2.1). Он предоставляет энумератор, у которого есть метод MoveNextAsync(), который может быть ожидаемым. Это означает, что инициатор может совершать асинхронные вызовы во время (посреди) получения результатов.

Вместо возврата Task<IEnumerable<T>> наш метод теперь может возвращать IAsyncEnumerable<T> и использовать yield return для передачи данных.

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;
        }
    }
}

Чтобы использовать результаты, нам нужно использовать новый синтаксис await foreach(), доступный в C# 8:

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

Это намного приятнее. Метод производит данные по мере их поступления. Код вызова использует данные в своем темпе.

IAsyncEnumerable<T> и ASP.NET Core


Начиная с .NET Core 3 Preview 7, ASP.NET может возвращать IAsyncEnumerable из экшена контроллера API. Это означает, что мы можем возвращать результаты нашего метода напрямую — эффективно передавая данные из базы данных в HTTP ответ.

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

Замена Task<IEnumerable<T>> на IAsyncEnumerable<T>


С течением времени по ходу освоения .NET Core 3 и .NET Standard 2.1, ожидается, что IAsyncEnumerable<T> будет использоваться в местах, где мы обычно использовали Task<IEnumerable>.

Я с нетерпением жду возможности увидеть поддержку IAsyncEnumerable<T> в библиотеках. В этой статье мы видели подобный код для запроса данных с помощью SDK Azure Cosmos DB 3.0:

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

Как и в наших предыдущих примерах, собственный SDK Cosmos DB также нагружает нас деталями реализации подкачки страниц, что затрудняет обработку результатов запроса.

Чтобы посмотреть, как это могло бы выглядеть, если бы GetItemQueryIterator<Product>() вместо этого возвращал IAsyncEnumerable<T>, мы можем создать метод-расширение в 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;
            }
        }
    }
}

Теперь мы можем обрабатывать результаты наших запросов намного более приятным способом:

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

Резюме


IAsyncEnumerable<T> — является долгожданным дополнением к .NET и во многих случаях сделает код более приятным и эффективным. Узнать об этом больше вы можете на этих ресурсах:

  • Tutorial: Generate and consume async streams using C# 8.0 and .NET Core 3.0
  • C# language proposals — Async Streams



Шаблон проектирования «Состояние (state)»



Читать ещё:


  • Лучшие практики повышения производительности в C#
  • Entity Framework Core
Источник: https://habr.com/ru/company/otus/blog/514594/


Интересные статьи

Интересные статьи

Недавно я начал оттачивать владение языком программирования Python. Я хотел изучить продвинутые паттерны, идиомы и методы программирования. Начал я с чтения книг по продвинутому Python,...
Добрый день. В нашей компании .NET используется с самого его рождения. У нас в продуктиве работают решения, написанные на всех версиях фреймворка: от самой первой и до последней на...
Привет, Хабровчани! Сегодня вы ознакомитесь со статьей, в которой будет рассказано, как создать бота, используя C# на .NET Core, и о том, как его завести на удаленном сервере. Статья буд...
Эта статья для тех, кто собирается открыть интернет-магазин, но еще рассматривает варианты и думает по какому пути пойти, заказать разработку магазина в студии, у фрилансера или выбрать облачный серви...
Хотите повстречаться с Джоном Гэллоуэем (исполнительным директором .NET Foundation), Павлом Йосифовичем (автором легендарной «Windows Internals» и новых курсов на Pluralsight)? Или может быть, с ...