【问题标题】:How to modify expression-based filters to avoid client-side evaluation in Entity Framework Core 3.0如何修改基于表达式的过滤器以避免在 Entity Framework Core 3.0 中进行客户端评估
【发布时间】:2020-05-11 22:28:44
【问题描述】:

我有以下代码用于将基于 Func 的过滤器转换为 Expression 并过滤 Entity Framework Core 2.2 中的数据:

public async Task<TType> GetDataAsync<TType>(Func<TType, bool> filtering = null) where TType : class
{
  Expression<Func<TType, bool>> filteringExpression = (type) => filtering(type);
  if (filtering != null)
    //return await myContext.Set<TType>().FirstOrDefaultAsync(filteringExpression);
    return await myContext.Set<TType>().Where(filteringExpression ).FirstOrDefaultAsync();
  return await myContext.Set<TType>().FirstOrDefaultAsync();
}

我就是这样使用它的:

public async Task<DataLog> GetDataLogByID(Guid dataLogID) => await GetDataAsync<DataLog>(dataLog => dataLog.ID == dataLogID);

(Un)幸运的是,当我升级到 Entity Framework Core 3.0 时,代码抛出了 InvalidOperationException,因为表达式无法转换为 SQL 查询(尽管它只过滤了一个属性匹配数据库列):

System.InvalidOperationException: 'LINQ 表达式 '在哪里( 来源:数据库集, 谓词:(f) => Invoke(__filtering_0, f[DataLog]) )' 无法翻译。要么重写查询形式, 翻译,或通过插入显式切换到客户端评估 调用 AsEnumerable()、AsAsyncEnumerable()、ToList() 或 ToListAsync()。见https://go.microsoft.com/fwlink/?linkid=2101038 更多信息。

那么你能告诉我,我应该如何修改代码以确保所有(大部分)处理都留在服务器端?保持通用代码符合标准的最佳做法是什么?

【问题讨论】:

  • 你的过滤表达式可以是任何东西,因为你已经指定了一个委托,所以这取决于你真的扔给它 - 你有传递给 GetDataAsync 方法的委托的例子吗?正如您所说,如果它只是一个简单的属性过滤器,那么我看不出为什么 3.0 无法翻译它。我想说这在 2.2 中起作用的唯一原因是,当无法翻译查询时,2.2 会自动切换到客户端评估。您是否检查过 2.2 确实在服务器上执行? 3 现在抛出异常以防止意外后果/性能。
  • @Charleh 我修改了最初的帖子以展示我如何使用它。我检查了一下,在 2.2 中它有很多这样的警告:“[WRN] LINQ 表达式'Skip(__p_2)' 无法翻译,将在本地进行评估。” 这是一个 SQLite数据库,但我认为,它支持这类查询。
  • 使用Expression&lt;Func&lt;TType, bool&gt;&gt; filtering 代替Func&lt;TType, bool&gt; filtering。一般遵循Queryable方法模式,即使用Expression&lt;Func&lt;…&gt;&gt;而不是Func&lt;…&gt;——前者可以翻译,后者不能。
  • @IvanStoev 将参数转换为Expression 在这种特殊情况下有所帮助,因此我尝试相应地修改数据处理程序方法。如果您宣传您的评论以回答,我会接受它。
  • 发现了同样的问题,我打算建议 - 如果你使用 Expression&lt;Func&lt;&gt;&gt; 而不是 Func&lt;&gt; 它可以工作。经验。您通过将Func 包装在另一个表达式中来构建的树实际上创建了一个原始委托链 - 即 LINQ 无法破译它,因为它不知道如何翻译调用。您可以在原始 lambda 中看到:t =&gt; Invoke(value(ConsoleApp1.Program+&lt;&gt;c__DisplayClass2_01[ConsoleApp1.GuidTable]).filtering, t) 与将表达式作为参数时得到的结果 t =&gt; (t.Guid == value(ConsoleApp1.Program+&lt;&gt;c__DisplayClass1_0).g)

标签: c# entity-framework-core-3.0


【解决方案1】:

恭喜,您发现了 EF Core 3.0 中的一项重大更改 - LINQ queries are no longer evaluated on the client

旧行为

在 3.0 之前,当 EF Core 无法将作为查询的一部分的表达式转换为 SQL 或参数时,它会自动评估客户端上的表达式。默认情况下,客户端对可能昂贵的表达式的评估只会触发警告。

新行为

从 3.0 开始,EF Core 仅允许在客户端评估顶级投影(查询中的最后一个 Select() 调用)中的表达式。当查询的任何其他部分中的表达式无法转换为 SQL 或参数时,将引发异常。

有关详细信息,请参阅文档(上面的链接),但是您在升级之前遇到的警告现在会生成 InvalidOperationExceptions 并且与 SQLite 无关,您会遇到与 SQL Server 相同的问题。

解决此问题的唯一方法是确保您的过滤表达式/函数可以转换为适当的 SQL...或恢复为 EF Core

更新

您可以尝试包装传递的 Func 并将参数类型更改为 Expression&lt;Func&lt;TType, bool&gt;&gt;(它应该不需要对调用该方法的代码进行任何更改)

public async Task<TType> GetDataAsync<TType>(Expression<Func<TType, bool>> filter = null)
    where TType : class
{
    var query = myContext.Set<TType>();

    if (filter != null)
        query = query.Where(filter);

    return await query.FirstOrDefaultAsync();
}

刚刚注意到对GetDataAsync 的调用似乎不正确,并且有一个额外的类型参数Guid 应该从本示例中删除。

public async Task<DataLog> GetDataLogByID(Guid dataLogID) =>
    await GetDataAsync<DataLog>(dataLog => dataLog.ID == dataLogID);

【讨论】:

  • 谢谢,我知道我遇到问题的原因,但我需要提示我当前代码的更改位置和内容。 替换参数是解决问题的关键。
  • 我现在只是想看看您的表达式是否正在被评估,或者它是否是评估后的问题。
  • 有一个额外的类型参数是什么意思?我使用该参数来过滤数据,因此它必须在该方法的签名中。
  • @Nestor GetDataAsync(Func filtering = null)` 只有 1 个泛型类型参数 TType 但你用 2 个 await GetDataAsync&lt;DataLog, Guid&gt;(dat... 调用它,这个似乎不对。 GetDataAsync 不需要知道更多关于传递的 Func/Expression 的内容
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多