39

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.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
QuarK
  • 1,162
  • 2
  • 12
  • 27
  • 2
    Interesting that the queryable framework assumes that you are using the Entity Framework in a place that is agnostic of the Entity Framework and can be used completely separate. – Georg Feb 12 '18 at 09:37
  • 2
    Your call to `AsQueryable` is an error. In this case it is enabling you to invoke unsupported behavior, pushing an error to runtime. Why are you trying to do that anyway? – Aluan Haddad Feb 12 '18 at 09:37
  • @AluanHaddad I fought the problem is in `AsQueryable` but have no ideas what to do with it. I use it because I have to cast result of `Distinct` to correspond with method return type. I can't change it, because `allInternalOrderInfo` is also `IQueryable`. Value of this variable comes from storage (repository). – QuarK Feb 12 '18 at 09:50
  • 3
    If you are not going to change design - you need to either change `AsQueryable()` to something that returns `IQueryable` which also implements `IDbAsyncEnumerable` (which will introduce dependency on EF to this library), or change `ToArrayAsync` to method which will not throw this exception. – Evk Feb 12 '18 at 09:52
  • @mjwills The value of `results.Count` is 2. Please read my previous comment about method return type. – QuarK Feb 12 '18 at 09:55
  • @mjwills No, it will cause incompatible types in other place. – QuarK Feb 12 '18 at 09:58

11 Answers11

24

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);
    }
realbart
  • 3,497
  • 1
  • 25
  • 37
  • Thanks!! Your AsyncQueryable code snippet worked for me :)!!! – C.O.D.E Feb 08 '23 at 18:57
  • 1
    After upgrading to .Net 6 I'm getting an error: `must be reducible node` – Michael Armitage Feb 21 '23 at 18:36
  • This worked great except that to use `CountAsync` on the resulting queryable, I had to edit `IAsyncQueryProvider.ExecuteAsync` to explicitly create a `Task`: https://gist.github.com/SolalPirelli/461a3bb1f0bac781531c3aaea97a947f – Solal Pirelli Aug 12 '23 at 23:20
17

I wrote an ICollection extension AsAsyncQueryable that I use in my tests

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

Peter Morris
  • 20,174
  • 9
  • 81
  • 146
  • 2
    This solution correctly let me mock EF Core `DbSet.Where` return values with a copy-paste. Thanks for your help sir. – Kyle L. May 25 '22 at 18:20
12

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.

Evk
  • 98,527
  • 8
  • 141
  • 191
  • 1
    I have tried first option, VS cannot find `IDbAsyncEnumerator` and says that I have to implement `IDbAsyncEnumerable.GetEnumerator()`. – QuarK Feb 12 '18 at 11:44
  • @QuarK for first option you have to reference Entity Framework library, because that's where `IDbAsyncEnumerable` and `IDbAsyncEnumerator` are defined. If that's not an option for you - you have to go with option 2. – Evk Feb 12 '18 at 11:47
  • Sorry, I forgot about it in all this. Thanks, the second option works perfectly. – QuarK Feb 12 '18 at 11:58
  • You should use this if there are no other options. It is really big unnecessary overhead in code and it doesn't solve the original problem it just removes the consequences – AlbertK Feb 12 '18 at 12:29
  • @Albert I agree that's it's better to use another way if possible, but I don't agree that there is huge overhead (or even any overhead) in doing this. By OPs design there is method which sometimes returns unmaterialized query (real IQueryable) and sometimes materialized query. Using `ToArrayAsyncSafe` (for example) has no overhead here, because it will materialize real query and just basically do nothing with already materialized one. – Evk Feb 12 '18 at 12:34
8

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 );
}
Vladimir
  • 91
  • 1
  • 3
  • 1
    This does not work for me because I need to select a subset of the properties in my entity. Example entity `public class Thing { public string SomeValue { get; set; } }` and then test code `var works = await new List().AsAsyncQueryable().ToListAsync();` that works but `var fails = await new List().AsAsyncQueryable().Select(t => new { ValueCopy = t.SomeValue }).ToListAsync();` does not work. It fails with `Expression of type 'System.Void' cannot be used for parameter of type 'System.Linq.IQueryable...` – James R. Feb 03 '21 at 16:05
  • 1
    I have the same issue as James R. The answer given by realbart did work for me. – Simmetric Mar 30 '21 at 14:35
8

For 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:

var result= await allInternalOrderInfo.Skip(offset).Take(limit);
var orders = await Task.FromResult(result.ToArray());
Munesh Kumar
  • 481
  • 3
  • 10
5

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.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
2

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

AlbertK
  • 11,841
  • 5
  • 40
  • 36
1

ERROR Message:  System.InvalidOperationException : The source 'IQueryable' doesn't implement 'IAsyncEnumerable'. Only sources that implement 'IAsyncEnumerable' can be used for Entity Framework asynchronous operations.

For my case the solution: when you are mocking your dbContext and pass data from your mockSet to your context change .Returns to .ReturnsDbSet

Exemple: var mockContext = new Mock<IWebApiDbContext>(); mockContext.Setup(m => m.User).ReturnsDbSet(mockSet.Object);

Full Code Mock db:

    var mockSet = new Mock<DbSet<User>>();
    mockSet.As<IDbAsyncEnumerable<User>>()
      .Setup(m => m.GetAsyncEnumerator())
      .Returns(new TestDbAsyncEnumerator<User>(data.GetEnumerator()));

    mockSet.As<IQueryable<User>>()
       .Setup(m => m.Provider)
       .Returns(new TestDbAsyncQueryProvider<User>(data.Provider));

    mockSet.As<IQueryable<User>>().Setup(m => m.Expression).Returns(data.Expression);
    mockSet.As<IQueryable<User>>().Setup(m => m.ElementType).Returns(data.ElementType);
    mockSet.As<IQueryable<User>>().Setup(m => m.GetEnumerator()).Returns(() => data.GetEnumerator());

    var mockContext = new Mock<IWebApiDbContext>();
    mockContext.Setup(m => m.User).ReturnsDbSet(mockSet.Object);
EcchiBi
  • 51
  • 6
  • 1
    Thank you, this helped me. Commenting to add that this required me to install the `Moq.EntityFrameworkCore` Nuget package. – egbrad Nov 22 '22 at 19:42
0

It's better to implement collection with IAsyncEnumerable<T> and IQueryable<T> rather than create your own ToListAsync extensions.

You can't apply your extensions in libraries.

For EF Core 5 and above check this implementation and tests.

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;
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
0

I had the same error message. I know your question states that you don't want to install Entity Framework, but in my case other readers come to this question and don't have a similar constraint, changing

using System.Data.Entity;

to

using Microsoft.EntityFrameworkCore;

worked for me.

0

Use this extension MockQueryable

//1 - create a List<T> with test items
var users = new List<UserEntity>()
{
  new UserEntity{LastName = "ExistLastName", DateOfBirth = DateTime.Parse("01/20/2012")},
  ...
};

//2 - build mock by extension
var mock = users.BuildMock();

//3 - setup the mock as Queryable for Moq
_userRepository.Setup(x => x.GetQueryable()).Returns(mock);

//3 - setup the mock as Queryable for NSubstitute
_userRepository.GetQueryable().Returns(mock);

//3 - setup the mock as Queryable for FakeItEasy
A.CallTo(() => userRepository.GetQueryable()).Returns(mock);
  • 2
    How does this answer the question? First off, where is `IAsyncEnumerable` here? Second, I know that there are other answers for Entity Framework, but they explicitly state they don't want to install it. – Gert Arnold Aug 12 '23 at 11:45