.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类重构为通用泛型实现,支持任意类型的异步查询:
- 实现通用的异步查询支持类
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; }
- 保持单元测试中的
GetMockEncompassLoanStatusSet方法不变即可,因为通用Provider会自动处理投影后的类型。
修改后,当查询经过Select投影为任意类型时,通用的异步查询Provider会创建对应类型的IAsyncEnumerable实现,满足EF Core的异步操作要求。
内容的提问来源于stack exchange,提问作者user14463446
相关产品推荐
相关产品推荐

