'AsyncEnumerableReader' достиг настроенного максимального размера буфера при перечислении значения

Я использую метод async/await, возвращающий IAsyncEnumerable<> для извлечения строк из базы данных SQL Server и предоставления их через интерфейс Web API .Net Core 3.0. Он отлично работает, пока я не превысил 8192 строки. Он не работает для строк за пределами этой точки. Я пробовал 3 разные таблицы, каждая с очень разными размерами строк. Каждый раз сбой происходит после получения 8192 строк. Вот ошибка, которую я вижу:

An unhandled exception occurred while processing the request.
InvalidOperationException: 'AsyncEnumerableReader' reached the configured maximum size of the buffer when enumerating a value of type 'Microsoft.EntityFrameworkCore.TestDatabase.TestDatabaseDbSet`1[TestNamespace.TestClass]'. This limit is in place to prevent infinite streams of 'IAsyncEnumerable<>' from continuing indefinitely. If this is not a programming mistake, consider ways to reduce the collection size, or consider manually converting 'Microsoft.EntityFrameworkCore.TestDatabase.TestDatabaseDbSet`1[TestNamespace.TestClass]' into a list rather than increasing the limit.
Microsoft.AspNetCore.Mvc.Infrastructure.AsyncEnumerableReader.ReadTestDatabase<T>(IAsyncEnumerable<object> value)

