You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET5迁移后单元测试报错:IQueryable未实现IAsyncEnumerable

.NET Core 3.x迁移至.NET 5.0后单元测试异步查询报错问题

问题现象

  • 项目升级到.NET 5.0后,生产环境运行正常,但所有单元测试失败
  • 报错信息:

The source 'IQueryable' doesn't implement 'IAsyncEnumerable<Migo.API.Dtos.FundedLoanDto>'. Only sources that implement 'IAsyncEnumerable' can be used for Entity Framework asynchronous operations

  • 报错触发点:业务类中调用ToListAsync()的代码段
var fundedLoans = await loans.Select(loan => new FundedLoanDto()
{
    BorrowerName = $"{loan.FirstName} {loan.LastName}",
    FundedDate = loan.Funded,
    LoanId = loan.EncompassId,
    LoanProduct = loan.LoanProgramName,
    ReceivedDate = loan.Createddate,
    SalesRep = loan.Salesrep
}).ToListAsync();

业务类核心代码

public async Task<GridDto> GetFundedLoans(LoanFilterDto loanFilter)
{
    var query = GetLoanCommonFilter(loanFilter);
    query = query.Where(loan => EF.Functions.Like(loan.Status, "FUNDED"));
    if (loanFilter.FromDate.HasValue)
        query = query.Where(loan => loan.Funded.HasValue && loan.Funded.Value.Date >= loanFilter.FromDate.Value.Date);

    if (loanFilter.ToDate.HasValue)
        query = query.Where(loan => loan.Funded.HasValue && loan.Funded.Value.Date <= loanFilter.ToDate.Value);

    Expression<Func<EncompassLoanStatus, dynamic>> orderByExpression = GetLoanOrderByExpression(loanFilter.SortColumn);

    if (loanFilter.SortOrder?.ToUpper() == "DESC")
        query = query.OrderByDescending(orderByExpression);
    else
        query = query.OrderBy(orderByExpression);

    int recordsCount = query.Count();
    var loans = loanFilter.PageSize > 0 ? query.Skip((loanFilter.PageNumber - 1) * loanFilter.PageSize).Take(loanFilter.PageSize) : query;
    var fundedLoans = await loans.Select(loan => new FundedLoanDto()
    {
        BorrowerName = $"{loan.FirstName} {loan.LastName}",
        FundedDate = loan.Funded,
        LoanId = loan.EncompassId,
        LoanProduct = loan.LoanProgramName,
        ReceivedDate = loan.Createddate,
        SalesRep = loan.Salesrep
    }).ToListAsync();
    return new GridDto()
    {
        Records = fundedLoans,
        RecordsCount = recordsCount
    };
}

private IQueryable<EncompassLoanStatus> GetLoanCommonFilter(LoanFilterDto loanFilter)
{
    var query = _context.EncompassLoanStatus.AsQueryable();
    if (string.IsNullOrEmpty(loanFilter.BorrowerFirstName) == false)
        query = query.Where(loan => EF.Functions.Like(loan.FirstName, $"%{loanFilter.BorrowerFirstName}%"));

    if (string.IsNullOrEmpty(loanFilter.BorrowerLastName) == false)
        query = query.Where(loan => EF.Functions.Like(loan.LastName, $"%{loanFilter.BorrowerLastName}%"));

    if (string.IsNullOrEmpty(loanFilter.LoanProduct) == false)
        query = query.Where(loan => EF.Functions.Like(loan.LoanProgramName, $"%{loanFilter.LoanProduct}%"));

    if (loanFilter.DealerPhoneNumbers?.Length > 0)
        query = query.Where(loan => loanFilter.DealerPhoneNumbers.Contains(loan.Dealerphone));

    if (string.IsNullOrEmpty(loanFilter.SalesRep) == false)
        query = query.Where(loan => EF.Functions.Like(loan.Salesrep, $"%{loanFilter.SalesRep}%"));

    return query;
}

private Expression<Func<EncompassLoanStatus, dynamic>> GetLoanOrderByExpression(string orderByColumn)
{
    Expression<Func<EncompassLoanStatus, dynamic>> orderByExpression;
    switch (orderByColumn?.ToUpper())
    {
        case "BORROWERNAME": orderByExpression = loan => loan.FirstName; break;
        case "LOANPRODUCT": orderByExpression = loan => loan.LoanProgramName; break;
        case "SALESREP": orderByExpression = loan => loan.Salesrep; break;
        case "CANCELLEDDATE": orderByExpression = loan => loan.Cancelled; break;
        case "FUNDEDDATE": orderByExpression = loan => loan.Funded; break;

        default: orderByExpression = loan => loan.Createddate; break;
    }
    return orderByExpression;
}

