.net core 6 - 实体框架 - 何时使用 AsEnumerable AsQueryable

Mon*_*oya 3 c# entity-framework .net-core

背景

几周前,我已将项目从 .net core 2.2 迁移到 .net core 6。在过去的几周里,我发现Entity Framework过去运行良好的通话出现了问题。

问题

以下代码在 .net core 2.2 上运行良好:

using (var context = new MyContext())
{
        return await context.Accounts
                    .AsNoTracking()
                    .Where(account => SOME_CONDITIONS)
                    .ToArrayAsync()
                    .ConfigureAwait(false);
}
Run Code Online (Sandbox Code Playgroud)

我们发现现在这段代码陷入了某种死锁,函数不返回。

建议的解决方案

添加AsAsyncEnumerable修复了问题并且函数非常快地完成执行:

using (var context = new MyContext())
{
        return await context.Accounts
                    .AsNoTracking()
                    .AsAsyncEnumerable()
                    .Where(account => SOME_CONDITIONS)
                    .ToArrayAsync()
                    .ConfigureAwait(false);
}
Run Code Online (Sandbox Code Playgroud)

问题

我看到建议使用 AsEnumerable/AsAsyncEnumerable/AsQueryable 在 .net core 6 上使用 EF 进行调用。

我什么时候应该使用AsEnumerable// AsAsyncEnumerableAsQueryable

Ste*_* Py 5

从 EF Core 2.x 迁移到 3.x+ 时,您会看到围绕查询工作然后不工作的典型错误,这与 EF Core 中称为客户端评估的功能有关。其本质上是对其进行设置,以便当 EF 遇到无法分解为 SQL 的内容时(通常在子句中Where,但也可能在 中Select),它将使用此时可以分解为 SQL 的内容来执行查询,然后针对内存中的实体重新运行其余部分。

对于 EF Core 2.x,此功能默认启用。在 EF Core 3+ 中,它默认处于禁用状态。客户端评估的危险类似于延迟加载,它是一种自动防故障装置,有助于确保代码最终执行,但它可能隐藏潜在的破坏性性能问题,尤其是在没有真正生产意义的情况下开发的新系统的情况下数据加载。当您只有几百条测试记录时,速度很快,但当生产集跨越服务器尝试加载到内存中的数百万行时,就会崩溃。

因此,在您的情况下,典型的罪魁祸首将是您的Where条款条件中的某些内容。这通常类似于调用实体上的方法或使用 EF 最终无法转换为 SQL 的 C# 函数。

在该子句之前使用AsEnumerable或本质上将通过在尝试该子句之前将查询加载到内存中的实体中来执行客户端评估。显然,这里的成本是将所有实体加载到内存中。或者,您可以选择启用客户端评估并获得大致相同的行为。ToListWhereWhere

最好的解决方案是始终设法避免任何无法评估到 SQL 的内容。

其他需要考虑的解决方案:

将所有Where可以转换为 SQL 的条件Where移至AsEnumerable. 这将有助于确保为客户端评估加载最少的数据量。

例如,如果我有类似的东西:

return await context.Accounts
    .AsNoTracking()
    .Where(a => a.CreatedAt >= startDate && a.CreatedAt < endDate && someEvaluation(a))
    .ToArrayAsync()
    .ConfigureAwait(false);
Run Code Online (Sandbox Code Playgroud)

someEvaluation方法无法分解为 SQL,因此需要客户端评估。AsEnumerable在Where 子句之前使用方法将涉及将所有帐户加载到内存中。相反,我可以这样做:

return await context.Accounts
    .AsNoTracking()
    .Where(a => a.CreatedAt >= startDate && a.CreatedAt < endDate)
    .AsEnumerableAsync()
    .Where(a => someEvaluation(a))
    .ToArrayAsync()
    .ConfigureAwait(false);
Run Code Online (Sandbox Code Playgroud)

至少在这种情况下,加载到内存中的帐户将仅限于日期范围。

对于复杂的条件表达式可能没那么简单。只要有可能,您就应该努力使表达式纯粹针对实体属性和最终可归结为 SQL 的计算。