ToArrayAsync() 抛出“源 IQueryable 未实现 IAsyncEnumerable”

Posted

技术标签:

【中文标题】ToArrayAsync() 抛出“源 IQueryable 未实现 IAsyncEnumerable”【英文标题】:ToArrayAsync() throws "The source IQueryable doesn't implement IAsyncEnumerable" 【发布时间】:2018-02-12 09:30:57 【问题描述】:

我在 ASP.NET Core 上有一个 MVC 项目,我的问题与 IQueryable 和异步有关。我在IQueryable<T>写了如下搜索方法:

private IQueryable<InternalOrderInfo> WhereSearchTokens(IQueryable<InternalOrderInfo> query, SearchToken[] searchTokens)

    if (searchTokens.Length == 0)
    
        return query;
    
    var results = new List<InternalOrderInfo>();
    foreach (var searchToken in searchTokens)
    
        //search logic, intermediate results are being added to `results` using `AddRange()`
    

    return results.Count != 0 ? results.Distinct().AsQueryable() : query;

我在方法ExecuteAsync()中调用它:

public async Task<GetAllInternalOrderInfoResponse> ExecuteAsync(GetAllInternalOrderInfoRequest request)

    //rest of the code
    if (searchTokens != null && searchTokens.Any())
    
        allInternalOrderInfo = WhereSearchTokens(allInternalOrderInfo, searchTokens);
    
    var orders = await allInternalOrderInfo.Skip(offset).Take(limit).ToArrayAsync();
    //rest of the code

当我对此进行测试时,我在调用 ToArrayAsync() 的地方得到一个 InvalidOperationException

源 IQueryable 未实现 IAsyncEnumerable。只有实现 IAsyncEnumerable 的源才能用于实体框架异步操作。

我已将ToArrayAsync() 更改为ToListAsync(),但没有任何改变。我已经搜索了这个问题一段时间,但解决的问题主要与DbContext 和实体创建有关。该项目没有安装EntityFramework,由于应用程序架构,最好不要安装。希望有人对我的情况有什么想法。

【问题讨论】:

有趣的是,可查询框架假定您在与实体框架无关的地方使用实体框架,并且可以完全单独使用。 您对AsQueryable 的调用是一个错误。在这种情况下,它使您能够调用不受支持的行为,将错误推送到运行时。你为什么要这么做? @AluanHaddad 我在AsQueryable 中解决了问题,但不知道如何处理它。我使用它是因为我必须将Distinct 的结果转换为与方法返回类型相对应。我无法更改它,因为allInternalOrderInfo 也是IQueryable。该变量的值来自存储(repository)。 如果您不打算更改设计 - 您需要将 AsQueryable() 更改为返回 IQueryable 的东西,这也实现了 IDbAsyncEnumerable (这将向这个库引入对 EF 的依赖),或将ToArrayAsync 更改为不会抛出此异常的方法。 @mjwills results.Count 的值为 2。请阅读我之前关于方法返回类型的评论。 【参考方案1】:

如果您不打算更改设计 - 您有多种选择:

1) 将AsQueryable 更改为另一个返回IQueryable 的方法,该方法也实现了IDbAsyncEnumerable。例如你可以扩展EnumerableQuery(由AsQueryable返回):

public class AsyncEnumerableQuery<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T> 
    public AsyncEnumerableQuery(IEnumerable<T> enumerable) : base(enumerable) 
    

    public AsyncEnumerableQuery(Expression expression) : base(expression) 
    

    public IDbAsyncEnumerator<T> GetAsyncEnumerator() 
        return new InMemoryDbAsyncEnumerator<T>(((IEnumerable<T>) this).GetEnumerator());
    

    IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator() 
        return GetAsyncEnumerator();
    

    private class InMemoryDbAsyncEnumerator<T> : IDbAsyncEnumerator<T> 
        private readonly IEnumerator<T> _enumerator;

        public InMemoryDbAsyncEnumerator(IEnumerator<T> enumerator) 
            _enumerator = enumerator;
        

        public void Dispose() 
        

        public Task<bool> MoveNextAsync(CancellationToken cancellationToken) 
            return Task.FromResult(_enumerator.MoveNext());
        

        public T Current => _enumerator.Current;

        object IDbAsyncEnumerator.Current => Current;
    