单元测试核心代码

[Theory]
[InlineData("BORROWERNAME")]
public async Task GetFundedLoansSortsCorrectly(string sortColumn)
{
    var mockEncompassLoanStatusSet = GetMockEncompassLoanStatusSet(FundedEncompassLoanStatuses);
    
    _mockDigitalApplicationContext.Setup(x => x.EncompassLoanStatus).Returns(mockEncompassLoanStatusSet.Object);

    var loanFilter = new LoanFilterDto
    {
        SortColumn = sortColumn,
        PageSize = 1,
        PageNumber = 1
    };

    var cancelledLoans = await _digitalApplicationRepository.GetFundedLoans(loanFilter);
    var records = ((List<FundedLoanDto>)cancelledLoans.Records).ToArray();

    Assert.Equal(3, cancelledLoans.RecordsCount);
    Assert.Equal("asdh", records[0].LoanId);
    Assert.Equal("asdg", records[1].LoanId);
    Assert.Equal("asdf", records[2].LoanId);
}

private static Mock<DbSet<EncompassLoanStatus>> GetMockEncompassLoanStatusSet(List<EncompassLoanStatus> loanStatuses)
{
    var mockEncompassLoanStatuses = loanStatuses.AsQueryable();

    var mockEncompassLoanStatusSet = new Mock<DbSet<EncompassLoanStatus>>();

    mockEncompassLoanStatusSet.As<IAsyncEnumerable<EncompassLoanStatus>>()
        .Setup(m => m.GetAsyncEnumerator(It.IsAny<CancellationToken>()))
        .Returns(new TestAsyncEnumerator<EncompassLoanStatus>(mockEncompassLoanStatuses.GetEnumerator()));

    mockEncompassLoanStatusSet.As<IQueryable<EncompassLoanStatus>>()
        .Setup(m => m.Provider)
        .Returns(new TestAsyncQueryProvider<EncompassLoanStatus>(mockEncompassLoanStatuses.Provider));

    mockEncompassLoanStatusSet.As<IQueryable<EncompassLoanStatus>>().Setup(m => m.Expression)
        .Returns(mockEncompassLoanStatuses.Expression);
    mockEncompassLoanStatusSet.As<IQueryable<EncompassLoanStatus>>().Setup(m => m.ElementType)
        .Returns(mockEncompassLoanStatuses.ElementType);
    mockEncompassLoanStatusSet.As<IQueryable<EncompassLoanStatus>>().Setup(m => m.GetEnumerator())
        .Returns(mockEncompassLoanStatuses.GetEnumerator());
    return mockEncompassLoanStatusSet;
}

问题原因

.NET 5.0对EF Core异步查询的验证逻辑更严格:单元测试中使用的TestAsyncQueryProvider是针对EncompassLoanStatus类型的,但业务代码调用Select后将查询结果投影成了FundedLoanDto类型,此时查询的Provider仍为原类型的实现,无法处理投影后的FundedLoanDto异步枚举,导致报错。

解决方案

将异步查询相关的Mock类重构为通用泛型实现,支持任意类型的异步查询:

  1. 实现通用的异步查询支持类
public class TestAsyncQueryProvider<TEntity> : IAsyncQueryProvider
{
    private readonly IQueryProvider _inner;

    public TestAsyncQueryProvider(IQueryProvider inner)
    {
        _inner = inner;
    }

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

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        return new TestAsyncEnumerable<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 TestAsyncEnumerable<TResult>(expression);
    }

    public TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
    {
        var resultType = typeof(TResult).GetGenericArguments()[0];
        var executeMethod = typeof(IQueryProvider).GetMethod(
            nameof(IQueryProvider.Execute),
            new[] { typeof(Expression) })
            ?.MakeGenericMethod(resultType);

        if (executeMethod == null)
            throw new InvalidOperationException("无法执行异步查询");

        return (TResult)executeMethod.Invoke(_inner, new[] { expression });
    }
}

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

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

    public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
    {
        return new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
    }

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

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

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

    public ValueTask DisposeAsync()
    {
        _inner.Dispose();
        return ValueTask.CompletedTask;
    }

    public ValueTask<bool> MoveNextAsync()
    {
        return new ValueTask<bool>(_inner.MoveNext());
    }

    public T Current => _inner.Current;
}
  1. 保持单元测试中的GetMockEncompassLoanStatusSet方法不变即可,因为通用Provider会自动处理投影后的类型。

修改后,当查询经过Select投影为任意类型时,通用的异步查询Provider会创建对应类型的IAsyncEnumerable实现,满足EF Core的异步操作要求。


内容的提问来源于stack exchange,提问作者user14463446

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 23:35:21