如何使用 GetAsyncEnumerator 中止正在运行的 EF Core 查询?

Jan*_*Jan 6 c# entity-framework-core

我正在使用 EF Core 5.0 并有以下代码:

public async IAsyncEnumerable<Item> GetItems([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    await using var ctx = _DbContextFunc();
    //Isolationlevel is required to not cause any issues with parallel working on already read items
    await ctx.Database.BeginTransactionAsync(IsolationLevel.ReadUncommitted, cancellationToken).ConfigureAwait(false);
    await foreach (var item in ctx.Item.
        .AsSplitQuery()
        .Include(i => i.ItemDetail1)
        .Include(i => i.ItemDetail2)
        .OrderByDescending(i => i.ItemId)
        .AsNoTracking()
        .AsAsyncEnumerable()
        .WithCancellation(cancellationToken))
    {
        yield return item;
    }
}
Run Code Online (Sandbox Code Playgroud)

它按预期工作,允许我在仍在加载更多数据的同时填充数据网格。如果我取消提供的 CancelationToken,首先我会在 MoveNextAsync() 行上收到预期的 TaskCanceledException。

但是:我可以在 SQL Profiler 中看到 SQL 查询本身不会中止,而是始终运行,直到加载所有数据,然后我才能在同一行上收到第二个 TaskCanceledException。

如何中止查询本身?

更新

我将 AsSplitQuery() 添加到示例中,因为事实证明它是我所经历的行为的原因(正如 Ivan 正确猜测的那样)。为了使样本更短而省略了它......

Iva*_*oev 6

所描述的行为是特定于某些类型的查询的 EF Core 实现,这些查询在内部使用所谓的缓冲数据读取器-DbDataReader该实现最初完全消耗底层数据读取器并缓冲结果,因此可以更早地释放底层数据读取器。

很难确切地说哪种类型的查询“按设计”使用它,但可以肯定的是,当禁用/不支持多个活动结果集 (MARS)时,分割查询会使用它。

为什么?拆分查询执行多个数据库查询并合并其结果,就好像它们是单个查询一样。整合需要多个数据读取器同时处于活动状态。当底层数据库提供程序不支持 MARS 或禁用 MARS(默认情况下)时,在存在活动读取器的情况下尝试执行第二个读取器会导致运行时异常。因此,为了解决这个问题,EF Core 必须消耗并缓冲活动读取器,并在执行下一个读取器之前释放(关闭/处置)它。

由于这是使该功能正常工作所必需的,因此没有外部方法可以控制它。除非数据库提供程序支持 MARS(例如,SqlServer 支持),在这种情况下,您可以通过添加在连接字符串中启用它

MultipleActiveResultSets=True
Run Code Online (Sandbox Code Playgroud)

分割查询将直接使用底层数据读取器,因此可以在不完全消耗它们的情况下提前取消。