web-dev-qa-db-ja.com

ToArrayAsync()は「ソースIQueryableはIAsyncEnumerableを実装していません」をスローします

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を実装していません。 Entity Frameworkの非同期操作に使用できるのは、IAsyncEnumerableを実装するソースのみです。

ToArrayAsync()ToListAsync()に変更しましたが、何も変更されていません。私はしばらくこの問題を検索しましたが、解決された質問は主にDbContextおよびエンティティの作成に関連しています。このプロジェクトにはEntityFrameworkがインストールされていません。アプリケーションのアーキテクチャ上、インストールしないことをお勧めします。誰かが私の状況で何をすべきか考えていることを願っています。

8
QuarK

デザインを変更しない場合は、いくつかのオプションがあります。

1)AsQueryableを、IQueryableも実装するIDbAsyncEnumerableを返す別のメソッドに変更します。たとえば、EnumerableQueryAsQueryableによって返される)を拡張できます。

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

そして、ToArrayAsyncSafeToArrayAsyncでない場合に同期列挙にフォールバックするIQueryableの代わりにIDbAsyncEnumerableを使用します。あなたの場合、これはクエリが実際にはメモリ内のリストであり、クエリではない場合にのみ発生するため、非同期実行はとにかく意味がありません。

10
Evk

@Titian Cernicova-Dragomirが述べたように、例外はList<InternalOrderInfo>IAsyncEnumerableを実装していないことを意味します

しかし、これは論理/設計エラーです。メソッドがIQueryableで動作し、IQueryableを返す場合、コレクションはアプリのメモリにあると想定するIQueryableではなく、IEnumarableで動作する必要があります。 IQueryableIEnumarableの違いと、メソッドから返す必要のあるものについて、もっと読む必要があります。まず、回答を読むことをお勧めします ここ および ここ

そのため、WhereSearchTokensメソッドまたはそれ以前にすでにdbから結果をフェッチしているため、ToArrayAsyncによって行われるdbへの非同期リクエストを実行してIQueryableを返す理由はありません。

ここには2つのオプションがあります:

1)InternalOrderInfoの前にWhereSearchTokensのコレクションがdbからメモリにフェッチされる場合、すべてのアクションを同期モードで実行します。つまり、ToArrayの代わりにToArrayAsyncを呼び出し、IEnumerableWhereSearchTokensの両方からTaks<IQueryable>ではなくExecuteAsyncを返します。

2)InternalOrderInfoのコレクションがWhereSearchTokens内でフェッチされ、dbへの非同期リクエストを行う場合は、//search logic, intermediate results are being added to results using AddRange()のどこかでのみ非同期EF APIを呼び出し、WhereSearchTokensからTaks<IEnumerable>ではなくTaks<IQueryable>を返す必要があります。

2
AlbertK

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

AsQueryable()は、resultリストをEntity Framework IQueryableに変換しません。そして、エラーが示すように、ToArrayAsync()で使用されるIQueryableIAsyncEnumerableを実装する必要がありますが、これはAsQueryableが返すものではありません。

Enumerables here でのAsQueryableの使用について詳しく読むことができます。