AsyncEnumerableReader достиг настроенного максимального размера, но не использует AsyncEnumerable - PullRequest
4 голосов
/ 31 января 2020

Я использую EF Core 3.1.1 (сделайте tnet core 3.1.1). И я хочу вернуть большое количество автомобилей лиц. К сожалению, я получаю следующее сообщение об ошибке:

'AsyncEnumerableReader' reached the configured maximum size of the buffer when enumerating a value of type 'Microsoft.EntityFrameworkCore.Internal.InternalDbSet`...

Я знаю, что есть еще один ответ на вопрос об этой же ошибке. Но я не делаю явной операции asyn c.

[HttpGet]
[ProducesResponseType(200, Type = typeof(Car[]))]
public IActionResult Index()
{
   return Ok(_carsDataModelContext.Cars.AsEnumerable());
}

_carDataModelContext.Car - это просто простая сущность, которая отображает 1-на-1 в таблицу в базе данных. public virtual DbSet<Car> Cars { get; set; }

Первоначально я возвращаю Ok(_carsDataModelContext.Cars.AsQueryable()), потому что нам нужно поддерживать OData. Но чтобы быть уверенным, что дело не в OData, я попытался вернуть AsEnumerable и удалить атрибут «[EnableQuery]» из метода. Но это все равно заканчивается той же ошибкой.

Единственный способ исправить это, если я верну Ok(_carsDataModelContext.Cars.ToList())

1 Ответ

4 голосов
/ 11 февраля 2020

Все реализации Ef Core IQueryable<T> (DbSet<T>, EntityQueryable<T>) также реализуют стандартный интерфейс IAsyncEnumerable<T> (при использовании из. NET Core 3), поэтому AsEnumerable(), AsQueryable() и AsAsyncEnumerable() просто верните то же самое приведение экземпляра в соответствующий интерфейс.

Вы можете легко проверить это с помощью следующего фрагмента:

var queryable = _carsDataModelContext.Cars.AsQueryable();
var enumerable = queryable.AsEnumerable();
var asyncEnumerable = queryable.AsAsyncEnumerable();
Debug.Assert(queryable == enumerable && queryable == asyncEnumerable);

Так что, даже если вы не возвращаете явно IAsyncEnumerable<T>, базовый объект реализует его и может быть запрошен. Зная, что Asp. Net Ядро является естественным асинхронным c фреймворком, мы можем смело предположить, что он проверяет, реализует ли объект новый стандарт IAsyncEnumerable<T>, и использует его за кадром вместо IEnumerable<T>.

Конечно, когда вы используете ToList(), возвращенный класс List<T> не реализует IAsyncEnumerable<T>, поэтому единственный вариант - использовать IEnumerable<T>.

Это должно объяснить поведение 3.1. Обратите внимание, что до 3.0 не было стандартного интерфейса IAsyncEnumerable<T>. EF Core внедрял и возвращал свой собственный асин c интерфейс, но инфраструктура. Net Core не знала об этом, поэтому не могла использовать его от вашего имени.


Единственный способ заставить предыдущее поведение без использования ToList() / ToArray() и т. д. означает скрыть базовый источник (отсюда IAsyncEnumerable<T>).

Для IEnumerable<T> это довольно просто. Все, что вам нужно, это создать собственный метод расширения, который использует итератор C#, например:

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

, а затем использовать

return Ok(_carsDataModelContext.Cars.ToEnumerable());

Если вы хотите вернуть IQueryable<T>, все становится сложнее. Создание пользовательской оболочки IQueryable<T> недостаточно, необходимо создать собственную оболочку IQueryProvider, чтобы убедиться, что компоновка поверх возвращенной оболочки IQueryable<T> продолжит возвращать оболочки до тех пор, пока не будет запрошена финальная IEnumerator<T> (или IEnumerator), а возвращенный базовый асинхронный c перечисляемый скрыт с помощью вышеупомянутого метода.

Вот упрощенная реализация вышеприведенного:

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

    class Queryable<T> : IQueryable<T>
    {
        internal Queryable(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(Queryable<>).MakeGenericType(query.ElementType),
                this, query.Expression);
        }
        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
            => new Queryable<TElement>(this, expression);
        public object Execute(Expression expression) => source.Execute(expression);
        public TResult Execute<TResult>(Expression expression) => source.Execute<TResult>(expression);
    }
}

Реализация поставщика запросов не полностью корректна, поскольку предполагает что только пользовательские Queryable<T> будут вызывать Execute методы для создания IEnumerable<T>, а внешние вызовы будут использоваться только для немедленных методов, таких как Count, Max, *1058* et c., но это должно работать для этого сценария.

Другим недостатком этой реализации является то, что все расширения EF Core, указанные c Queryable, не будут работать, что может быть проблемой / showtopper, если OData $expand использует такие методы, как Include / ThenInclude. Но исправление, которое требует более сложной реализации, копаясь во внутренностях EF Core.

С учетом сказанного, использование, конечно, будет:

return Ok(_carsDataModelContext.Cars.ToQueryable());
...