作为一般规则,Entity Framework Core 会尝试尽可能多地评估服务器上的查询。 EF Core 将查询的各个部分转换为可在客户端上计算的参数。 向数据库提供程序提供查询的其余部分(以及生成的参数),以确定在服务器上评估的等效数据库查询。 EF Core 支持在顶层投影(实质上是最后一次调用 Select()
)中的部分客户端评估。 如果查询中的顶级投影无法在服务器上执行,EF Core 将从服务器获取所需的数据,并在客户端上评估查询的剩余部分。 如果 EF Core 检测到表达式,位于顶级投影以外的任何位置(无法转换为服务器),则会引发运行时异常。 请参阅 查询的工作原理 ,以了解 EF Core 如何确定无法转换为服务器的内容。
注释
在版本 3.0 之前,Entity Framework Core 支持在查询中的任何位置进行客户端评估。 有关详细信息,请参阅 以前的版本部分。
小窍门
可以在 GitHub 上查看本文 的示例 。
顶层规划中的客户评估
在以下示例中,帮助程序方法用于标准化博客的 URL,这些 URL 是从 SQL Server 数据库返回的。 由于 SQL Server 提供程序无法深入了解此方法的实现方式,因此无法将其转换为 SQL。 查询的所有其他方面都在数据库中进行评估,但通过此方法传递返回 URL
的内容是在客户端上完成的。
var blogs = await context.Blogs
.OrderByDescending(blog => blog.Rating)
.Select(
blog => new { Id = blog.BlogId, Url = StandardizeUrl(blog.Url) })
.ToListAsync();
public static string StandardizeUrl(string url)
{
url = url.ToLower();
if (!url.StartsWith("http://"))
{
url = string.Concat("http://", url);
}
return url;
}
不支持的客户端评估
虽然客户端评估很有用,但有时可能会导致性能不佳。 请考虑以下查询,其中帮助程序方法现在用于where筛选器中。 由于无法在数据库中应用筛选器,因此所有数据都需要提取到内存中,才能在客户端上应用筛选器。 根据服务器上的筛选器和数据量,客户端评估可能会导致性能不佳。 因此 Entity Framework Core 会阻止此类客户端评估并引发运行时异常。
var blogs = await context.Blogs
.Where(blog => StandardizeUrl(blog.Url).Contains("dotnet"))
.ToListAsync();
显式客户端评估
在某些情况下,可能需要明确强制进行客户端评估,例如如下所示
- 数据量很小,因此在客户端上进行评估不会造成巨大的性能损失。
- 正在使用的 LINQ 运算符没有服务器端翻译。
在这种情况下,可以通过显式地调用方法,比如AsEnumerable
或 ToList
(异步情况下,调用AsAsyncEnumerable
或 ToListAsync
),来选择加入客户端评估。 通过使用 AsEnumerable
,你将流式传输结果;而使用 ToList
时,会因为创建列表而导致缓冲,同时它也需要占用额外的内存。 不过,如果要重复多次枚举,那么将结果存储到列表中会更有帮助,因为这样只需要对数据库进行一次查询。 根据特定用法,应评估哪种方法对事例更有用。
var blogs = context.Blogs
.AsAsyncEnumerable()
.Where(blog => StandardizeUrl(blog.Url).Contains("dotnet"))
.ToListAsync();
小窍门
如果使用 AsAsyncEnumerable
并想要在客户端进一步撰写查询,则可以使用 System.Interactive.Async 库来定义异步可枚举的运算符。 有关详细信息,请参阅 客户端 linq 运算符。
客户端评估中潜在的内存泄漏
由于查询转换和编译成本高昂,EF Core 会缓存已编译的查询计划。 缓存委托在执行顶级投影的客户端评估时,可以使用客户端代码。 EF Core 为树的客户端评估部分生成参数,并通过替换参数值来重用查询计划。 但是表达式树中的某些常量无法转换为参数。 如果缓存的委托包含这些常量,那么这些对象无法被垃圾回收,因为它们仍然处于被引用状态。 如果此类对象包含 DbContext 或其他服务,则可能会导致应用内存使用量随时间推移而增长。 此行为通常是内存泄漏的标志。 每当遇到无法使用当前数据库提供程序映射的类型常量时,EF Core 都会引发异常。 常见原因及其解决方案如下所示:
- 使用实例方法:在客户端投影中使用实例方法时,表达式树包含实例的常量。 如果方法不使用实例中的任何数据,请考虑将该方法静态化。 如果需要方法正文中的实例数据,请将特定数据作为参数传递给该方法。
- 将常量参数传递给方法:这种情况通常发生在将常量作为参数传递给客户端方法时。 请考虑将参数拆分为多个标量参数,该参数可由数据库提供程序映射。
- 其他常量:如果在任何其他情况下遇到常量,则可以评估处理中是否需要常量。 如果需要具有常量,或者不能使用上述情况中的解决方案,请创建一个局部变量来存储该值并在查询中使用局部变量。 EF Core 会将本地变量转换为参数。
以前的版本
以下部分适用于 3.0 之前的 EF Core 版本。
以往的 EF Core 版本支持在查询的任何部分进行客户端评估,而不仅限于顶层的查询结果投影。 这就是为什么类似于在 “不支持的客户端评估 ”部分下发布的查询正常工作的原因。 由于此行为可能会导致意外的性能问题,EF Core 记录了客户端评估警告。 有关查看日志记录输出的详细信息,请参阅 日志记录。
可选地,EF Core 允许你更改默认行为,以便在执行客户端评估(除了投影之外)时抛出异常或什么都不做。 引发异常的行为将使其更类似于 3.0 版本中的行为。 若要更改行为,需要在设置上下文选项时配置警告,通常在 DbContext.OnConfiguring
中,或者如果您使用 Startup.cs
ASP.NET Core。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;")
.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning));
}