顶级 EF Core 投影中的客户端评估

Client evaluation in top-level EF Core projection

提问人:lonix 提问时间:11/12/2023 更新时间:11/13/2023 访问量:43

问:

假设我执行查询并投影到匿名类型中;这将在服务器上执行评估:

var people = await context
  .People
  .OrderBy(x => x.Name)
  .Select(x => new { x.Id, x.Surname, x.Forename })
  .ToListAsync();

但是假设我执行一个查询,并且需要投影到具体类型中。

选项 1:将结果映射到具体类型:

var people = (await context
  .People
  .OrderBy(x => x.Name)
  .Select(x => new { x.Id, x.Surname, x.Forename })
  .ToListAsync())
  .Select(x => new PersonDto(x.Id, x.Surname, x.Forename));

选项 2:在“顶级投影”中使用具体类型:

var people = await context
  .People
  .OrderBy(x => x.Name)
  .Select(x => new PersonDto(x.Id, x.Surname, x.Forename));
  .ToListAsync()

关于(2),文档指出,在顶级投影中,一些评估在数据库中执行,一些在客户端上执行。(但是文档显示了匿名类型的客户端函数示例,因此它与我的用例不同。

在这个具体的例子中,我假设所有评估都是在服务器上执行的,并且 (2) 只是 (1) 的捷径,即它们在功能上是等效的,这是否正确?

C# LINQ 实体框架核心 ef-core-7.0

评论

0赞 Dai 11/12/2023
我不确定 EFCore 7,但自从 2008 年的 EF1.0 以来,查询转换器从不支持对结果类型使用非默认构造函数:您总是必须发布对象初始值设定项列表或匿名类型,但它确实支持服务器上的查询投影,因此它只对对象初始值设定项列表匿名类型拉入的实际数据执行。如果您发现选项 2 有效(即您的实际查询是最佳的),那么我想 EFCore7 现在终于支持 ctor(终于!SELECTSELECT
0赞 Dai 11/12/2023
更新:呜呜!看起来 EFCore 确实支持在查询投影中使用 ctor:learn.microsoft.com/en-us/ef/core/modeling/constructors
0赞 lonix 11/12/2023
@Dai我认为这是在将获取的数据绑定到对象时(他们添加了它以使类似 DDD 的代码更方便,其中有一个用于 EF 的私有 ctor,以及用于客户端代码的参数化 ctor,它强制执行验证)。但我想知道这是否也适用于在投影中使用 ctor?
0赞 Dai 11/12/2023
那么,您是否尝试过针对真实数据库的示例查询并查看它生成的 SQL?
1赞 Svyatoslav Danyliv 11/12/2023
是的,你是对的。在顶级投影中,EF 正在尝试将所有内容转换为 SQL(参数、赋值)。如果不可能,则可以在客户端评估部分投影。匿名类和 DTO 类之间没有区别。所以(2)变体是正确的选择。

答:

1赞 dani herrera 11/12/2023 #1

问题:

所有评估都在服务器上执行,而 (2) 只是 (1) 的捷径,即它们在功能上是等效的?

显然,该对象是在客户端实例化的,但是,EF 能够从数据库中获取三个构造函数参数值。只有这 3 个值 ( ):PersonDtox.Id, x.Surname, x.ForenameSelect id, surname, forename from people order by name

EF Core 支持在顶级投影中进行部分客户端评估(实质上是对 Select() 的最后一次调用)。如果查询中的顶级投影无法转换为服务器,EF Core 将从服务器提取任何所需的数据,并在客户端上评估查询的其余部分。如果 EF Core 在顶级投影以外的任何位置检测到无法转换为服务器的表达式,则会引发运行时异常

文档中的完整解释。https://learn.microsoft.com/en-us/ef/core/querying/client-eval

测试

下面是 EF 将顶级投影转换为 SQL 的示例。表达:

var result = 
    dbcontext.
    Customers.
    Select(x => new PersonName2CharsDto(x.Name.Substring(0,2))).
    ToList();

生成的 sql(用于 sqlite):

SELECT substr("c"."Name", 0 + 1, 2)
FROM "Customers" AS "c"