12

I am using an async/await method returning IAsyncEnumerable<> to retrieve rows from a SQL Server database and provide them via a Web API .Net Core 3.0 interface. It works fine until I exceed 8192 rows. It fails for rows beyond that point. I have tried 3 different tables, each with very different row sizes. Each time the failure occurs after yielding 8192 rows. Here is the error I see:

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
tymtam
  • 31,798
  • 8
  • 86
  • 126
David Stevens
  • 173
  • 1
  • 1
  • 8

4 Answers4

22

What's happening

The limit comes from MvcOptions.MaxIAsyncEnumerableBufferLimit Property :

Gets or sets the most number of entries of an IAsyncEnumerable that that ObjectResultExecutor will buffer.

When Value is an instance of IAsyncEnumerable, ObjectResultExecutor will eagerly read the enumeration and add to a synchronous collection prior to invoking the selected formatter. This property determines the most number of entries that the executor is allowed to buffer.

(...)

Defaults to 8192.

Fix

From best to worst, in my opinion.

A. (Preferred) Don't return so many records

I'd recommend not to return so many records from a web api.

Paging is one way to deal with large number of records. You could check out Tutorial: Add sorting, filtering, and paging - ASP.NET MVC with EF Core.

B. Adjust the limit

MvcOptions.MaxIAsyncEnumerableBufferLimit can be set to another value.

C. Don't use IAsyncEnumerable

You could return IEnumerable - it will be able to handle the higher number of records.

tymtam
  • 31,798
  • 8
  • 86
  • 126
  • Thank you. This is just the information I was hoping for. If I do set this to a non-default value, do I set it in Server.DataAccess.Startup.ConfigureServices with the passed in IServiceCollection object? And if so, how is it set? – David Stevens Nov 22 '19 at 04:19
  • 8
    In .net core 3.0 I think it would be `services.AddControllers(options => options.MaxIAsyncEnumerableBufferLimit = N);` – tymtam Nov 22 '19 at 04:30
  • That's it. Thanks again. – David Stevens Nov 22 '19 at 12:48
  • what are the potential problems of setting MaxIAsyncEnumerableBufferLimit? – user441365 May 09 '22 at 10:47
  • 2
    In this choose your own adventure people picking C or B will eventually got to A, spoilers I know. – jjhayter Nov 07 '22 at 01:21
2

In ASP.NET Core 5 indeed instances of the type IAsyncEnumerable have been processed by buffering the sequence in memory and formatting the buffered collection all at once. This explains, why you see that exception.

However, with ASP.NET Core 6.0 this will not happen anymore!

In ASP.NET Core 6, when formatting using System.Text.Json, MVC no longer buffers IAsyncEnumerable instances. Instead, MVC relies on the support that System.Text.Json added for these types ([reference][1])

See Clarification on how IAsyncEnumerable works with ASP.NET Web API for more details on how to use IAsyncEnumerable.

Fable
  • 371
  • 1
  • 4
  • 11
0

The exception message and description describe the situation perfectly. You are reading a(n exceptionally large) amount of data into a buffer, and the framework is warning you that you are probably doing something that you don't expect, or have queried for too much information.

Since you are using WEBAPI, you are being warned because the goal of WEBAPI, and websites in general is to have a low response time, and having to collect data on 8K+ records doesn't usually achieve that. That being said, we don't know the actual use case of your application, and you may not care, but all the same, the framework is trying to help you do the "right thing".

Regardless of your experience level, not reading/interpreting the error messages is an easy trap to fall into. Instead, think about what they mean, and if you don't understand them, reread and research until you do. We have come a long way since guru meditations, so don't take this useful information for granted.

TL;DR You are reading too much data at a time. Find a way to chunk the data into smaller pieces to return to the client. A smaller page size is a great place to start.

A.R.
  • 15,405
  • 19
  • 77
  • 123
  • 2
    Good advice. Here is my use case: I want to use a simple, single method for serving up data from persistent data storage to a wide variety of clients, e.g. Matlab, R, SAS, Python, Excel, PowerBI, Tableau, etc. WEBAPI seemed like a good choice because most clients support it. To preserve a flexible implementation of the persistent data (currently SQL Server), I do not want to give direct access, but rather abstract that out to a layer that is simple and broadly useful. If you have suggestions for a better strategy, I would love to know them. Thanks. – David Stevens Nov 22 '19 at 13:06
0

In my case, I faced this issue in the context of migration odata-endpoints from .net core 2.2 to .net core 3.1. As a solution, I came across the article with provided solution. To make a long story short, we should create our own extension method ToQueryable() which doesn't implement the IAsyncEnumerable interface and use it instead of the existing AsQueryable().

It works like a charm without additional filtering through the query string. But when we tried to send $top=3 in the filtering query we got the error: 'InvalidCastException: Unable to cast object of type Queryable to type System.Linq.IOrderedQueryable'. The solution was to update the code provided in the article referenced above with the substitution of IQueryable with IOrderedQueryable. As the result I've got:

    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);
        }
    }
Sergii Fasolko
  • 188
  • 1
  • 7
  • Please don't use aggregation sites that don't actually take you to the SO article. For those wondering here is the direct link: https://stackoverflow.com/questions/60003861/asyncenumerablereader-reached-the-configured-maximum-size-but-not-using-async – jjhayter Nov 09 '22 at 00:24