using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace AdvancedApp.Controllers
public class HomeController : Controller
private AdvancedContext context;
public HomeController(AdvancedContext ctx) => context = ctx;
public IActionResult Index()
return View(context.Employees.AsNoTracking());
public IActionResult Edit(string SSN, string firstName, string familyName)
return View(string.IsNullOrWhiteSpace(SSN)
? new Employee() : context.Employees.Include(e => e.OtherIdentity)
.AsNoTracking()
.First(e => e.SSN == SSN
&& e.FirstName == firstName
&& e.FamilyName == familyName));
[HttpPost]
public IActionResult Update(Employee employee)
if (context.Employees.Count(e => e.SSN == employee.SSN
&& e.FirstName == employee.FirstName
&& e.FamilyName == employee.FamilyName) == 0)
context.Add(employee);
context.Update(employee);
context.SaveChanges();
return RedirectToAction(nameof(Index));
我在Index
和Edit
action 方法中向查询添加了AsNotTracking
方法。AsTracking
和AsNotTracking
方法应用于IQueryable<T>
对象,这意味着它们必须包含在创建查询的方法链中,比如将结果缩小为单个对象的First
方法。
禁用更改跟踪没有明显的效果,但 Entity Framework Core 不再设置用于只读操作的对象的跟踪。
从更改跟踪中删除单个对象
在 ASP.NET Core MVC 应用程序中使用 MVC 模型绑定器创建的对象执行更新时,会出现一个常见的问题,该对象与使用更改跟踪的 Entity Framework Core 查询加载的对象具有相同的主键。为了演示这个问题,我修改了 Home 控制器中的Update
方法,如清单20-4所示。
清单 20-4:Controllers 文件夹下的 HomeController.cs 文件,混合对象
[HttpPost]
public IActionResult Update(Employee employee)
if (context.Employees.Find(employee.SSN, employee.FirstName,
employee.FamilyName) == null)
context.Add(employee);
context.Update(employee);
context.SaveChanges();
return RedirectToAction(nameof(Index));
我已在清单中更改了查询,以便它使用Find
方法来确定是否已使用键。这是一个人为的问题,因为原始代码显示将 lambda 表达式传递给 LINQ Count
方法是有效的,但这是一个非常常见的问题,即使您已经看到了一种避免它的技术,也值得演示这个问题。
使用dotnet run
启动应用程序,并导航至 http://localhost:5000。单击某个雇员的【Edit】按钮,然后单击【Save】按钮;您将看到图20-2所示的错误。
图20-2 更改跟踪功能导致的异常
错误信息报告只能跟踪一个具有特定键的对象。由于 Entity Framework Core 已将作为Find
方法的结果创建的Employee
对象置于更改跟踪中,因此出现了此问题,并且该对象与 MVC 模型绑定器创建的并传递给Update
方法的Employee
对象具有相同的主键。
避免此问题的最简单方法是使用不受更改跟踪影响的简单结果的查询,例如,清单中依赖于 LINQ Count
方法的原始代码。Entity Framework Core 只对实体对象执行更改跟踪,因此任何产生非实体结果(如int
值)的查询都不受更改跟踪的影响。您还可以使用前面一节中描述的AsNoTracking
方法,该方法将排除由来自更改跟踪的查询创建的所有对象。
如果这两种方法都不合适,则可以显式地从更改跟踪中删除对象。这并不能避免 Entity Framework Core 为跟踪对象而执行的工作,但它确实防止 Entity Framework Core 抛出异常。
在清单20-5中,我修改了Update
方法,以便从更改跟踪中删除 Entity Framework Core 创建的Employee
对象,这样它就不会与 MVC模型绑定器 创建的Employee
对象发生冲突。
清单 20-5:Controllers 文件夹下的 HomeController.cs 文件,在跟踪中移除对象
[HttpPost]
public IActionResult Update(Employee employee)
Employee existing = context.Employees.Find(employee.SSN,
employee.FirstName, employee.FamilyName);
if (existing == null)
context.Add(employee);
context.Entry(existing).State = EntityState.Detached;
context.Update(employee);
context.SaveChanges();
return RedirectToAction(nameof(Index));
被跟踪的对象被传递到 context 对象的Entry
方法,并且State
属性被分配给EntityState.Detached
值。其结果是 Entity Framework Core 从更改跟踪中删除对象,这意味着它不再与对象冲突,其主键与 MVC 模型绑定器从 HTTP 请求创建的主键相同。
更改默认更改跟踪行为
如果大多数查询没有修改对象,那么可以更简单地禁用 context 对象所做的所有查询的跟踪,并使用AsTracking
方法来针对那些需要它的查询进行启用。
在清单 20-6 中,我对AdvancedContext
类的所有查询禁用了跟踪。示例应用程序中只有一个 context,但是清单中的更改不会影响其他 context,每个 context 必须以相同的方式配置。
清单 20-6:Models 文件夹下的 AdvancedContext.cs 文件,禁用更改跟踪
using Microsoft.EntityFrameworkCore;
namespace AdvancedApp.Models
public class AdvancedContext : DbContext
public AdvancedContext(DbContextOptions<AdvancedContext> options)
: base(options)
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
public DbSet<Employee> Employees { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity<Employee>().Ignore(e => e.Id);
modelBuilder.Entity<Employee>()
.HasKey(e => new { e.SSN, e.FirstName, e.FamilyName });
modelBuilder.Entity<SecondaryIdentity>()
.HasOne(s => s.PrimaryIdentity)
.WithOne(e => e.OtherIdentity)
.HasPrincipalKey<Employee>(e => new {
e.SSN,
e.FirstName,
e.FamilyName
.HasForeignKey<SecondaryIdentity>(s => new {
s.PrimarySSN,
s.PrimaryFirstName,
s.PrimaryFamilyName
ChangeTracker
属性由DbContext
类定义,它返回一个ChangeTracker
对象,该对象的QueryTrackingBehavior
属性是使用同名枚举配置的。表20-5显示了QueryTrackingBehavior
枚举的值。
表 20-5:QueryTrackingBehavior 值
如果默认禁用更改跟踪,则必须在依赖跟踪以检测更改的任何查询中使用AsTracking
方法。在清单20-7中,我修改了 Home 控制器的Update
方法中的查询,以便将Salary
属性的值应用于从数据库读取的Employee
对象,只有在允许 Entity Framework Core 使用更改跟踪来检测修改后的值时,才能工作。
清单 20-7:Controllers 文件夹下的 HomeController.cs 文件,启用跟踪
[HttpPost]
public IActionResult Update(Employee employee)
Employee existing = context.Employees
.AsTracking()
.First(e => e.SSN == employee.SSN && e.FirstName == employee.FirstName
&& e.FamilyName == employee.FamilyName);
if (existing == null)
context.Add(employee);
existing.Salary = employee.Salary;
context.SaveChanges();
return RedirectToAction(nameof(Index));
没有AsTracking
方法,Entity Framework Core 将无法检测更改,也不能更新数据库。
使用查询过滤器
查询过滤器应用于应用程序中针对特定实体类的所有查询。查询筛选器的一个有用的应用是实现“软删除”功能,它标记被删除的对象而不将其从数据库中删除,如果数据被错误删除,则允许还原数据。
注意:我在第22章描述了高级特性 —— 真删除数据
作为准备,我向Employee
类添加了一个属性,该属性将指示存储在数据库中的对象何时被用户软删除,如清单20-8所示。
清单 20-8:Models 文件夹下的 Employee.cs 文件,添加属性
namespace AdvancedApp.Models
public class Employee
public long Id { get; set; }
public string SSN { get; set; }
public string FirstName { get; set; }
public string FamilyName { get; set; }
public decimal Salary { get; set; }
public SecondaryIdentity OtherIdentity { get; set; }
public bool SoftDeleted { get; set; } = false;
下一步是添加一个查询过滤器,将软删除的雇员对象排除在查询结果外,如清单20-9所示。我还注释掉了禁用更改跟踪代码以保持示例尽可能简单。
清单 20-9:Models 文件夹下的 AdvancedContext.cs 文件,定义查询过滤器
using Microsoft.EntityFrameworkCore;
namespace AdvancedApp.Models
public class AdvancedContext : DbContext
public AdvancedContext(DbContextOptions<AdvancedContext> options)
: base(options)
//ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
public DbSet<Employee> Employees { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity<Employee>()
.HasQueryFilter(e => !e.SoftDeleted);
modelBuilder.Entity<Employee>().Ignore(e => e.Id);
modelBuilder.Entity<Employee>()
.HasKey(e => new { e.SSN, e.FirstName, e.FamilyName });
modelBuilder.Entity<SecondaryIdentity>()
.HasOne(s => s.PrimaryIdentity)
.WithOne(e => e.OtherIdentity)
.HasPrincipalKey<Employee>(e => new {
e.SSN,
e.FirstName,
e.FamilyName
.HasForeignKey<SecondaryIdentity>(s => new {
s.PrimarySSN,
s.PrimaryFirstName,
s.PrimaryFamilyName
查询过滤器是通过Entity
方法选择类,然后调用HasQueryFilter
方法来创建的。过滤器应用于对所选类的所有查询,只有 lambda 表达式返回true
的对象才会包含在查询结果中。在清单中,我定义了一个查询过滤器,它选择SoftDeleted
值为false
的Employee
对象。
要实现软删除功能,我更新了Home
控制器,如清单20-10所示。我添加了Delete
action 用于设置Employee
对象的SoftDeleted
属性为true
,它将确保软删除对象不会被查询过滤器排除。
清单 20-10:Controllers 文件夹下的 HomeController.cs 文件,支持软删除
using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace AdvancedApp.Controllers
public class HomeController : Controller
private AdvancedContext context;
public HomeController(AdvancedContext ctx) => context = ctx;
public IActionResult Index()
return View(context.Employees.AsNoTracking());
public IActionResult Edit(string SSN, string firstName, string familyName)
return View(string.IsNullOrWhiteSpace(SSN)
? new Employee() : context.Employees.Include(e => e.OtherIdentity)
.AsNoTracking()
.First(e => e.SSN == SSN
&& e.FirstName == firstName
&& e.FamilyName == familyName));
[HttpPost]
public IActionResult Update(Employee employee)
Employee existing = context.Employees
.AsTracking()
.First(e => e.SSN == employee.SSN && e.FirstName == employee.FirstName
&& e.FamilyName == employee.FamilyName);
if (existing == null)
context.Add(employee);
existing.Salary = employee.Salary;
context.SaveChanges();
return RedirectToAction(nameof(Index));
[HttpPost]
public IActionResult Delete(Employee employee)
context.Attach(employee);
employee.SoftDeleted = true;
context.SaveChanges();
return RedirectToAction(nameof(Index));
要允许用户使用软删除功能,我向 Index.cshtml 视图添加了新元素,如清单20-11所示,它将向Delete
action 方法发送包含Employee
主键值的 HTTP POST 请求。
清单 20-11:Views/Home 文件夹下的 Index.cshtml 文件,添加元素
@model IEnumerable<Employee>
ViewData["Title"] = "Advanced Features";
Layout = "_Layout";
<h3 class="bg-info p-2 text-center text-white">Employees</h3>
<table class="table table-sm table-striped">
<thead>
<th>Key</th>
<th>SSN</th>
<th>First Name</th>
<th>Family Name</th>
<th>Salary</th>
<th></th>
</thead>
<tbody>
<tr class="placeholder"><td colspan="7" class="text-center">No Data</td></tr>
@foreach (Employee e in Model)
<td>@e.Id</td>
<td>@e.SSN</td>
<td>@e.FirstName</td>
<td>@e.FamilyName</td>
<td>@e.Salary</td>
<td class="text-right">
<input type="hidden" name="SSN" value="@e.SSN" />
<input type="hidden" name="Firstname" value="@e.FirstName" />
<input type="hidden" name="FamilyName"
value="@e.FamilyName" />
<button type="submit" asp-action="Delete" formmethod="post"
class="btn btn-sm btn-danger">
Delete
</button>
<button type="submit" asp-action="Edit" formmethod="get"
class="btn btn-sm btn-primary">
</button>
</form>
</tbody>
</table>
<div class="text-center">
<a asp-action="Edit" class="btn btn-primary">Create</a>
form
元素及其内容用于删除和编辑功能。每个按钮元素都配置了 action 和 HTTP 方法,用户单击它时应该使用该方法,以替换我以前使用的锚元素。
在 AdvancedApp 项目文件夹下运行清单20-12所示的命令,创建新的迁移并应用至数据库。
清单 20-12:创建并应用数据库迁移
dotnet ef migrations add SoftDelete
dotnet ef database update
要查看软删除的效果,使用dotnet run
启动应用程序,导航至 http://localhost:5000,删除一个Employee
对象。删除对象后,它将从Employee
对象表中消失,如图20-3所示。
图20-3 软删除数据
重写查询过滤器
只有在有删除对象的工具时,才能对象进行软删除。这意味着我需要重写过滤器,以便能够查询数据库中的软删除对象,并将它们呈现给用户。我在 Controllers 文件夹中添加了一个名为 DeleteController.cs 的类文件,并使用它来定义如清单20-13所示的控制器。
清单 20-13:Controllers 文件夹下的 DeleteController.cs 文件的内容
using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace AdvancedApp.Controllers
public class DeleteController : Controller
private AdvancedContext context;
public DeleteController(AdvancedContext ctx) => context = ctx;
public IActionResult Index()
return View(context.Employees.Where(e => e.SoftDeleted)
.Include(e => e.OtherIdentity).IgnoreQueryFilters());
[HttpPost]
public IActionResult Restore(Employee employee)
context.Employees.IgnoreQueryFilters()
.First(e => e.SSN == employee.SSN
&& e.FirstName == employee.FirstName
&& e.FamilyName == employee.FamilyName).SoftDeleted = false;
context.SaveChanges();
return RedirectToAction(nameof(Index));
Index
和Restore
action 方法都需要查询软删除对象,而查询过滤器排除了它们。为了确保这些查询能够访问它们所需的数据,我调用了IgnoreQueryFilters
方法,如下所示:
return View(context.Employees.Where(e => e.SoftDeleted)
.Include(e => e.OtherIdentity).IgnoreQueryFilters());
此方法在不应用查询筛选器的情况下进行查询。为了向控制器提供一个视图,我创建了 Views/Delete 文件夹,并向它添加了一个名为 Index.cshtml 的文件,内容如清单20-14所示。
清单 20-14:Views/Delete 文件夹下的 Index.cshtml 文件的内容
@model IEnumerable<Employee>
ViewData["Title"] = "Advanced Features";
Layout = "_Layout";
<h3 class="bg-info p-2 text-center text-white">Deleted Employees</h3>
<table class="table table-sm table-striped">
<thead>
<th>SSN</th>
<th>First Name</th>
<th>Family Name</th>
<th></th>
</thead>
<tbody>
<tr class="placeholder"><td colspan="4" class="text-center">No Data</td></tr>
@foreach (Employee e in Model)
<td>@e.SSN</td>
<td>@e.FirstName</td>
<td>@e.FamilyName</td>
<td class="text-right">
<form method="post">
<input type="hidden" name="SSN" value="@e.SSN" />
<input type="hidden" name="FirstName" value="@e.FirstName" />
<input type="hidden" name="FamilyName"
value="@e.FamilyName" />
<button asp-action="Restore"
class="btn btn-sm btn-success">
Restore
</button>
</form>
</tbody>
</table>
使用dotnet run
启动应用程序并导航至 http://localhost:5000/delete;您将看到一个软删除对象列表。单击【Restore】按钮以将对象的SoftDeleted
属性设置为false
,这会将其还原到由 Home 控制器提供的主数据表中,如图20-4所示。
图20-4 还原软删除对象
使用搜索模式进行查询
Entity Framework Core 支持 SQL 的LIKE
表达式,这意味着可以使用搜索模式进行查询。在清单 20-15 中,我更改了 Home 控制器的Index
action,以便它接收用于创建LIKE
查询的搜索项参数。
清单 20-15:Controllers 文件夹下的 HomeController.cs 文件,使用搜索模式
using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace AdvancedApp.Controllers
public class HomeController : Controller
private AdvancedContext context;
public HomeController(AdvancedContext ctx) => context = ctx;
public IActionResult Index(string searchTerm)
IQueryable<Employee> data = context.Employees;
if (!string.IsNullOrEmpty(searchTerm))
data = data.Where(e => EF.Functions.Like(e.FirstName, searchTerm));
return View(data);
public IActionResult Edit(string SSN, string firstName, string familyName)
return View(string.IsNullOrWhiteSpace(SSN)
? new Employee() : context.Employees.Include(e => e.OtherIdentity)
.AsNoTracking()
.First(e => e.SSN == SSN
&& e.FirstName == firstName
&& e.FamilyName == familyName));
[HttpPost]
public IActionResult Update(Employee employee)
Employee existing = context.Employees
.AsTracking()
.First(e => e.SSN == employee.SSN && e.FirstName == employee.FirstName
&& e.FamilyName == employee.FamilyName);
if (existing == null)
context.Add(employee);
existing.Salary = employee.Salary;
context.SaveChanges();
return RedirectToAction(nameof(Index));
[HttpPost]
public IActionResult Delete(Employee employee)
context.Attach(employee);
employee.SoftDeleted = true;
context.SaveChanges();
return RedirectToAction(nameof(Index));
LINQ 中没有对LIKE
的直接支持,这会导致语法上的尴尬。EF.Functions.Like
方法用于访问Where
子句中的Like
功能,并接收将被匹配的属性和作为参数的搜索项。在清单中,我使用了Like
方法搜索其FirstName
值与 action 方法接收的搜索项参数相匹配的Employee
对象。搜索项可以用四个通配符来表示,它们在表20-6中描述。
表 20-6:SQL LIKE 通配符
要查看搜索是如何工作的,使用dotnet run
启动应用程序,确保所有示例Employee
对象已经从软删除中恢复,导航至以下 URL:
http://localhost:5000?searchTerm=%[ae]%
在 URL 查询字符串中指定的搜索项将匹配包含字母A
或E
的任何名称。如果检查应用程序生成的日志消息,您将看到 Entity Framework Core 已发送到数据库服务器的查询。
SELECT [e].[SSN], [e].[FirstName], [e].[FamilyName], [e].[Salary], [e].[SoftDeleted]
FROM [Employees] AS [e]
WHERE ([e].[SoftDeleted] = 0) AND [e].[FirstName] LIKE @__searchTerm_1
查询的重要部分是LIKE
关键字,我已经突出显示了这个关键字。这将确保只从数据库中读取与搜索词匹配的对象。
在使用表20-3中的数据创建的三个对象中,只有Alice
和Peter
将被搜索词匹配,产生如图20-5所示的结果。
图20-5 在查询中使用搜索项
避免 LIKE 评估陷阱
必须注意将EF.Functions.Like
方法仅应用于IQueryable<T>
对象。您必须避免对IEnumerable<T>
对象使用如下方式调用LIKE
方法:
public IActionResult Index(string searchTerm)
IEnumerable<Employee> data = context.Employees;
if (!string.IsNullOrEmpty(searchTerm))
data = data.Where(e => EF.Functions.Like(e.FirstName, searchTerm));
return View(data);
结果是一样的,但如果检查发送到数据库服务器的查询,则会发现查询中没有包含LIKE
关键字。
SELECT [e].[SSN], [e].[FirstName], [e].[FamilyName], [e].[Salary], [e].[SoftDeleted]
FROM [Employees] AS [e]
WHERE [e].[SoftDeleted] = 0
Entity Framework Core 将检索搜索项可能匹配的所有对象,在应用程序中处理它们,并丢弃那些不需要的对象。对于示例应用程序,这意味着查询将加载一个额外的对象,但在实际项目中,加载并丢弃的数据量可能很大。
进行异步查询
大多数使用 Entity Framework Core 的查询都是同步的。在大多数应用程序中,同步查询是完全可以接受的,因为查询是 ASP.NET Core MVC action 方法执行的唯一活动,该方法也是同步的。
Entity Framework Core 也可以异步执行查询,如果您使用异步 action 方法,并且该 action 方法需要同时执行多个活动,而这些活动中只有一个是数据库查询,则该查询非常有用。异步查询有用的一组环境非常具体,事实上,大多数 ASP.NET Core MVC 项目都不需要使用它们。
在清单20-16中,我已经重写Index
action 以便它变为异步的,并且利用 Entity Framewrok Core 对异步查询的支持。
清单 20-16:Controllers 文件夹下的 HomeController.cs 文件,进行异步查询
using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
using System.Net.Http;
namespace AdvancedApp.Controllers
public class HomeController : Controller
private AdvancedContext context;
public HomeController(AdvancedContext ctx) => context = ctx;
public async Task<IActionResult> Index(string searchTerm)
IQueryable<Employee> employees = context.Employees;
if (!string.IsNullOrEmpty(searchTerm))
employees = employees.Where(e =>
EF.Functions.Like(e.FirstName, searchTerm));
HttpClient client = new HttpClient();
ViewBag.PageSize = (await client.GetAsync("http://apress.com"))
.Content.Headers.ContentLength;
return View(await employees.ToListAsync());
// ...其它省略...
异步查询的局限性使得很难创建一个有用的示例。在清单中,我使用HttpClient
类向 apress.com 发送异步 HTTP GET 请求,同时还查询数据库。
避免并发查询陷阱
Microsoft 特别警告不要使用 context 对象来执行多个异步请求,因为 DbContext 类的编写并不是为了适应这些请求。这导致一些有进取心的开发人员使用依赖注入来接收两个 context 对象,每个对象用于执行并发异步请求。
public HomeController(AdvancedContext ctx, AdvancedContext ctx2) {
这种方法的问题在于 ASP.NET Core MVC 依赖注入功能只会创建一个 context 对象,并使用它来解决这两个依赖关系,这意味着始终只存在一个 context 对象。我的建议是接受异步查询支持的限制。
Entity Framework Core 提供了一系列强制对查询进行异步计算的方法。表20-7中描述了最常用的异步方法,但是对于强制查询计算的所有方法都有异步等效方法,因此LastAsync
方法是Last
方法的异步对应。创建查询不需要异步方法的版本(如Where
),因为它们在不执行查询的情况下构建查询。
表 20-7:执行异步查询的常用方法
注意:ForEachAsync
方法没有对应的同步版本,但可用于为从查询结果创建的每个对象调用函数。
在清单中,我使用ToListAsync
方法来异步地查询数据库,并将其产生的List<Employee>
传递给View
方法。List<T>
类实现了IEnumerable<T>
接口,这意味着现有视图可以在不进行任何更改的情况下枚举对象。为了显示清单20-16中的异步 HTTP 请求从 apress.com 读取的字节数,我将清单20-17中所示的元素添加到 Index 视图中。
清单 20-17:Views/Home 文件夹下的 Index.cshtml 文件,添加元素
@model IEnumerable<Employee>
ViewData["Title"] = "Advanced Features";
Layout = "_Layout";
<h3 class="bg-info p-2 text-center text-white">Employees</h3>
<table class="table table-sm table-striped">
<!-- ...此处省略... -->
</table>
@if (ViewBag.PageSize != null)
<h4 class="bg-info p-2 text-center text-white">
Page Size: @ViewBag.PageSize bytes
<div class="text-center">
<a asp-action="Edit" class="btn btn-primary">Create</a>
要测试异步查询,使用dotnet run
启动应用程序,并导航至 http://localhost:5000。您将看到如图20-6所示的 page size 元素,以及同时从数据库中检索的员工数据。(您可能会看到不同数量的字节显示,因为 apress.com 经常被更新。)
图20-6 进行并发查询
显式编译查询
最引人注目的 Entity Framework Core 功能之一是将 LINQ 查询转换为 SQL 的方式。翻译过程可能很复杂,为了提高性能,Entity Framework Core 自动保存它处理过的查询的缓存,并为其处理的每个查询创建哈希表示,以确定是否有缓存的译本可用。如果存在,则使用缓存译本;如果没有,则创建一个新的译本,并将其放入缓存中供将来使用。
您可以通过查询的显式译本来提高此过程的性能,这样 Entity Framework Core 就不必创建哈希代码并检查缓存。这被称为查询的显式编译(explicitly compiling)。在清单20-18中,我更新了 Home 控制器,以便显式编译Index
action 执行的查询。
清单 20-18:Controllers 文件夹下的 HomeController.cs 文件,编辑一个查询
using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
using System.Net.Http;
using System;
using System.Collections.Generic;
namespace AdvancedApp.Controllers
public class HomeController : Controller
private AdvancedContext context;
private static Func<AdvancedContext, string, IEnumerable<Employee>> query
= EF.CompileQuery((AdvancedContext context, string searchTerm)
=> context.Employees
.Where(e => EF.Functions.Like(e.FirstName, searchTerm)));
public HomeController(AdvancedContext ctx) => context = ctx;
public IActionResult Index(string searchTerm)
return View(string.IsNullOrEmpty(searchTerm)
? context.Employees : query(context, searchTerm));
// ...其它省略...
生成编译查询的语句可能难以阅读。编译是使用EF.CompileQuery
方法执行的,如下所示:
EF.CompileQuery((AdvancedContext context, string searchTerm)
=> context.Employees.Where(e => EF.Functions.Like(e.FirstName, searchTerm)));
CompileQuery
方法的参数是 lambda 表达式,它接收查询中使用的 context 对象和参数,并返回IQueryable<T>
作为结果。在清单中,lambda 表达式接收一个AdvancedContext
对象和一个字符串,并使用它们创建一个IQueryable<Employee>
,它将使用LIKE
功能查询数据库。
EF.CompileQuery
方法的结果是Func<AdvancedContext, string, IEnumerable<Employee>>
对象,它表示接受 context 和字符串并生成一系列Employee
对象的函数。
private static Func<AdvancedContext, string, IEnumerable<Employee>> query
= EF.CompileQuery((AdvancedContext context, string searchTerm)
=> context.Employees.Where(e => EF.Functions.Like(e.FirstName, searchTerm)));
请注意,编译的函数返回一个IEnumerable<T>
对象,这意味着对结果执行的任何进一步操作都将在内存中执行,而不是基于发送到数据库的请求。这是有意义的,因为这个过程的目的是创建一个不可变的查询,这意味着您必须确保所需查询的每个方面都包含在传递给CompileQuery
方法的表达式中。
执行查询是通过调用CompiledQuery
方法返回的函数来完成的,如下所示:
return View(string.IsNullOrEmpty(searchTerm )
? context.Employees : query(context, searchTerm));
执行编译查询的方式没有明显的差别,但在幕后,Entity Framework Core 可以跳过创建查询的哈希表示的过程,并检查它是否以前已经翻译过。
避免过度查询陷阱
注意,我在显式编译查询之外检查Index
action 方法的searchTerm
参数是否为null
。在定义查询表达式时,常见的错误是包含要在应用程序中执行的检查,如下所示:
EF.CompileQuery((AdvancedContext context, string searchTerm)
=> context.Employees.Where(e => string.IsNullOrEmpty(searchTerm)
|| EF.Functions.Like(e.FirstName, searchTerm)));
问题是 Entity Framework Core 将对null
值的检查合并到 SQL 查询中,如下所示,这可能不是您想要的:
SELECT [e].[SSN], [e].[FirstName], [e].[FamilyName], [e].[Salary],
[e].[SoftDeleted]
FROM [Employees] AS [e]
WHERE ([e].[SoftDeleted] = 0) AND ((@__searchTerm IS NULL
OR (@__searchTerm = N'')) OR [e].[FirstName] LIKE @__searchTerm)
这是与我在下一节中描述的客户端评估陷阱相反的问题,但这两个问题都强调了检查 Entity Framework 生成的 SQL 查询的重要性,以确保它们精确地针对您想要的数据。
避免客户端评估陷阱
当您开始使用 Entity Framework Core 时,您可能需要一段时间才能确信 LINQ 查询将被转换为所需的 SQL。正如这本书所演示的,有许多潜在的陷阱可能导致太多的数据或太少的数据被从数据库检索到。有一个潜在的错误非常常见,以至于 Entity Framework Core 在将查询转换为 SQL 时会警告您。
当 Entity Framework Core 无法查看 LINQ 查询的所有细节并无法将其完全转换为 SQL 时,就会出现此问题。这通常发生在对查询进行重构时,以便在整个应用程序中更一致地使用选择一组数据对象的代码。Entity Framework Core 将查询拆分为由数据库服务器执行查询的部分和由客户端应用程序执行的查询。这不仅增加了应用程序必须执行的处理量,而且可能极大地增加查询从数据库检索的数据量。
为了演示,我在 Controllers 文件夹中添加了一个名为 QueryController.cs 的类文件,并使用它定义了如清单20-19所示的控制器。
清单 20-19:Controllers 文件夹下的 QueryController.cs 文件的内容
using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
namespace AdvancedApp.Controllers
public class QueryController : Controller
private AdvancedContext context;
public QueryController(AdvancedContext ctx) => context = ctx;
public IActionResult ServerEval()
return View("Query", context.Employees.Where(e => e.Salary > 150_000));
public IActionResult ClientEval()
return View("Query", context.Employees.Where(e => IsHighEarner(e)));
private bool IsHighEarner(Employee e)
return e.Salary > 150_000;
控制器定义两个 action,用于查询薪资值大于150,000
的Employee
对象。ServerEval
方法将过滤器表达式直接放在 LINQ 表达式的Where
子句中,而ClientEval
方法使用一个单独的方法,该方法表示一个典型的重构过程,它允许将选择高收入者的标准放到单独方法中。
为了向这两个 action 提供一个视图,我创建了 Views/Query 文件夹,并向其添加了一个名为 Query.cshtml 的文件,其包含清单20-20所示内容。
清单 20-20:Views/Query 文件夹下的 Query.cshtml 文件的内容
@model IEnumerable<Employee>
ViewData["Title"] = "Advanced Features";
Layout = "_Layout";
<h3 class="bg-info p-2 text-center text-white">Employees</h3>
<table class="table table-sm table-striped">
<thead>
<th>SSN</th>
<th>First Name</th>
<th>Family Name</th>
<th>Salary</th>
</thead>
<tbody>
<tr class="placeholder"><td colspan="4" class="text-center">No Data</td></tr>
@foreach (Employee e in Model)
<td>@e.SSN</td>
<td>@e.FirstName</td>
<td>@e.FamilyName</td>
<td>@e.Salary</td>
</tbody>
</table>
要了解查询执行方式的不同,请使用dotnet run
启动应用程序,运行并导航到 http://localhost:5000/query/servereval 和 http://localhost:5000/query/clienteval,这两种方法都将产生图20-7所示的结果。
图20-7 查询结果
要理解这两个 action 方法之间的区别,必须检查发送到数据库服务器的查询。ServerEval
action 的结果是这个查询:
SELECT [e].[SSN], [e].[FirstName], [e].[FamilyName], [e].[Salary], [e].[SoftDeleted]
FROM [Employees] AS [e]
WHERE ([e].[SoftDeleted] = 0) AND ([e].[Salary] > 150000.0)
此查询中的WHERE
子句将只检索Salary
值超过150,000
的Employee
对象。这意味着只有随后将显示给用户的对象才会从数据库中检索。
相反,这里是由ClientEval
action 生成的查询:
SELECT [e].[SSN], [e].[FirstName], [e].[FamilyName], [e].[Salary], [e].[SoftDeleted]
FROM [Employees] AS [e]
WHERE [e].[SoftDeleted] = 0
Entity Framework Core 无法查看控制器的IsHighEarner
方法,并将它包含的逻辑合并到 SQL 查询中。相反,Entity Framework Core 将它可以看到的查询部分转换为 SQL,然后通过IsHighEarner
方法传递它接收的对象,以生成查询结果。没有由IsHighEarner
方法选择的对象被丢弃,其结果是从数据库读取更多的数据,而应用程序需要更多的工作来生成所需的结果。在示例应用程序中,这意味着读取和创建一个额外的对象,但在实际应用程序中,差异可能很大。
抛出客户端评估异常
当必须在客户端中计算查询的一部分时,Entity Framework Core 将在日志记录输出中显示警告消息,如下所示:
The LINQ expression 'where value(AdvancedApp.Controllers.QueryController)
.IsHighEarner([e])' could not be translated and will be evaluated locally
在日志记录消息流中,这很容易被忽略,您可能会发现,客户端对查询的评估传递时不被注意,直到在生产应用程序中出现性能问题。在开发期间,当在客户端中计算查询的一部分时,接收异常可能很有用,这将使问题更加明显。在清单20-21中,我更改了 Entity Framework Core 配置,以便抛出异常。
清单 20-21:AdvancedApp 文件夹下的 Startup.cs 文件,配置异常
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using AdvancedApp.Models;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace AdvancedApp
public class Startup
public Startup(IConfiguration config) => Configuration = config;
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
services.AddMvc();
string conString = Configuration["ConnectionStrings:DefaultConnection"];
services.AddDbContext<AdvancedContext>(options =>
options.UseSqlServer(conString).ConfigureWarnings(warning =>
warning.Throw(RelationalEventId.QueryClientEvaluationWarning)));
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
app.UseDeveloperExceptionPage();
app.UseStatusCodePages();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
ConfigureWarnings
方法用于使用 lambda 表达式配置 Entity Framework Core 产生的警告,该表达式接收定义表20-8中方法的WarningsConfigurationsBuilder
对象。
表 20-8:WarningsConfigurationBuilder 方法
表20-8中的方法与RelationalEventId
枚举一起使用,它定义了表示可能在 Entity Framework Core 应用程序中遇到的诊断事件的值。几乎有 30 个不同事件的值,尽管其中大多数与数据库连接和事务的生命周期以及迁移的创建和应用有关。您可以在 https://docs.microsoft.com/en-us/ef/core/api/microsoft.entityframeworkcore.infrastructure.relationaleventid 查看完整的事件清单。我在清单20-20中使用的值是QueryClientEvaluationWarning
,它表示当请求的一部分由客户端计算时,由 Entity Framework Core 触发的事件。要查看更改的效果,请使用dotnet run
启动应用程序,然后导航到 http://localhost:5000/query/clienteval。您将看到图20-8中的异常,而不是容易忽略的警告。
图20-8 客户端查询计算异常
在本章中,我描述了 Entity Framework Core 为查询数据提供的高级功能。我解释了如何控制更改跟踪功能,如何使用查询过滤器,如何使用 SQL 的LIKE
的功能执行查询,以及如何显式编译查询。在结束本章时,我演示了重构查询所引起的一个常见问题,以及如何配置应用程序,以便您知道何时会在自己的项目中遇到此问题。在下一章中,我将描述存储数据的高级功能。
© 2018 - IOT小分队文章发布系统 v0.3