The provider for the source IQueryable doesn't implement IAsyncQueryProvider
Asked Answered
V

7

50

I have some codes like below, I want to write unit tests my method. But I'm stuck in async methods. Can you help me please ?

public class Panel
{
    public int Id { get; set; }
    [Required] public double Latitude { get; set; }
    public double Longitude { get; set; }
    [Required] public string Serial { get; set; }
    public string Brand { get; set; }
}

public class CrossSolarDbContext : DbContext
{
    public CrossSolarDbContext()
    {
    }

    public CrossSolarDbContext(DbContextOptions<CrossSolarDbContext> options) : base(options)
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    }
}

public interface IGenericRepository<T>
{
    Task<T> GetAsync(string id);

    IQueryable<T> Query();

    Task InsertAsync(T entity);

    Task UpdateAsync(T entity);
}

public abstract class GenericRepository<T> : IGenericRepository<T>
    where T : class, new()
{
    protected CrossSolarDbContext _dbContext { get; set; }

    public async Task<T> GetAsync(string id)
    {
        return await _dbContext.FindAsync<T>(id);
    }

    public IQueryable<T> Query()
    {
        return _dbContext.Set<T>().AsQueryable();
    } 

    public async Task InsertAsync(T entity)
    {
        _dbContext.Set<T>().Add(entity);
        await _dbContext.SaveChangesAsync();
    }

    public async Task UpdateAsync(T entity)
    {
        _dbContext.Entry(entity).State = EntityState.Modified;
        await _dbContext.SaveChangesAsync();
    }
}

public interface IPanelRepository : IGenericRepository<Panel> { }

public class PanelRepository : GenericRepository<Panel>, IPanelRepository
{
    public PanelRepository(CrossSolarDbContext dbContext)
    {
        _dbContext = dbContext;
    }
}

[Route("[controller]")]
public class PanelController : Controller
{
    private readonly IPanelRepository _panelRepository;

    public PanelController(IPanelRepository panelRepository)
    {
        _panelRepository = panelRepository;
    }

    // GET panel/XXXX1111YYYY2222
    [HttpGet("{panelId}")]
    public async Task<IActionResult> Get([FromRoute] string panelId)
    {
        Panel panel = await _panelRepository.Query().FirstOrDefaultAsync(x => x.Serial.Equals(panelId, StringComparison.CurrentCultureIgnoreCase));
        if (panel == null) return NotFound();
        return Ok(panel);
    }
}

public class PanelControllerTests
{
    private readonly PanelController _panelController;
    private static readonly Panel panel = new Panel { Id = 1, Brand = "Areva", Latitude = 12.345678, Longitude = 98.7655432, Serial = "AAAA1111BBBB2222" };

    private readonly IQueryable<Panel> panels = new List<Panel>() { panel }.AsQueryable();
    private readonly Mock<IPanelRepository> _panelRepositoryMock = new Mock<IPanelRepository>();

    public PanelControllerTests()
    {
        _panelRepositoryMock.Setup(x => x.Query()).Returns(panels);
        // I also tried this. I got another error 'Invalid setup on an extension method: x => x.FirstOrDefaultAsync<Panel>(It.IsAny<Expression<Func<Panel, Boolean>>>(), CancellationToken)'
        // _panelRepositoryMock.As<IQueryable<Panel>>().Setup(x => x.FirstOrDefaultAsync(It.IsAny<Expression<Func<Panel, bool>>>(), default(CancellationToken))).ReturnsAsync(panel);
        _panelController = new PanelController(_panelRepositoryMock.Object);
    }

    [Fact]
    public async Task Register_ShouldInsertOneHourElectricity()
    {
        IActionResult result = await _panelController.Get("AAAA1111BBBB2222");
        Assert.NotNull(result);
        var createdResult = result as CreatedResult;
        Assert.NotNull(createdResult);
        Assert.Equal(201, createdResult.StatusCode);
    }
}

I'm getting this error

The provider for the source IQueryable doesn't implement IAsyncQueryProvider. Only providers that implement IEntityQueryProvider can be used for Entity Framework asynchronous operations.

