44

我正在尝试将解决方案升级到新的 Core Framework 3.0.0。现在我有一个我不明白的小问题。

看,这种方法在 2.2.6 中没有问题:

public async Task<IEnumerable<ApplicationUser>> GetBirthdayUsersCurrentMonth()
    {
        return await ApplicationDbContext.Users
            .Where(x => x.Gender != ApplicationUser.GenderTypes.generic)
            .Where(x => x.BirthDate.GetValueOrDefault().Month == DateTime.Now.Month)
            .Where(x => x.RetireDate == null)
            .OrderBy(x => x.BirthDate.GetValueOrDefault())
            .ToListAsync();
    }

现在在 3.0.0 中,我收到一个 Linq 错误:

InvalidOperationException:LINQ 表达式 'Where(source: Where(source: DbSet, predicate: (a) => (int)a.Gender != 0), predicate: (a) => a.BirthDate.GetValueOrDefault().Month == DateTime.Now.Month)' 无法翻译。以可翻译的形式重写查询,或通过插入对 AsEnumerable()、AsAsyncEnumerable()、ToList() 或 ToListAsync() 的调用显式切换到客户端评估

当我禁用此行时:

.Where(x => x.BirthDate.GetValueOrDefault().Month == DateTime.Now.Month)

错误消失了,但我当然会得到所有用户。而且我在这个查询中看不到错误。这可能是 EF Core 3.0.0 中的错误吗?

4

7 回答 7

44

原因是 EF Core 3 中禁用了隐式客户端评估。

这意味着以前,您的代码没有在服务器上执行 WHERE 子句。相反,EF 将所有行加载到内存中并计算内存中的表达式。

要在升级后解决此问题,首先,您需要弄清楚 EF 究竟是什么无法转换为 SQL。我的猜测是调用GetValueOrDefault(),因此尝试像这样重写它:

.Where(x => x.BirthDate != null && x.BirthDate.Value.Month == DateTime.Now.Month)
于 2019-09-24T07:14:12.297 回答
6

当您尝试将解决方案的 .netCore 版本升级到 3.0 时,我将在执行升级的人员范围内回答您的问题:

通过参考EF Core 3.0 重大更改官方文档,您会发现这一行

不再在客户端上评估 LINQ 查询

由于 EF 无法解释 GetValueOrDefault(),因此您的下面的查询将不再在客户端进行评估:

.Where(x => x.BirthDate.GetValueOrDefault().Month == DateTime.Now.Month)

这在 3.0 之前工作的原因是因为它在无法转换为原始 SQL 的段之前评估所有内容,然后在客户端 (c#) 端评估其余的段。这意味着您的代码被粗略评估为:

return (await ApplicationDbContext.Users
            .Where(x => x.Gender != ApplicationUser.GenderTypes.generic).ToListAsync()) //sql evaluated till here
            .Where(x => x.BirthDate.GetValueOrDefault().Month == DateTime.Now.Month)
            .Where(x => x.RetireDate == null)
            .OrderBy(x => x.BirthDate.GetValueOrDefault())
            .ToList();

这在 EF Core 3.0 中不再允许,因为隐藏客户端评估在具有较大数据集的生产中是不利的,而在开发中,可能会忽略性能影响。

您有 2 个解决方案。

首选是将受影响的行重写为类似的内容,其中 defaultMonthValue 是一个 const int,其中包含一些在 GetValueOrDefault() 扩展中使用的默认月份整数。

.Where(x => (x.BirthDate != null && x.BirthDate.Value.Month == DateTime.Now.Month) || (x.BirthDate == null && defaultMonthValue == DateTime.Now.Month))

第二个但不推荐的解决方案是在此处的问题段之前显式添加 .AsEnumerable() 以强制 EF 评估先前的语句。

.AsEnumerable() // switches to LINQ to Objects
.Where(x => x.BirthDate.GetValueOrDefault().Month == DateTime.Now.Month)

对于打算从 2.2 迁移到 3.0 并希望在实际迁移之前测试 2.2 代码库中的客户端评估重大更改的人的一些提示:

Microsoft docs开始,将以下内容添加到您的 startup.cs 以模拟 3.0 客户端查询抛出。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;")
        .ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning));
}
于 2020-04-14T14:09:12.257 回答
3

