EF Core传入CancellationToken调用ToListAsync时抛出空引用异常
问题
调用ToListAsync并传入CancellationToken时会抛出空引用异常,但仅当IQueryable未添加where子句时才会触发。若不传取消令牌或传入CancellationToken.None,则运行正常。
PersonService 代码
public class PersonService { private readonly MyDbContext _context; public PersonService(MyDbContext context) { _context = context; } public async Task<IEnumerable<Person>> GetPersonsInList(List<int> ids, CancellationToken cancellationToken) { var persons = _context.Persons.AsEnumerable().AsQueryable(); // 添加下面这行就正常了,搞不懂为啥没where子句会报错 //persons = persons.Where(x => x.Id > 0); if (false) { persons = persons.Where(x => ids.Contains(x.Id)); } var personsResult = await persons.ToListAsync(cancellationToken: cancellationToken); return personsResult; } } public class Person { [Key] public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } public class MyDbContext : DbContext { public virtual DbSet<Person> Persons { get; set; } }
单元测试代码
public class UnitTest1 { [Fact] public async Task PersonService_GetPersonsInList_ReturnsList() { // 准备测试数据 Mock<MyDbContext> mockContext = new(); IList<Person> persons = new List<Person> { new() { Id = 1, FirstName = "John", LastName = "Doe" }, new() { Id = 2, FirstName = "Jane", LastName = "Doe" } }; mockContext.SetupGet(x => x.Persons).ReturnsDbSet(persons); PersonService personService = new(mockContext.Object); CancellationTokenSource cancellationTokenSource = new(); CancellationToken cancellationToken = cancellationTokenSource.Token; // 执行测试方法 var personsResult = await personService.GetPersonsInList(new List<int> { 1, 2 }, cancellationToken); // 断言结果 Assert.Equal(persons.Count, personsResult.Count()); } }
原因分析
问题根源在_context.Persons.AsEnumerable().AsQueryable()这一行:
_context.Persons本身就是EF Core的DbSet<Person>,已经实现了IQueryable<Person>,原生支持EF的异步查询方法。- 调用
AsEnumerable()会把EF的查询转换成内存枚举集合,再调用AsQueryable()得到的是EnumerableQuery<Person>(属于LINQ to Objects的IQueryable实现),而非EF原生的DbQuery。 - 未添加
where子句时,这个EnumerableQuery调用ToListAsync(cancellationToken)时,内部异步实现未正确处理取消令牌,再加上Mock的DbSet没覆盖这个场景,最终导致空引用异常。 - 添加
where子句后,会生成新的IQueryable包装,间接修复了调用链的问题。
解决方案
直接去掉多余的AsEnumerable().AsQueryable(),直接使用_context.Persons本身的IQueryable即可:
public async Task<IEnumerable<Person>> GetPersonsInList(List<int> ids, CancellationToken cancellationToken) { var persons = _context.Persons; // 直接用EF原生DbSet,本身就是IQueryable if (false) { persons = persons.Where(x => ids.Contains(x.Id)); } var personsResult = await persons.ToListAsync(cancellationToken: cancellationToken); return personsResult; }
如果确实需要切换到内存查询场景,也可以手动处理异步逻辑:
// 用Task.Run包裹内存查询并传入取消令牌 var personsResult = await Task.Run(() => persons.ToList(), cancellationToken);
内容的提问来源于stack exchange,提问作者Herrozerro
相关产品推荐
相关产品推荐