然后你改变

results.Distinct().AsQueryable()

new AsyncEnumerableQuery<InternalOrderInfo>(results.Distinct())

之后,ToArrayAsync 将不再抛出异常(显然您可以创建自己的扩展方法,如 AsQueryable)。

2) 更改ToArrayAsync 部分:

public static class EfExtensions 
    public static Task<TSource[]> ToArrayAsyncSafe<TSource>(this IQueryable<TSource> source) 
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        if (!(source is IDbAsyncEnumerable<TSource>))
            return Task.FromResult(source.ToArray());
        return source.ToArrayAsync();
    

并使用ToArrayAsyncSafe 而不是ToArrayAsync,如果IQueryable 不是IDbAsyncEnumerable,它将回退到同步枚举。在您的情况下,这只发生在查询实际上是内存列表而不是查询时,因此异步执行无论如何都没有意义。

【讨论】:

我尝试了第一个选项,VS找不到IDbAsyncEnumerator并说我必须实现IDbAsyncEnumerable.GetEnumerator() @QuarK 对于第一个选项,您必须引用实体框架库,因为这是定义 IDbAsyncEnumerableIDbAsyncEnumerator 的地方。如果这不是您的选择 - 您必须选择选项 2。 对不起,我忘了这一切。谢谢,第二个选项效果很好。 如果没有其他选项,您应该使用它。这确实是代码中不必要的大开销,它并没有解决原始问题,它只是消除了后果 @Albert 我同意如果可能的话最好使用另一种方式,但我不同意这样做会产生巨大的开销(甚至是任何开销)。通过 OPs 设计,有一种方法有时会返回非物化查询(真正的 IQueryable),有时会返回物化查询。使用ToArrayAsyncSafe(例如)在这里没有开销,因为它将实现真正的查询,并且基本上对已经实现的查询不做任何事情。【参考方案2】:

我发现我必须做更多的工作才能让事情顺利进行:

namespace TestDoubles

    using Microsoft.EntityFrameworkCore.Query.Internal;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Threading;
    using System.Threading.Tasks;

    public static class AsyncQueryable
    
        /// <summary>
        /// Returns the input typed as IQueryable that can be queried asynchronously
        /// </summary>
        /// <typeparam name="TEntity">The item type</typeparam>
        /// <param name="source">The input</param>
        public static IQueryable<TEntity> AsAsyncQueryable<TEntity>(this IEnumerable<TEntity> source)
            => new AsyncQueryable<TEntity>(source ?? throw new ArgumentNullException(nameof(source)));
    

    public class AsyncQueryable<TEntity> : EnumerableQuery<TEntity>, IAsyncEnumerable<TEntity>, IQueryable<TEntity>
    
        public AsyncQueryable(IEnumerable<TEntity> enumerable) : base(enumerable)  
        public AsyncQueryable(Expression expression) : base(expression)  
        public IAsyncEnumerator<TEntity> GetEnumerator() => new AsyncEnumerator(this.AsEnumerable().GetEnumerator());
        public IAsyncEnumerator<TEntity> GetAsyncEnumerator(CancellationToken cancellationToken = default) => new AsyncEnumerator(this.AsEnumerable().GetEnumerator());
        IQueryProvider IQueryable.Provider => new AsyncQueryProvider(this);

        class AsyncEnumerator : IAsyncEnumerator<TEntity>
        
            private readonly IEnumerator<TEntity> inner;
            public AsyncEnumerator(IEnumerator<TEntity> inner) => this.inner = inner;
            public void Dispose() => inner.Dispose();
            public TEntity Current => inner.Current;
            public ValueTask<bool> MoveNextAsync() => new ValueTask<bool>(inner.MoveNext());