正如 Daniel Hilgarth 所写,他的解决方案很好并且有效。Wiktor Zychla 的加入似乎也有效。我重写了方法如下:

public async Task<IEnumerable<ApplicationUser>> GetBirthdayUsersCurrentMonth()
    {
        return await ApplicationDbContext.Users
            .Where(x => x.Gender != ApplicationUser.GenderTypes.generic)
            //.Where(x => x.BirthDate.GetValueOrDefault().Month == DateTime.Now.Month)
            .Where(x => x.BirthDate.Value.Month == DateTime.Now.Month)
            .Where(x => x.RetireDate == null)
            .OrderBy(x => x.BirthDate)
            .ToListAsync();
    }

因此,在 Core 3.0.0 中,如果这些是类本身提供的标准方法,那么使用上述评估方法事件并不是一个好主意。

谢谢你的帮助。

于 2019-09-24T07:35:20.837 回答
1

对于未来的读者。

我遇到了同样的问题。

通过对“内联日期计算”进行简单替换,我能够解决这个问题。

“之前”IQueryable(下)(不起作用):

(只关注“之前”中的这一部分

" perParentMaxUpdateDate.UpdateDateStamp < DateTime.Now.Add(cutOffTimeSpan) "

)

    IQueryable<MyChildEntity> filteredChildren = from chd in this.entityDbContext.MyChilds
                                                 join perParentMaxUpdateDate in justParentsAndMaxUpdateTs
                                                 on new { CompoundKey = chd.UpdateDateStamp, chd.MyParentUUID } equals new { CompoundKey = perParentMaxUpdateDate.UpdateDateStamp, perParentMaxUpdateDate.MyParentUUID }
                                                 where magicValues.Contains(chd.MyChildMagicStatus)
                                                 && perParentMaxUpdateDate.UpdateDateStamp < DateTime.Now.Add(cutOffTimeSpan) /* <- FOCUS HERE */
                                                 select chd;

“之后”IQueryable(如下)(效果很好):

    DateTime cutOffDate = DateTime.Now.Add(cutOffTimeSpan); /* do this external of the IQueryable......and use this declaration of the DateTime value ..... instead of the "inline" time-span calcuation as seen above */

    IQueryable<MyChildEntity> filteredChildren = from chd in this.entityDbContext.MyChilds
                                                 join perParentMaxUpdateDate in justParentsAndMaxUpdateTs
                                                 on new { CompoundKey = chd.UpdateDateStamp, chd.MyParentUUID } equals new { CompoundKey = perParentMaxUpdateDate.UpdateDateStamp, perParentMaxUpdateDate.MyParentUUID }
                                                 where magicValues.Contains(chd.MyChildMagicStatus)
                                                 && perParentMaxUpdateDate.UpdateDateStamp < cutOffDate /* <- FOCUS HERE */
                                                 select chd;

我猜是因为我的问题和原始问题的问题...... DateTimes 有点棘手。

因此,虽然(是的)​​以前工作的代码..停止工作很烦人......了解“默认为不在客户端上运行代码”对于性能非常重要。

PS,我是3.1

我的包参考完整性。

<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.3" />
于 2020-04-14T12:22:12.923 回答
0

就我而言,问题是使用来自DateTime的Ticks

 x.BirthDate.Ticks
于 2020-03-04T13:59:17.270 回答
0

当我升级到 efcore 3.0 时,我遇到了同样的问题。efcore 3.0 的变化是,当他无法执行查询时,他只会停下来并抛出错误。

之前版本的 efcore 的行为是相同的,但是当他没有识别参数时,他只会执行不带参数的查询(获取比你想要的更多的数据)但也许你从来没有注意到。

您可以在日志中检查。

如果要在 efcore 3.0 中执行查询,则必须在查询中使用也在 db 表中的字段。他将无法识别未映射到 de db 的对象属性

现在重新编写查询很痛苦,但过段时间就会成功

于 2019-10-30T19:59:39.013 回答
-3

如果你的列表是IQueryable在哪里,你需要使用Tolist()。

list.ToList().Where(p => (DateTime.Now - p.CreateDate).Days <= datetime).AsQueryable();
于 2020-10-11T06:47:25.370 回答