I think that I need to mock 'FirstOrDefaultAsync' but I'm not sure and I don't know how to do. I tried something, however it couldn't be compiled.

Vesica answered 25/6, 2018 at 12:12 Comment(4)
note really an answer, but an unrelated tip: if the only thing async in a method is an await on the last line, you can almost always remove the async modifier from the method and just return the downstream task - i.e. return _dbContext.SaveChangesAsync(); (not await) - this removes a level of Task indirection and avoids an async state machine in your method.Zaxis
@MarcGravell Thank you very much for your attention. Actually Get method is not so short like this. It contains another operations as well. I did it short for better understanding of the main purpose. Sorry for causing confusion.Vesica
You're better off doing integration tests with controller actions. Controllers require so much set up and mocking to function correctly via a unit test, that it pretty much invalidates the unit test. Does it fail because it's actually failing or because you didn't mock something right? On the flip side, it might actually pass with your mocks but fail when run within a real request pipeline, because again, you might not have mocked everything just right. Instead, use the test host: learn.microsoft.com/en-us/aspnet/core/test/…Reikoreilly
@MarcGravell I actually avoid omitting the async because it can break Exception handling.Bluster
S
53

I get stuck on this issue today and this lib resolve it for me https://github.com/romantitov/MockQueryable completely, please refer:

Mocking Entity Framework Core operations such ToListAsync, FirstOrDefaultAsync etc.

//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.AsQueryable().BuildMock();

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

//3 - setup the mock as Queryable for NSubstitute
_userRepository.GetQueryable().Returns(mock);
Suggestible answered 14/10, 2018 at 7:51 Comment(4)
Bullet #2 says to build by Extension, where is this extension, is it part of the Moq framework or something you built?Grandnephew
BrianH, I think it's part of the Moq framework, it's just a lib that is built by @author: romantitov, please contact him. ThanksSuggestible
There is also BuildMockDbSet() method available in this library, if someone need DbSet type.Siu
For EF Core the library Moq.EntityFrameworkCore can be used instead.Vaporish
O
13

You could implement an AsyncEnumerable which can be used like this:

private readonly IQueryable<Panel> panels = new AsyncEnumerable(new List<Panel>() 
{
    panel
});

Here is the implementation of it:

public class AsyncEnumerable<T> : EnumerableQuery<T>, IAsyncEnumerable<T>, IQueryable<T>
{
    public AsyncEnumerable(IEnumerable<T> enumerable) : base(enumerable) { }

    public AsyncEnumerable(Expression expression) : base(expression) { }

    public IAsyncEnumerator<T> GetEnumerator()
    {
        return new AsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
    }

    IQueryProvider IQueryable.Provider => new AsyncQueryProvider<T>(this);
}

The AsyncEnumerator class:

public class AsyncEnumerator<T> : IAsyncEnumerator<T>
{
    private readonly IEnumerator<T> _inner;

    public AsyncEnumerator(IEnumerator<T> inner)
    {
        _inner = inner;
    }

    public void Dispose()
    {
        _inner.Dispose();
    }

    public T Current => _inner.Current;

    public Task<bool> MoveNext(CancellationToken cancellationToken)
    {
        return Task.FromResult(_inner.MoveNext());
    }
}

The AsyncQueryProvider class:

public class AsyncQueryProvider<TEntity> : IAsyncQueryProvider
{
    private readonly IQueryProvider _inner;

    internal AsyncQueryProvider(IQueryProvider inner)
    {
        _inner = inner;
    }

