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

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()这一行:

  1. _context.Persons本身就是EF Core的DbSet<Person>,已经实现了IQueryable<Person>,原生支持EF的异步查询方法。
  2. 调用AsEnumerable()会把EF的查询转换成内存枚举集合,再调用AsQueryable()得到的是EnumerableQuery<Person>(属于LINQ to Objects的IQueryable实现),而非EF原生的DbQuery。
  3. 未添加where子句时,这个EnumerableQuery调用ToListAsync(cancellationToken)时,内部异步实现未正确处理取消令牌,再加上Mock的DbSet没覆盖这个场景,最终导致空引用异常。
  4. 添加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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 14:59:55