EF6异步方法单元测试报错,请求解析相关测试代码
EF6异步单元测试错误解析及代码说明
问题背景
在EF6编写异步方法的单元测试时,遇到错误:the source 'iqueryable' doesn't implement 'iasyncenumerable moq'。这是因为Moq默认模拟的IQueryable只实现了同步查询接口,而EF的异步方法(如ToListAsync、FirstOrDefaultAsync)依赖IAsyncEnumerable接口,所以需要手动实现一套支持异步查询的模拟类。
1. TestAsyncQueryProvider:异步查询提供者
这个类实现了IAsyncQueryProvider接口,是EF异步查询的核心调度类,负责创建查询对象和执行异步操作:
internal class TestAsyncQueryProvider<TEntity> : IAsyncQueryProvider { private readonly IQueryProvider _inner; internal 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); } // 同步执行查询(复用内部IQueryProvider的逻辑) 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); } // 异步执行查询,将同步结果包装成Task返回 public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken) { return Task.FromResult(Execute<TResult>(expression)); } }
关键逻辑说明:
- 构造函数接收一个普通的
IQueryProvider(比如来自内存集合的查询提供者),复用它的同步执行逻辑。 CreateQuery方法返回自定义的TestAsyncEnumerable,把普通查询转换成支持异步的查询对象。ExecuteAsync方法有两个重载:一个返回IAsyncEnumerable用于异步遍历,另一个把同步执行的结果包装成Task,满足EF异步方法的调用要求。
2. TestAsyncEnumerable:异步可枚举集合
这个类继承自EnumerableQuery<T>(内存集合的查询实现),同时实现IAsyncEnumerable<T>和IQueryable<T>,把普通的内存集合转换成支持异步查询的集合:
internal class TestAsyncEnumerable<T> : EnumerableQuery<T>, IAsyncEnumerable<T>, IQueryable<T> { // 从普通IEnumerable创建异步集合 public TestAsyncEnumerable(IEnumerable<T> enumerable) : base(enumerable) { } // 从表达式树创建异步集合(支持LINQ表达式解析) public TestAsyncEnumerable(Expression expression) : base(expression) { } // 返回异步枚举器,用于异步遍历集合 public IAsyncEnumerator<T> GetEnumerator() { return new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator()); } // 重写Provider属性,返回自定义的异步查询提供者 IQueryProvider IQueryable.Provider { get { return new TestAsyncQueryProvider<T>(this); } } }
关键逻辑说明:
- 两个构造函数分别支持从普通集合或LINQ表达式树创建异步集合,兼容不同的测试场景。
GetEnumerator方法返回自定义的TestAsyncEnumerator,把普通的同步枚举器包装成异步枚举器。- 显式实现
IQueryable.Provider,替换成TestAsyncQueryProvider,确保所有查询操作都通过异步提供者调度。
3. TestAsyncEnumerator:异步枚举器
这个类实现IAsyncEnumerator<T>接口,包装普通的IEnumerator<T>,把同步的遍历操作转换成异步操作:
internal class TestAsyncEnumerator<T> : IAsyncEnumerator<T> { private readonly IEnumerator<T> _inner; public TestAsyncEnumerator(IEnumerator<T> inner) { _inner = inner; } // 释放内部枚举器资源 public void Dispose() { _inner.Dispose(); } // 当前元素(复用内部枚举器的Current属性) public T Current { get { return _inner.Current; } } // 异步移动到下一个元素,把同步的MoveNext结果包装成Task public Task<bool> MoveNext(CancellationToken cancellationToken) { return Task.FromResult(_inner.MoveNext()); } }
关键逻辑说明:
- 构造函数接收普通的同步枚举器,所有操作都委托给它执行。
MoveNext方法把同步的MoveNext()调用包装成Task<bool>,满足异步枚举的接口要求。- 完全复用内部枚举器的
Dispose和Current逻辑,避免重复实现。
如何使用这些类?
在单元测试中,你可以把内存集合转换成TestAsyncEnumerable,再赋值给Moq模拟的DbSet:
var data = new List<YourEntity> { /* 测试数据 */ }.AsQueryable(); var mockSet = new Mock<DbSet<YourEntity>>(); // 把普通IQueryable转换成异步支持的TestAsyncEnumerable mockSet.As<IAsyncEnumerable<YourEntity>>() .Setup(m => m.GetEnumerator()) .Returns(new TestAsyncEnumerator<YourEntity>(data.GetEnumerator())); mockSet.As<IQueryable<YourEntity>>() .Setup(m => m.Provider) .Returns(new TestAsyncQueryProvider<YourEntity>(data.Provider)); mockSet.As<IQueryable<YourEntity>>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As<IQueryable<YourEntity>>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As<IQueryable<YourEntity>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
这样模拟的DbSet就支持EF的异步方法调用了,不会再出现IAsyncEnumerable相关的错误。
内容的提问来源于stack exchange,提问作者Peter Nguyen
相关产品推荐
相关产品推荐

