使用xUnit与Moq测试FindAsync方法时遭遇IAsyncQueryProvider实现异常
问题描述
使用xUnit和Moq对ParticipantService的FindAsync方法做单元测试时,抛出以下异常:
Message: System.Exception : The provider for the source 'IQueryable' doesn't implement 'IAsyncQueryProvider'. Only providers that implement 'IAsyncQueryProvider' can be used for Entity Framework asynchronous operations.
测试代码:
[Fact] public async Task FindAsyncTestAsync() { var participantList = ParticipantMockData.ListAllAsyncEntity(); // 非空,包含2个元素 _participantRepository.Setup(_ => _.FindAsync(new Guid("ffbf486f-bf31-4bb4-8fc2-5beaae59e127"))) .Returns(Task.FromResult(participantList[0])); var sut = new ParticipantService( _participantRepository.Object, _participantSupportNeed.Object, _participantConnection.Object, _participantDisability.Object, _participantMedicalCondition.Object, _supportNeed.Object, _disabilityn.Object, _medicalCodition.Object, _participantApplicationUser.Object, _appLogger.Object); var result = await sut.FindAsync(new Guid("ffbf486f-bf31-4bb4-8fc2-5beaae59e127")); Assert.NotNull(result); Assert.Equal(participantList[0].Id, result?.Id); }
IRepository接口定义:
public interface IRepository<E> where E : IEntityBase { Task<List<E>> ListAsync(bool withInactives = false); IQueryable<E> AsNoTracking(bool withInactives = false); Task<E?> FindAsync(params object[] keyValues); }
ParticipantService的FindAsync实现:
public async Task<ParticipantAsyncModel?> FindAsync(Guid id) { try { var participant = await participantRepository.AsNoTracking() .Where(a => a.Id == id) .Include(a => a.ParticipantAddresses) .Include(a => a.ParticipantConnections)!.ThenInclude(a => a.Connection) .Include(a => a.ParticipantApplicationUsers)!.ThenInclude(a => a.ApplicationUser) .FirstOrDefaultAsync(); var participantView = new ParticipantAsyncModel() { // 省略对象初始化代码 }; if (participantView is null) return null; var participantModel = participantView.Adapt<ParticipantAsyncModel>(); return participantModel; } catch (Exception ex) { throw new Exception(ex.Message); } }
问题原因
ParticipantService的FindAsync方法并未调用仓储的FindAsync方法,而是调用AsNoTracking()获取IQueryable集合,之后使用EF Core的异步扩展方法FirstOrDefaultAsync查询。
普通内存集合(如List)转成的IQueryable,其QueryProvider未实现IAsyncQueryProvider接口,无法支持EF Core的异步查询操作,因此抛出异常。而测试中Mock的FindAsync方法根本没有被执行,完全无效。
解决办法
1. 实现支持异步查询的IQueryable扩展
手动编写工具类,将普通IEnumerable转成支持异步查询的IQueryable:
public static class AsyncQueryableExtensions { public static IQueryable<T> AsAsyncQueryable<T>(this IEnumerable<T> source) { return new AsyncQueryable<T>(source.AsQueryable()); } } public class AsyncQueryable<T> : EnumerableQuery<T>, IAsyncEnumerable<T>, IQueryable<T> { public AsyncQueryable(IEnumerable<T> enumerable) : base(enumerable) { } public AsyncQueryable(Expression expression) : base(expression) { } public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default) { return new AsyncEnumerator<T>(this.AsEnumerable().GetEnumerator()); } IQueryProvider IQueryable.Provider => new AsyncQueryProvider<T>(this.Provider); } public class AsyncQueryProvider<TEntity> : IAsyncQueryProvider { private readonly IQueryProvider _inner; public AsyncQueryProvider(IQueryProvider inner) { _inner = inner; } public IQueryable CreateQuery(Expression expression) { return new AsyncQueryable<object>(expression); } public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { return new AsyncQueryable<TElement>(expression); } public object Execute(Expression expression) { return _inner.Execute(expression); } public TResult Execute<TResult>(Expression expression) { return _inner.Execute<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); return (TResult)typeof(Task).GetMethod(nameof(Task.FromResult))! .MakeGenericMethod(resultType) .Invoke(null, new[] { executeMethod.Invoke(this, new[] { expression }) })!; } } public class AsyncEnumerator<T> : IAsyncEnumerator<T> { private readonly IEnumerator<T> _inner; public AsyncEnumerator(IEnumerator<T> inner) { _inner = inner; } public T Current => _inner.Current; public ValueTask DisposeAsync() { _inner.Dispose(); return ValueTask.CompletedTask; } public ValueTask<bool> MoveNextAsync() { return ValueTask.FromResult(_inner.MoveNext()); } }
2. 修改测试代码
删除对FindAsync的Mock,转而MockAsNoTracking方法,返回支持异步查询的IQueryable集合:
[Fact] public async Task FindAsyncTestAsync() { var participantList = ParticipantMockData.ListAllAsyncEntity(); // 将普通集合转为支持异步查询的IQueryable var asyncQueryable = participantList.AsAsyncQueryable(); // Mock AsNoTracking方法,返回准备好的异步查询集合 _participantRepository.Setup(_ => _.AsNoTracking(It.IsAny<bool>())) .Returns(asyncQueryable); var sut = new ParticipantService( _participantRepository.Object, _participantSupportNeed.Object, _participantConnection.Object, _participantDisability.Object, _participantMedicalCondition.Object, _supportNeed.Object, _disabilityn.Object, _medicalCodition.Object, _participantApplicationUser.Object, _appLogger.Object); var targetId = new Guid("ffbf486f-bf31-4bb4-8fc2-5beaae59e127"); var result = await sut.FindAsync(targetId); Assert.NotNull(result); Assert.Equal(participantList[0].Id, result?.Id); }
3. 可选:使用第三方库简化实现
如果不想手动编写异步查询工具类,可以安装EntityFrameworkCore.Testing.MoqNuGet包,它提供了开箱即用的Mock EF Core异步查询能力:
// 安装包后,直接构建支持异步的Mock集合 var asyncQueryable = participantList.AsQueryable().BuildMockDbSet().AsQueryable(); // Mock AsNoTracking方法 _participantRepository.Setup(_ => _.AsNoTracking(It.IsAny<bool>())).Returns(asyncQueryable);
内容的提问来源于stack exchange,提问作者hanushi-thana
相关产品推荐
相关产品推荐