#pragma warning disable CS1998 // Nothing to await
            public async ValueTask DisposeAsync() => inner.Dispose();
#pragma warning restore CS1998
        

        class AsyncQueryProvider : IAsyncQueryProvider
        
            private readonly IQueryProvider inner;
            internal AsyncQueryProvider(IQueryProvider inner) => this.inner = inner;
            public IQueryable CreateQuery(Expression expression) => new AsyncQueryable<TEntity>(expression);
            public IQueryable<TElement> CreateQuery<TElement>(Expression expression) => new AsyncQueryable<TElement>(expression);
            public object Execute(Expression expression) => inner.Execute(expression);
            public TResult Execute<TResult>(Expression expression) => inner.Execute<TResult>(expression);
            public IAsyncEnumerable<TResult> ExecuteAsync<TResult>(Expression expression) => new AsyncQueryable<TResult>(expression);
            TResult IAsyncQueryProvider.ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken) => Execute<TResult>(expression);
        
    

这使我能够编写这样的测试:

    [TestCase("", 3, 5)]
    [TestCase("100", 2, 4)]
    public async Task GetOrderStatusCounts_ReturnsCorrectNumberOfRecords(string query, int expectedCount, int expectedStatusProductionCount)
    
        // omitted CreateOrder helper function

        const int productionStatus = 6;
        const int firstOtherStatus = 5;
        const int otherOtherStatus = 7;

        var items = new[]
        
            CreateOrder(1, "100000", firstOtherStatus, 1),
            CreateOrder(2, "100000", firstOtherStatus, 4),
            CreateOrder(3, "100000", productionStatus, 4),
            CreateOrder(4, "100001", productionStatus, 4),
            CreateOrder(5, "100100", productionStatus, 4),
            CreateOrder(6, "200000", otherOtherStatus, 4),
            CreateOrder(7, "200001", productionStatus, 4),
            CreateOrder(8, "200100", productionStatus, 4)
        .AsAsyncQueryable(); // this is where the magic happens

        var mocker = new AutoMocker();

        // IRepository implementation is also generic and calls DBCntext
        // for easier testing
        mocker.GetMock<IRepository<Order>>() 
            .Setup(m => m.BaseQuery()
            .Returns(items); 
            // the base query is extended in the system under test.
            // that's the behavior I'm testing here

        var sut = mocker.CreateInstance<OrderService>();

        var counts = await sut.GetOrderStatusCountsAsync(4, query);

        counts.Should().HaveCount(expectedCount);
        counts[OrderStatus.Production].Should().Be(expectedStatusProductionCount);
    

【讨论】:

【参考方案3】:

对于 EF Core:

public static class QueryableExtensions

    public static IQueryable<T> AsAsyncQueryable<T>(this IEnumerable<T> input)
    
        return new NotInDbSet<T>( input );
    



public class NotInDbSet< T > : IQueryable<T>, IAsyncEnumerable< T >, IEnumerable< T >, IEnumerable

    private readonly List< T > _innerCollection;
    public NotInDbSet( IEnumerable< T > innerCollection )
    
        _innerCollection = innerCollection.ToList();
    


    public IAsyncEnumerator< T > GetAsyncEnumerator( CancellationToken cancellationToken = new CancellationToken() )
    
        return new AsyncEnumerator( GetEnumerator() );
    

    public IEnumerator< T > GetEnumerator()
    
        return _innerCollection.GetEnumerator();
    

    IEnumerator IEnumerable.GetEnumerator()
    
        return GetEnumerator();
    

    public class AsyncEnumerator : IAsyncEnumerator< T >
    
        private readonly IEnumerator< T > _enumerator;
        public AsyncEnumerator( IEnumerator< T > enumerator )
        
            _enumerator = enumerator;
        

        public ValueTask DisposeAsync()
        
            return new ValueTask();
        

        public ValueTask< bool > MoveNextAsync()
        
            return new ValueTask< bool >( _enumerator.MoveNext() );
        

        public T Current => _enumerator.Current;
    

    public Type ElementType => typeof( T );
    public Expression Expression => Expression.Empty();
    public IQueryProvider Provider => new EnumerableQuery<T>( Expression );

【讨论】:

这对我不起作用,因为我需要在我的实体中选择属性的子集。示例实体public class Thing public string SomeValue get; set; ,然后测试代码var works = await new List&lt;Thing&gt;().AsAsyncQueryable().ToListAsync(); 可以工作但var fails = await new List&lt;Thing&gt;().AsAsyncQueryable().Select(t =&gt; new ValueCopy = t.SomeValue ).ToListAsync(); 不起作用。它失败了Expression of type 'System.Void' cannot be used for parameter of type 'System.Linq.IQueryable... 我和 James R 有同样的问题。realbart 给出的答案确实对我有用。【参考方案4】:

对于 EFCore 晚了一点,但对于希望解决此类问题的其他人来说,一种可能的解决方案是更改代码以通过这种方式使用 Task.FromResult() 方法:

var result= await allInternalOrderInfo.Skip(offset).Take(limit);
var orders = await Task.FromResult(result.ToArray());

【讨论】:

我正处于从 .Net Framework 4.6.1 迁移大型解决方案的早期阶段,但遇到了一些依赖地狱。这让我可以将我的类库转换为 .Net Standard 2.0。 太好了,很高兴你来参加聚会。 :)【参考方案5】:

AsQueryable() 不会将result 列表转换为实体框架IQueryable。并且正如错误所述,与ToArrayAsync() 一起使用的IQueryable 应该实现IAsyncEnumerable,这不是AsQueryable 将返回的内容。

您可以阅读更多关于 AsQueryable 在枚举 here 上的用法。

【讨论】:

【参考方案6】:

我写了一个 ICollection 扩展 AsAsyncQueryable 我在我的测试中使用

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

namespace Whatevaaaaaaaa

    public static class ICollectionExtensions
    
        public static IQueryable<T> AsAsyncQueryable<T>(this ICollection<T> source) =>
            new AsyncQueryable<T>(source.AsQueryable());
    

    internal class AsyncQueryable<T> : IAsyncEnumerable<T>, IQueryable<T>
    
        private IQueryable<T> Source;

        public AsyncQueryable(IQueryable<T> source)
        
            Source = source;
        

        public Type ElementType => typeof(T);

        public Expression Expression => Source.Expression;

        public IQueryProvider Provider => new AsyncQueryProvider<T>(Source.Provider);

        public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
        
            return new AsyncEnumeratorWrapper<T>(Source.GetEnumerator());
        

        public IEnumerator<T> GetEnumerator() => Source.GetEnumerator();
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    

    internal class AsyncQueryProvider<T> : IQueryProvider
    
        private readonly IQueryProvider Source;

        public AsyncQueryProvider(IQueryProvider source)
        
            Source = source;
        

        public IQueryable CreateQuery(Expression expression) =>
            Source.CreateQuery(expression);

        public IQueryable<TElement> CreateQuery<TElement>(Expression expression) =>
            new AsyncQueryable<TElement>(Source.CreateQuery<TElement>(expression));

        public object Execute(Expression expression) => Execute<T>(expression);

        public TResult Execute<TResult>(Expression expression) =>
            Source.Execute<TResult>(expression);
    



    internal class AsyncEnumeratorWrapper<T> : IAsyncEnumerator<T>
    
        private readonly IEnumerator<T> Source;

        public AsyncEnumeratorWrapper(IEnumerator<T> source)
        
            Source = source;
        

        public T Current => Source.Current;

        public ValueTask DisposeAsync()
        
            return new ValueTask(Task.CompletedTask);
        

        public ValueTask<bool> MoveNextAsync()
        
            return new ValueTask<bool>(Source.MoveNext());
        
    