    public IQueryable CreateQuery(Expression expression)
    {
        return new AsyncEnumerable<TEntity>(expression);
    }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        return new AsyncEnumerable<TElement>(expression);
    }

    public object Execute(Expression expression)
    {
        return _inner.Execute(expression);
    }

    public TResult Execute<TResult>(Expression expression)
    {
        return _inner.Execute<TResult>(expression);
    }

    public IAsyncEnumerable<TResult> ExecuteAsync<TResult>(Expression expression)
    {
        return new AsyncEnumerable<TResult>(expression);
    }

    public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute<TResult>(expression));
    }
}
Outcome answered 25/6, 2018 at 14:10 Comment(8)
I have tried this too, but it doesn't work. It gives the same error for IQueryable. Link: msdn.microsoft.com/en-us/library/dn314429(v=vs.113).aspxVesica
It does work in EF Core 2.2 when return empty IQueryable and ToListAsyncVtarj
This doesn't work with EF Core 3.1.3, I assume the interfaces have changed (for example IAsyncQueryProvider has XML documentation on it warning that it's intended as an internal API and might change without notice). I haven't looked into when the interfaces changed, though I'd guess it's the same for all versions of EF Core 3.Manual
@Manual yeah Im struggling with the same issue at the mo. Would be nice if the answer can be updated to support EF Core 3.1.xGurevich
@Tim: See here for a Core 3.1 solution: https://mcmap.net/q/205243/-iasyncqueryprovider-mock-issue-when-migrated-to-net-core-3-adding-tresult-iasyncqueryproviderPostbellum
@Gurevich I assume you mean the repo in the answer linked by Gary McGill? I've updated the link there. The repo still exists, it's just that the file which was originally linked no longer exists. The code is still in the repo, it's just been tweaked a bit and moved around.Manual
Thanks @Tim, I think I can accept this as a workaround.Gurevich
Here's implementation for .net core 3.1 and higher https://mcmap.net/q/203132/-how-to-mock-an-async-repository-with-entity-framework-coreIglesias
O
8

For issue in entity framework core ,use Moq.EntityFrameworkCore library and setup your dbset as below:

contextMock.Setup(x => x.entity).ReturnsDbSet(entityList);
Outsell answered 31/8, 2021 at 13:18 Comment(1)
Fixed the problem. Thank you!Embrey
Z
7

This is because of your mocking approach; your mock provider just returns panels for Query, and panels is a simple object with LINQ-to-Objects exposing it as queryable:

private readonly IQueryable<Panel> panels = new List<Panel>() { panel }.AsQueryable();

Indeed, this does not implement IAsyncQueryProvider. If you can get hold of the regular query provider, you should be able to wrap that with a fake always-synchronous version to spoof it (just use return Task.FromResult(Execute(expression))), but frankly I'm not sure that this would be a useful test... at that point you're skipping so many of the important realities of async that it probably isn't worth it.

Zaxis answered 25/6, 2018 at 12:34 Comment(7)
I am facing the similar problem with similar code. How it can be fixed now?Dink
@Dink by preferring integration tests to unit testsZaxis
Gravel,Thank you! I have already solved this issue perfectly :) with the help of a answer to the another question.Dink
@Dink Can you share the answer?Pegpega
@Dink bumpGurevich
@MarcGravell I'd say now when the EF Core 3.1 is available there would be a huge benefit of having the possibility to test async LINQ queries just in C#. The result in C# should be exactly the same as the one after query transformation to SQL. It's just faster to unit testing something, integration tests are very expensive and require a proper setup which often is not something that can be achieved very quickly and easily...Gurevich
@Gurevich I think I'm going to keep leaning towards integration testing here; unit testing where the mock is this complicated: isn't very useful IMO; maybe invest more in the "proper setup" - I've never found it a barrierZaxis
G
0

if you are using EntityFrameworkCore, make sure instead of

using System.Data.Entity;

you are using

 using Microsoft.EntityFrameworkCore;
Gatian answered 22/2 at 16:24 Comment(0)
A
-3

I was facing the same issue and solved it that way:

Instead of having my method public async Task I switched it to public void and then resolved the asynchronous methods with .GetAwaiter() instead of await.

Alb answered 13/2, 2021 at 16:43 Comment(1)
When you say you change the method signature to public void, do you mean the test method (in the example in the question, Register_ShouldInsertOneHourElectricity)?Pinsk
L
-9

When querying for panel, removing the async in firstordefault.

Also remove the async from tolist when querying for analytics

Learning answered 28/6, 2018 at 21:56 Comment(2)
I think the downvotes here (I didn't downvote) are probably because removing async is seen as a backwards step.Gemmell
Yeah and you can fix all bugs by deleting all of your code...Tranship

© 2022 - 2024 — McMap. All rights reserved.