【问题标题】:The provider for the source IQueryable doesn't implement IAsyncQueryProvider源 IQueryable 的提供程序未实现 IAsyncQueryProvider
【发布时间】:2018-12-04 00:20:45
【问题描述】:

我有一些如下代码,我想编写单元测试我的方法。但我被困在异步方法中。你能帮我吗 ?

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

我收到了这个错误

源 IQueryable 的提供程序未实现 IAsyncQueryProvider。只有实现 IEntityQueryProvider 的提供程序才能用于实体框架异步操作。

我认为我需要模拟“FirstOrDefaultAsync”,但我不确定,也不知道该怎么做。我尝试了一些东西,但它无法编译。

【问题讨论】:

  • 注意确实是一个答案,但一个不相关的提示:如果方法中的 only 事物 async 是最后一行的 await,您几乎总是可以删除方法中的 async 修饰符和下游任务的 return - 即 return _dbContext.SaveChangesAsync();(不是 await) - 这将删除 Task 间接级别并避免方法中的异步状态机。
  • @MarcGravell 非常感谢您的关注。其实Get 方法并没有这么短。它还包含其他操作。我这样做是为了更好地理解主要目的。很抱歉造成混乱。
  • 您最好使用控制器操作进行集成测试。控制器需要大量设置和模拟才能通过单元测试正确运行,这几乎会使单元测试无效。它失败是因为它实际上失败了还是因为你没有正确地模拟某些事情?另一方面,它实际上可能会通过您的模拟,但在真正的请求管道中运行时会失败,因为同样,您可能没有正确模拟所有内容。而是使用测试主机:docs.microsoft.com/en-us/aspnet/core/test/…
  • @MarcGravell 我实际上避免省略异步,因为它会破坏异常处理。

标签: c# unit-testing entity-framework-6 asp.net-core-2.0 iqueryable


【解决方案1】:

这是因为你的嘲笑方法;您的模拟提供程序只为Query 返回panels,而panels 是一个简单的对象,带有 LINQ-to-Objects 将其公开为可查询:

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

确实,这并没有实现IAsyncQueryProvider。如果您可以获取 regular 查询提供程序,您应该能够使用伪造的始终同步版本来欺骗它(只需使用return Task.FromResult(Execute(expression))),但坦率地说我不确定这将是一个有用的测试......在这一点上,你跳过了async 的许多重要现实,这可能不值得。

【讨论】:

  • 我面临类似代码的类似问题。现在如何解决?
  • @TanvirArjel 通过更喜欢集成测试而不是单元测试
  • 砾石,谢谢!在另一个问题的答案的帮助下,我已经完美地解决了这个问题:)。
  • @TanvirArjel 你能分享答案吗?
  • @GoldenAge 我想我会继续倾向于这里的集成测试;单元测试 mock 复杂的地方:IMO 不是很有用;也许更多地投资于“正确的设置”——我从来没有发现它是一个障碍
【解决方案2】:

你可以实现一个AsyncEnumerable,它可以像这样使用:

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

下面是它的实现:

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

AsyncEnumerator 类:

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

AsyncQueryProvider 类:

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

【讨论】:

  • 这个我也试过了,还是不行。它对IQueryable 给出了相同的错误。链接:msdn.microsoft.com/en-us/library/dn314429(v=vs.113).aspx
  • 当返回空的 IQueryable 和 ToListAsync 时,它确实在 EF Core 2.2 中工作
  • 这不适用于 EF Core 3.1.3,我假设接口已更改(例如,IAsyncQueryProvider 上有 XML 文档,警告它是作为内部 API 使用的,可能会更改,恕不另行通知)。我没有调查接口何时发生变化,但我猜所有版本的 EF Core 3 都是一样的。
  • @Tim 是的,我正在努力解决同样的问题。如果可以更新答案以支持 EF Core 3.1.x,那就太好了
  • @Tim:请参阅此处了解 Core 3.1 解决方案:stackoverflow.com/a/58314109/98422
【解决方案3】:

查询面板时,先去掉异步还是默认。

同时在查询分析时从 tolist 中删除异步

【讨论】:

  • 我认为这里的反对票(我没有反对)可能是因为删除异步被视为倒退。
【解决方案4】:

我今天被这个问题卡住了,这个库为我完全解决了https://github.com/romantitov/MockQueryable,请参考:

模拟实体框架核心操作,如ToListAsyncFirstOrDefaultAsync

//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);

【讨论】:

  • Bullet #2 说通过扩展构建,这个扩展在哪里,它是 Moq 框架的一部分还是你构建的?
  • BrianH,我认为它是 Moq 框架的一部分,它只是一个由 @author: romantitov 构建的库,请联系他。谢谢
  • 如果有人需要 DbSet 类型,这个库中还有 BuildMockDbSet() 方法。
【解决方案5】:

我遇到了同样的问题并以这种方式解决了它:

我没有使用我的方法public async Task,而是将其切换为public void,然后使用.GetAwaiter()而不是await解析异步方法。

【讨论】:

    【解决方案6】:

    对于实体框架核心中的问题,请使用Moq.EntityFrameworkCore library 并设置您的数据库集,如下所示:

    contextMock.Setup(x => x.entity).ReturnsDbSet(entityList);
    

    【讨论】:

      猜你喜欢
      • 2021-12-09
      • 1970-01-01
      • 1970-01-01
      • 2020-01-09
      • 1970-01-01
      • 2019-02-21
      • 1970-01-01
      • 2019-02-09
      • 1970-01-01
      相关资源
      最近更新 更多