【讨论】:

【参考方案7】:

正如@Titian Cernicova-Dragomir 所述,异常意味着List&lt;InternalOrderInfo&gt; 没有实现IAsyncEnumerable

但这是一个逻辑/设计错误。如果您的方法与IQueryable 一起使用并返回IQueryable,则它应该与IQueryable 一起使用,而不是与IEnumarable 一起使用,假设集合在app 的内存中。您确实需要阅读更多关于IQueryableIEnumarable 之间的区别以及您应该从该方法返回的内容。一个好的开始是阅读答案here和here

因此,由于您已经在 WhereSearchTokens 方法中甚至之前从 db 中获取了结果,因此没有理由对 db 进行异步请求,这将由 ToArrayAsync 完成并返回 IQueryable

您有两种选择:

1) 如果您的InternalOrderInfo 集合在WhereSearchTokens 之前从数据库提取到内存中,则使您的所有操作都处于同步模式,即调用ToArray 而不是ToArrayAsync,并返回IEnumerable 而不是Taks&lt;IQueryable&gt;来自WhereSearchTokensExecuteAsync

2) 如果您的 InternalOrderInfo 集合在 WhereSearchTokens 中获取,并且您想要对 db 执行异步请求,您需要仅在 //search logic, intermediate results are being added to results using AddRange() 的某处调用异步 EF API 并再次返回 Taks&lt;IEnumerable&gt; 而不是 @ 987654346@来自WhereSearchTokens

【讨论】:

WhereSearchTokens 中没有对 db 的调用,它只适用于query 参数。 IQueryable 以隐式惰性方式调用 db。而且我相信它会在AddRange 期间或之前调用db。 是的,在WhereSearchTokens之前有对db的调用。【参考方案8】:

最好使用 IAsyncEnumerable 和 IQueriable 实现集合,而不是创建自己的 ToListAsync 扩展。

你不能在库中应用你的扩展

Fore EF Core 5 及更高版本检查此implementation 和tests。

短版:

public sealed class FixedQuery<T> : IAsyncEnumerable<T>, IQueryable<T>

    public static readonly IQueryable<T> Empty = Create(ArraySegment<T>.Empty);

    public static IQueryable<T> Create(params T[] items)
    
        return Create((IEnumerable<T>)items);
    
    public static IQueryable<T> Create(IEnumerable<T> items)
    
        return new FixedQuery<T>(items ?? ArraySegment<T>.Empty).AsQueryable();
    

    private readonly IQueryable<T> _items;

    private FixedQuery(IEnumerable<T> items)
    
        _items = (items ?? throw new ArgumentNullException(nameof(items))).AsQueryable();
    

    #pragma warning disable CS1998
    public async IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
    #pragma warning restore CS1998
    
        foreach (var item in _items)
        
            yield return item;
        
    

    public IEnumerator<T> GetEnumerator()
    
        return _items.GetEnumerator();
    
    IEnumerator IEnumerable.GetEnumerator()
    
        return GetEnumerator();
    

    public Type ElementType => _items.ElementType;
    public Expression Expression => _items.Expression;
    public IQueryProvider Provider => _items.Provider;

【讨论】:

以上是关于ToArrayAsync() 抛出“源 IQueryable 未实现 IAsyncEnumerable”的主要内容,如果未能解决你的问题,请参考以下文章

Nuget 包 - 提要 (VSTS):尝试添加源时抛出异常“System.AggregateException”

swift 构建OSX拖放操场:将dragndrop.swift抛出到共享源中,然后测试单个playgro中的示例

使用选定列更改表 - debezium 抛出 ArrayIndexOutOfBoundsException

为什么即使目标阵列很小,strcpy也不会抛出错误? [重复]

泛型类继承和 LINQ 扩展方法

HSQLDB 在检查点期间在 db.script.new 文件上抛出 Asset failed 异常和文件 io 错误