Stack Query Cookies Headers Routing
InvalidOperationException: 'AsyncEnumerableReader' reached the configured maximum size of the buffer when enumerating a value of type 'Microsoft.EntityFrameworkCore.TestDatabase.TestDatabaseDbSet`1[TestNamespace.TestClass]'. This limit is in place to prevent infinite streams of 'IAsyncEnumerable<>' from continuing indefinitely. If this is not a programming mistake, consider ways to reduce the collection size, or consider manually converting 'Microsoft.EntityFrameworkCore.TestDatabase.TestDatabaseDbSet`1[TestNamespace.TestClass]' into a list rather than increasing the limit.
Microsoft.AspNetCore.Mvc.Infrastructure.AsyncEnumerableReader.ReadTestDatabase<T>(IAsyncEnumerable<object> value)
Microsoft.AspNetCore.Mvc.Infrastructure.AsyncEnumerableReader.ReadTestDatabase<T>(IAsyncEnumerable<object> value)
Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, IAsyncEnumerable<object> asyncEnumerable)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeResultFilters>g__Awaited|27_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Show raw exception details
System.InvalidOperationException: 'AsyncEnumerableReader' reached the configured maximum size of the buffer when enumerating a value of type 'Microsoft.EntityFrameworkCore.TestDatabase.TestDatabaseDbSet`1[TestNamespace.TestClass]'. This limit is in place to prevent infinite streams of 'IAsyncEnumerable<>' from continuing indefinitely. If this is not a programming mistake, consider ways to reduce the collection size, or consider manually converting 'Microsoft.EntityFrameworkCore.TestDatabase.TestDatabaseDbSet`1[TestNamespace.TestClass]' into a list rather than increasing the limit.
   at Microsoft.AspNetCore.Mvc.Infrastructure.AsyncEnumerableReader.ReadTestDatabase[T](IAsyncEnumerable`1 value)
   at Microsoft.AspNetCore.Mvc.Infrastructure.AsyncEnumerableReader.ReadTestDatabase[T](IAsyncEnumerable`1 value)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, IAsyncEnumerable`1 asyncEnumerable)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeResultFilters>g__Awaited|27_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
reached the configured maximum size of the buffer when enumerating a value of type Microsoft.EntityFrameworkCore DbSet  This limit is in place to prevent infinite streams from continuing indefinitely. If this is not a programming mistake, consider ways to reduce the collection size, or consider manually converting Microsoft.EntityFrameworkCore DbSet  into a list rather than increasing the limit.

Microsoft.EntityFrameworkCore.DbSet AsyncEnumerable

5 ответов

Решение

Что происходит

Предел исходит от свойства MvcOptions.MaxIAsyncEnumerableBufferLimit:

Получает или задает максимальное количество записей IAsyncEnumerable, которое ObjectResultExecutor будет буферизовать.

Когда Value является экземпляром IAsyncEnumerable, ObjectResultExecutor будет охотно читать перечисление и добавлять его в синхронную коллекцию перед вызовом выбранного средства форматирования. Это свойство определяет максимальное количество записей, которое исполнитель может буферизовать.

(...)

По умолчанию 8192.

Исправить

На мой взгляд, от лучшего к худшему.

A. (Предпочтительно) Не ​​возвращайте так много записей

Я бы рекомендовал не возвращать так много записей из веб-API.

Пейджинг - это один из способов работы с большим количеством записей. Вы можете проверить Учебник: добавление сортировки, фильтрации и разбиения на страницы - ASP.NET MVC с EF Core.

Б. Отрегулируйте предел

MvcOptions.MaxIAsyncEnumerableBufferLimit можно установить другое значение.

C. Не используйте IAsyncEnumerable

Вы можете вернуться IEnumerable - он сможет обрабатывать большее количество записей.

В ASP.NET Core 5 действительно экземпляры типа IAsyncEnumerableбыли обработаны путем одновременной буферизации последовательности в памяти и форматирования буферизованной коллекции. Это объясняет, почему вы видите это исключение.

Однако с ASP.NET Core 6.0 этого больше не будет!

В ASP.NET Core 6 при форматировании с использованием System.Text.Json MVC больше не буферизует экземпляры IAsyncEnumerable. Вместо этого MVC полагается на поддержку, которую System.Text.Json добавил для этих типов ([ссылка][1])

См. Раздел Разъяснение того, как IAsyncEnumerable работает с веб-API ASP.NET, для получения дополнительных сведений о том, как использовать IAsyncEnumerable.

В моем случае я столкнулся с этой проблемой в контексте миграции конечных точек odata с .net core 2.2 на .net core 3.1. В качестве решения я наткнулся на статью с предложенным решением . Короче говоря, мы должны создать собственный метод расширения ToQueryable(), который не реализует интерфейс IAsyncEnumerable, и использовать его вместо существующего AsQueryable().

Он работает как шарм без дополнительной фильтрации по строке запроса. Но когда мы попытались отправить $top=3 в запросе фильтрации, мы получили ошибку: InvalidCastException: невозможно преобразовать объект типа Queryable печатать System.Linq.IOrderedQueryable'. Решением было обновить код, приведенный в упомянутой выше статье, путем замены IQueryable на IOrderedQueryable. В результате у меня получилось:

          public static partial class CustomOrderedQueryable
    {
        public static IEnumerable<T> ToEnumerable<T>(this IEnumerable<T> source)
        {
            foreach (var item in source)
                yield return item;
        }
    }

    public static partial class CustomOrderedQueryable
    {
        public static IOrderedQueryable<T> ToQueryable<T>(this IQueryable<T> source)
            => new OrderedQueryable<T>(new QueryProvider(source.Provider), source.Expression);

        class OrderedQueryable<T> : IOrderedQueryable<T>
        {
            internal OrderedQueryable(IQueryProvider provider, Expression expression)
            {
                Provider = provider;
                Expression = expression;
            }
            public Type ElementType => typeof(T);
            public Expression Expression { get; }
            public IQueryProvider Provider { get; }
            public IEnumerator<T> GetEnumerator() => Provider.Execute<IEnumerable<T>>(Expression)
                .ToEnumerable().GetEnumerator();
            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
        }

        class QueryProvider : IQueryProvider
        {
            private readonly IQueryProvider source;
            internal QueryProvider(IQueryProvider source) => this.source = source;
            public IQueryable CreateQuery(Expression expression)
            {
                var query = source.CreateQuery(expression);
                return (IQueryable)Activator.CreateInstance(
                    typeof(OrderedQueryable<>).MakeGenericType(query.ElementType),
                    this, query.Expression);
            }
            public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
                => new OrderedQueryable<TElement>(this, expression);
            public object Execute(Expression expression) => source.Execute(expression);
            public TResult Execute<TResult>(Expression expression) => source.Execute<TResult>(expression);
        }
    }

Сообщение об исключении и описание прекрасно описывают ситуацию. Вы читаете (исключительно большой) объем данных в буфер, и фреймворк предупреждает вас, что вы, вероятно, делаете то, чего не ожидаете, или запросили слишком много информации.

Поскольку вы используете WEBAPI, вас предупреждают, потому что цель WEBAPI и веб-сайтов в целом - иметь низкое время отклика, а необходимость сбора данных по 8K+ записям обычно этого не позволяет. При этом мы не знаем фактического варианта использования вашего приложения, и вам может быть все равно, но все же фреймворк пытается помочь вам поступать "правильно".

Независимо от вашего уровня опыта, отказ от чтения / интерпретации сообщений об ошибках - это легкая ловушка. Вместо этого подумайте, что они означают, и, если вы их не понимаете, перечитайте и исследуйте, пока не поймете. Мы прошли долгий путь со времен медитаций гуру, поэтому не принимайте эту полезную информацию как должное.

TL;DR Вы читаете слишком много данных за раз. Найдите способ разбить данные на более мелкие части, чтобы вернуть их клиенту. Меньший размер страницы - отличное место для начала.

Действительно, в ASP.NET Core 5 экземпляры типа обрабатывались путем одновременной буферизации последовательности в памяти и форматирования буферизованной коллекции. Это объясняет, почему вы видите это исключение.

Однако с ASP.NET Core 6.0 этого больше не будет!

В ASP.NET Core 6 при форматировании с использованием System.Text.Json MVC больше не буферизует экземпляры IAsyncEnumerable. Вместо этого MVC полагается на поддержку, которую System.Text.Json добавил для этих типов (ссылка)

Релиз для ASP.NET Core 6 запланирован на ноябрь 2021 года (для справки). Уже сейчас можно протестировать новое поведение с помощью предварительных версий. Я успешно протестировал следующий код, используя предварительную версию 6.0.100-preview.6.21355.2. Код создает бесконечный поток целых чисел и возвращает его через контроллер, используя . В цикл «доказывает», что данные возвращаются до того, как все будет обработано, потому что, очевидно, цикл никогда не завершится *.

      using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

namespace dot_net_api_streaming.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class LoopController : ControllerBase
    {

        [HttpGet]
        public IAsyncEnumerable<int> Get()
        {
            return GetInfiniteInts();
        }

        private async IAsyncEnumerable<int> GetInfiniteInts()
        {
            int index = 0;
            while (true)
                yield return index++;
        }
    }
}
 

* Пожалуйста, имейте это в виду, экспериментируя с моим кодом, чтобы ваша машина не вылетела из строя :)

Другие вопросы по тегам