问题
I have a MVC project on ASP.NET Core, my problem is connected with IQueryable and asynchronous. I wrote the following method for search in 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()
:
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()
The source IQueryable doesn't implement IAsyncEnumerable. Only sources that implement IAsyncEnumerable can be used for Entity Framework asynchronous operations.
I had changed ToArrayAsync()
to ToListAsync()
but nothing have changed. I have searched this problem for a while, but resolved questions are connected mostly with DbContext
and entity creating. EntityFramework is not installed for this project and it's better not to do it because of application architecture. Hope someone has any ideas what to do in my situation.
回答1:
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
. For example you can extend EnumerableQuery
(which is returned by 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
).
2) Change ToArrayAsync
part:
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
. In your case this only happens when query is really in-memory list and not query, so async execution does not make sense anyway.
回答2:
For 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 );
}
回答3:
As noted by @Titian Cernicova-Dragomir the exception means that List<InternalOrderInfo>
doesn't implement 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. You really need to read more about the difference between IQueryable
and IEnumarable
and what you should return from the method. 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
.
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 i.e call ToArray
instead of ToArrayAsync
, and return IEnumerable
instead of Taks<IQueryable>
from both WhereSearchTokens
and ExecuteAsync
.
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
回答4:
The AsQueryable()
will not transform the result
list into an Entity Framework IQueryable
. And as the error states, the IQueryable
that are used with ToArrayAsync()
should implement IAsyncEnumerable
, which is not what AsQueryable
will return.
You can read more about the uses of AsQueryable
on enumerables here.
来源:https://stackoverflow.com/questions/48743165/toarrayasync-throws-the-source-iqueryable-doesnt-implement-iasyncenumerable