[英]ToArrayAsync() throws "The source IQueryable doesn't implement IAsyncEnumerable"
I have a MVC project on ASP.NET Core, my problem is connected with IQueryable and asynchronous.我在 ASP.NET 核心上有一个 MVC 项目,我的问题与 IQueryable 和异步有关。 I wrote the following method for search in
IQueryable<T>
:我在
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;
}
I call this in method ExecuteAsync()
:我在方法
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
}
When I test this I get an InvalidOperationException on line where I call ToArrayAsync()
当我测试这个时,我在调用
ToArrayAsync()
的地方得到一个 InvalidOperationException
The source IQueryable doesn't implement IAsyncEnumerable.
源 IQueryable 未实现 IAsyncEnumerable。 Only sources that implement IAsyncEnumerable can be used for Entity Framework asynchronous operations.
只有实现 IAsyncEnumerable 的源才能用于实体框架异步操作。
I had changed ToArrayAsync()
to ToListAsync()
but nothing have changed.我已将
ToArrayAsync()
更改为ToListAsync()
但没有任何改变。 I have searched this problem for a while, but resolved questions are connected mostly with DbContext
and entity creating.我已经搜索了这个问题一段时间,但解决的问题主要与
DbContext
和实体创建有关。 EntityFramework is not installed for this project and it's better not to do it because of application architecture.该项目没有安装EntityFramework,由于应用程序架构,最好不要安装。 Hope someone has any ideas what to do in my situation.
希望有人对我的情况有什么想法。
If you are not going to change your design - you have several options:如果您不打算更改您的设计 - 您有多种选择:
1) Change AsQueryable
to another method which returns IQueryable
which also implements IDbAsyncEnumerable
. 1) 将
AsQueryable
更改为另一个返回IQueryable
方法,该方法也实现了IDbAsyncEnumerable
。 For example you can extend EnumerableQuery
(which is returned by AsQueryable
):例如,您可以扩展
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;
}
}
Then you change那你改变
results.Distinct().AsQueryable()
to到
new AsyncEnumerableQuery<InternalOrderInfo>(results.Distinct())
And later, ToArrayAsync
will not throw exception any more (obviously you can create your own extension method like AsQueryable
).稍后,
ToArrayAsync
将不再抛出异常(显然您可以创建自己的扩展方法,如AsQueryable
)。
2) Change ToArrayAsync
part: 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();
}
}
And use ToArrayAsyncSafe
instead of ToArrayAsync
, which will fallback to synchronous enumeration in case IQueryable
is not IDbAsyncEnumerable
.并使用
ToArrayAsyncSafe
代替ToArrayAsync
,这将回退到同步枚举的情况下IQueryable
不IDbAsyncEnumerable
。 In your case this only happens when query is really in-memory list and not query, so async execution does not make sense anyway.在您的情况下,这只发生在查询确实是内存列表而不是查询时,因此异步执行无论如何都没有意义。
For EF Core:对于 EF 核心:
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 );
}
I found I had to do a bit more work to get things to work nicely:我发现我必须做更多的工作才能让事情顺利进行:
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);
}
}
}
This enables me to write tests like this:这使我能够编写这样的测试:
[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);
}
The AsQueryable()
will not transform the result
list into an Entity Framework IQueryable
. AsQueryable()
不会将result
列表转换为实体框架IQueryable
。 And as the error states, the IQueryable
that are used with ToArrayAsync()
should implement IAsyncEnumerable
, which is not what AsQueryable
will return.正如错误所述,与
ToArrayAsync()
一起使用的IQueryable
应该实现IAsyncEnumerable
,这不是AsQueryable
将返回的内容。
You can read more about the uses of AsQueryable
on enumerables here .您可以在此处
AsQueryable
有关可枚举的AsQueryable
使用的更多信息。
As noted by @Titian Cernicova-Dragomir the exception means that List<InternalOrderInfo>
doesn't implement IAsyncEnumerable
正如@Titian Cernicova-Dragomir 所指出的,异常意味着
List<InternalOrderInfo>
没有实现IAsyncEnumerable
But here is a logical/design error.但这是一个逻辑/设计错误。 If your method works with
IQueryable
and returns IQueryable
it should work with it as with IQueryable
and not as with IEnumarable
that assumes that collection is in a memory of app.如果您的方法适用于
IQueryable
并返回IQueryable
则它应该像IQueryable
一样使用它,而不是像IEnumarable
那样假定集合在应用程序的内存中。 You really need to read more about the difference between IQueryable
and IEnumarable
and what you should return from the method.你真的需要阅读更多关于
IQueryable
和IEnumarable
之间的区别以及你应该从方法返回什么的信息。 A good point to start is to read answers here and here一个好的开始是在这里和这里阅读答案
So, since you already fetched results from db in WhereSearchTokens
method or even before, there is no reason to do asynchronous request to db which is would be done by ToArrayAsync
and return IQueryable
.因此,由于您已经在
WhereSearchTokens
方法中甚至之前从 db 获取了结果,因此没有理由对 db 进行异步请求,这将由ToArrayAsync
完成并返回IQueryable
。
You have two options here:您在这里有两个选择:
1) If your collection of InternalOrderInfo
is fetched from db into memory before WhereSearchTokens
make your all actions in synchronous mode ie call ToArray
instead of ToArrayAsync
, and return IEnumerable
instead of Taks<IQueryable>
from both WhereSearchTokens
and ExecuteAsync
. 1) 如果您的
InternalOrderInfo
集合在WhereSearchTokens
使您的所有操作处于同步模式之前从数据库提取到内存中,即调用ToArray
而不是ToArrayAsync
,并从WhereSearchTokens
和ExecuteAsync
返回IEnumerable
而不是Taks<IQueryable>
。
2) If your collection of InternalOrderInfo
is fetched inside WhereSearchTokens
and you want to do the async request to db you need to call async EF API only somewhere in //search logic, intermediate results are being added to results using AddRange()
and again return Taks<IEnumerable>
istead of Taks<IQueryable>
from WhereSearchTokens
2) 如果您的
InternalOrderInfo
集合是在WhereSearchTokens
获取的,并且您想对 db 执行异步请求,则您只需要在//search logic, intermediate results are being added to results using AddRange()
某处调用异步 EF API //search logic, intermediate results are being added to results using AddRange()
并再次返回Taks<IEnumerable>
代替了来自WhereSearchTokens
的Taks<IQueryable>
For EFCore对于 EFCore
Bit late to the party but to others looking to resolve this type of problem, one of possible solution is to change code to use Task.FromResult()
method in this way:参加聚会有点晚,但对于希望解决此类问题的其他人来说,可能的解决方案之一是更改代码以通过这种方式使用
Task.FromResult()
方法:
var result= await allInternalOrderInfo.Skip(offset).Take(limit);
var orders = await Task.FromResult(result.ToArray());
I wrote an ICollection extension AsAsyncQueryable
that I use in my tests我写了一个 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());
}
}
}
It's better to implement collection with IAsyncEnumerable<T>
and IQueryable<T>
rather than create your own ToListAsync
extensions.最好使用
IAsyncEnumerable<T>
和IQueryable<T>
实现集合,而不是创建自己的ToListAsync
扩展。
You can't apply your extensions in libraries.您不能在库中应用您的扩展。
For EF Core 5 and above check this implementation and tests .对于 EF Core 5 及更高版本,请检查此实现和测试。
Short version:精简版:
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;
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.