EF Core 9 中的新增功能:您需要了解的功能

· 6 分钟阅读

Entity Framework Core 9 于 2024 年 11 月与 .NET 9 一起发布,在花了大量时间在多个项目中使用它之后,我可以说它是一段时间内最有意义的版本之一。不是因为它重新发明了轮子,而是因为它完善了 EF Core 历史上造成最大摩擦的领域——查询翻译、性能以及使用现代数据模式。

在这篇文章中,我将介绍对我的日常工作影响最大的功能。如果您仍在使用 EF Core 8(甚至 7),这应该可以让您清楚地了解升级的另一边等待您的是什么。

.NET 9 生态系统中的 EF Core 9

EF Core 9 针对 .NET 8 和 .NET 9,这意味着您不一定需要将整个应用程序升级到 .NET 9 才能利用其中大部分功能。也就是说,一些 AOT 和性能改进与 .NET 9 运行时更改紧密结合,因此您可以通过一路走来充分利用它。

该版本遵循 Microsoft 制定的奇数/偶数节奏:奇数版本(如 .NET 9)是标准期限支持 (STS),提供 18 个月的支持,而偶数版本(如 .NET 8)是长期支持 (LTS)。在规划升级时间表时请记住这一点。

LINQ 翻译改进

这是大多数开发人员会立即感受到差异的地方。 EF Core 9 在将 LINQ 表达式转换为真正有意义的 SQL 方面取得了重大进展。

更好的 GroupBy 翻译

如果您曾经在 EF Core 中编写过 GroupBy 查询并最终收到客户端评估警告或奇怪的 SQL,您就会知道这种痛苦。 EF Core 9 直接在 SQL 中处理更广泛的 GroupBy 场景。

var salesByCategory = await context.Products
    .GroupBy(p => p.Category.Name)
    .Select(g => new
    {
        Category = g.Key,
        TotalRevenue = g.Sum(p => p.Price * p.UnitsSold),
        AveragePrice = g.Average(p => p.Price),
        ProductCount = g.Count()
    })
    .OrderByDescending(x => x.TotalRevenue)
    .ToListAsync();

在以前的版本中,涉及 GroupBy 内导航属性聚合的查询有时会回退到客户端评估。 EF Core 9 将其干净地转换为具有 GROUP BYSUMAVGCOUNT 的单个 SQL 查询。

复杂的投影和子查询

嵌套子查询和复杂投影也得到了重大升级。考虑这样的事情:

var orderSummaries = await context.Customers
    .Select(c => new CustomerSummaryDto
    {
        Name = c.FullName,
        TotalOrders = c.Orders.Count(),
        MostRecentOrder = c.Orders
            .OrderByDescending(o => o.OrderDate)
            .Select(o => new OrderBriefDto
            {
                Id = o.Id,
                Date = o.OrderDate,
                Total = o.LineItems.Sum(li => li.Quantity * li.UnitPrice)
            })
            .FirstOrDefault(),
        TopCategory = c.Orders
            .SelectMany(o => o.LineItems)
            .GroupBy(li => li.Product.Category.Name)
            .OrderByDescending(g => g.Count())
            .Select(g => g.Key)
            .FirstOrDefault()
    })
    .ToListAsync();

EF Core 9 现在可以将整个表达式转换为 SQL,而无需触发客户端计算。生成的查询在适当的情况下使用相关子查询和横向联接,并且 SQL 计划比早期版本生成的计划要高效得多。

参数化原始集合

突出的 LINQ 改进之一是能够将原始值集合直接传递到查询中:

var statusFilter = new List<string> { "Active", "Pending", "Review" };

var filteredOrders = await context.Orders
    .Where(o => statusFilter.Contains(o.Status))
    .ToListAsync();

在 EF Core 8 中,这是使用带有内联值的 IN 子句进行转换的,这意味着列表更改时无法重用查询计划缓存。 EF Core 9 正确参数化这些集合,将它们作为结构化参数发送。这对于 SQL Server 和 PostgreSQL 上的查询计划缓存来说是一件大事。## 批量操作 — ExecuteUpdate 和 ExecuteDelete

ExecuteUpdateExecuteDelete 是在 EF Core 7 中引入的,但 EF Core 9 以有意义的方式扩展了您可以使用它们执行的操作。

更复杂的更新表达式

您现在可以在 ExecuteUpdate 中使用更复杂的表达式,包括通过导航属性引用其他表:

await context.Products
    .Where(p => p.Category.IsDiscontinued)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsAvailable, false)
        .SetProperty(p => p.DiscontinuedDate, DateTimeOffset.UtcNow)
        .SetProperty(p => p.Price, p => p.Price * 0.5m));

这会生成一个带有 JOIN 到类别表的 UPDATE 语句 — 不需要将实体加载到内存中,也没有更改跟踪开销。

使用子查询进行条件批量删除

现在完全支持使用子查询过滤器进行批量删除:

await context.AuditLogs
    .Where(log => log.CreatedAt < DateTime.UtcNow.AddYears(-2))
    .Where(log => !context.ProtectedRecords
        .Any(pr => pr.AuditLogId == log.Id))
    .ExecuteDeleteAsync();

这将转换为带有 NOT EXISTS 子查询的 DELETE,这正是您手写的内容。没有加载实体,没有往返。

JSON 列增强功能

JSON 列是最近 EF Core 版本中最令人兴奋的功能之一,而 EF Core 9 更进一步。

查询 JSON 内部

您现在可以通过更好的翻译支持从 JSON 列中过滤和投影数据:

public class Order
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
    public ShippingAddress Address { get; set; } // Stored as JSON
    public List<OrderNote> Notes { get; set; }   // Stored as JSON
}

public class ShippingAddress
{
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
    public string Country { get; set; }
}

public class OrderNote
{
    public DateTime CreatedAt { get; set; }
    public string Text { get; set; }
    public string Author { get; set; }
}

您的 DbContext 中的配置:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>(builder =>
    {
        builder.OwnsOne(o => o.Address, ab =>
        {
            ab.ToJson();
        });

        builder.OwnsMany(o => o.Notes, nb =>
        {
            nb.ToJson();
        });
    });
}

现在您可以直接查询 JSON:

var newYorkOrders = await context.Orders
    .Where(o => o.Address.State == "NY")
    .OrderByDescending(o => o.Notes.Count)
    .Select(o => new
    {
        o.CustomerName,
        o.Address.City,
        LatestNote = o.Notes
            .OrderByDescending(n => n.CreatedAt)
            .Select(n => n.Text)
            .FirstOrDefault()
    })
    .ToListAsync();

EF Core 9 在 SQL Server(或其他提供程序上的等效项)上生成正确的 JSON_VALUEJSON_QUERY 调用,并且转换涵盖了比以前更广泛的 JSON 元素 LINQ 操作。

更新 JSON 属性

EF Core 8 中的摩擦点之一是更新 JSON 列内的单个属性会导致整个 JSON 文档被重写。 EF Core 9 通过对 JSON 映射类型进行更精细的更改跟踪来改进这一点,并在可能的情况下生成更有针对性的更新。

var order = await context.Orders.FindAsync(orderId);
order.Address.ZipCode = "10001";
await context.SaveChangesAsync();

在受支持的提供程序上,这可以生成更有针对性的 JSON 修改,而不是重写整个 blob。

复杂类型——没有标识的值对象

复杂类型是领域驱动设计实践者一直在等待的功能之一。与自有类型不同,复杂类型没有身份——它们是纯值对象。

[ComplexType]
public record Money(decimal Amount, string Currency);

[ComplexType]
public record DateRange(DateTime Start, DateTime End);

public class Project
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Money Budget { get; set; }
    public DateRange Timeline { get; set; }
}

这些作为平展列存储在父表中 - Budget_AmountBudget_CurrencyTimeline_StartTimeline_End - 不需要单独的表或任何类型的键。

与自有类型的主要区别:复杂类型是按值比较,而不是按引用比较。具有相同 AmountCurrency 的两个 Money 实例被视为相等,无论它们属于哪个实体。

var expensiveProjects = await context.Projects
    .Where(p => p.Budget.Amount > 100_000m && p.Budget.Currency == "USD")
    .OrderByDescending(p => p.Budget.Amount)
    .ToListAsync();

这直接转化为对展平列的过滤——干净、高效,并且正是您所期望的。

SQL Server 的 HierarchyId 支持

如果您曾经使用过 SQL Server 中的分层数据(组织结构图、类别树、文件系统),您就会知道 HierarchyId 是其内置类型。 EF Core 9 为其带来了一流的支持。

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Title { get; set; }
    public HierarchyId PathFromCeo { get; set; }
}

现在可以直接查询层次关系:

var managerId = HierarchyId.Parse("/1/3/");

// All direct and indirect reports
var allReports = await context.Employees
    .Where(e => e.PathFromCeo.IsDescendantOf(managerId))
    .Where(e => e.PathFromCeo != managerId) // exclude the manager
    .OrderBy(e => e.PathFromCeo)
    .ToListAsync();

// Direct reports only (one level down)
var directReports = await context.Employees
    .Where(e => e.PathFromCeo.GetAncestor(1) == managerId)
    .ToListAsync();

// Get an employee's depth in the hierarchy
var employeesWithDepth = await context.Employees
    .Select(e => new
    {
        e.Name,
        e.Title,
        Level = e.PathFromCeo.GetLevel()
    })
    .OrderBy(e => e.Level)
    .ToListAsync();
```所有这些都转换为 SQL Server 的本机 `HierarchyId` 方法。如果您一直在使用自引用外键和递归 CTE 来实现树结构,那么这是一种更简洁的方法。

## 编译模型和 AOT 支持

注重性能的开发人员将欣赏对编译模型和提前 (AOT) 编译支持的持续投资。

### 编译模型

编译模型会预先生成 EF Core 通常在启动时构建的模型元数据。对于大型模型(考虑数百个实体),这可以显着减少冷启动时间。

```bash
dotnet ef dbcontext optimize --output-dir CompiledModels --namespace MyApp.CompiledModels

然后连线:

builder.Services.AddDbContext<AppDbContext>(options =>
    options
        .UseSqlServer(connectionString)
        .UseModel(MyApp.CompiledModels.AppDbContextModel.Instance));

在 EF Core 9 中,编译的模型更加完整 - 它们支持更多映射功能并生成更小的输出。对于具有大约 400 个实体的模型,启动时间可以从几秒缩短到接近瞬时。

AOT编译进度

对 EF Core 的完整本机 AOT 支持仍在进行中,但 EF Core 9 取得了重大进展。许多反射密集型代码路径已被重构为修剪友好型,编译模型是 AOT 故事的关键部分。如果你的目标是 Azure Functions 或微服务等冷启动很重要的场景,那么这些改进是直接相关的。

Cosmos DB 提供程序更新

Azure Cosmos DB 提供程序通过 EF Core 9 不断成熟。一些值得注意的改进:

分区键处理

该提供程序现在支持分层分区键并更智能地处理分区键过滤器:

public class TenantDocument
{
    public string Id { get; set; }
    public string TenantId { get; set; }  // Partition key
    public string Region { get; set; }     // Sub-partition key
    public string Content { get; set; }
}
// This query now correctly uses the partition key for routing
var docs = await context.TenantDocuments
    .Where(d => d.TenantId == "tenant-42" && d.Region == "us-east")
    .Where(d => d.Content.Contains("important"))
    .ToListAsync();

改进了 LINQ 到 NoSQL 的转换

更多 LINQ 操作现在转换为 Cosmos DB 的 SQL 方言,包括对 ContainsAny、嵌套数组操作和数学函数的更好支持。以前退回到客户端评估的查询现在由服务器端处理。

矢量搜索支持

EF Core 9 引入了对 Cosmos DB 向量相似性搜索的早期支持,如果您正在构建与嵌入或 AI 驱动的搜索集成的应用程序,这将非常有用:

var results = await context.Documents
    .OrderBy(d => EF.Functions.VectorDistance(d.Embedding, queryVector))
    .Take(10)
    .ToListAsync();

迁移改进

迁移带来了一些生活质量的改善,这使得在团队环境中与他们一起工作不再那么痛苦。

迁移中的时态表

迁移现在可以更优雅地处理时态表配置,并适当支持周期列和历史表命名:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>()
        .ToTable("Employees", b => b.IsTemporal(t =>
        {
            t.HasPeriodStart("ValidFrom");
            t.HasPeriodEnd("ValidTo");
            t.UseHistoryTable("EmployeeHistory");
        }));
}

幂等脚本

默认情况下,Script-Migration 命令(及其 CLI 等效项)会生成更好的幂等脚本,并改进了对依赖于某些状态中存在的数据的模式更改的边缘情况的处理。

迁移包

迁移捆绑包将迁移打包成独立的可执行文件以进行部署,在 EF Core 9 中更加可靠,具有更好的错误报告和针对暂时性故障的重试逻辑。

dotnet ef migrations bundle --self-contained -r linux-x64

这会生成一个可以在 CI/CD 管道中运行的二进制文件,而无需在部署目标上安装 .NET SDK。

性能基准以下是我自己测试的一些粗略基准。这些来自一个包含大约 200 个实体的项目,针对 SQL Server 2022 运行,并使用 BenchmarkDotNet 进行测量。您的数字会有所不同,但相对改进应该相似。

场景EF Core 8EF Core 9改进
模型构建(冷启动)1,850 毫秒320 毫秒速度提高约 5.8 倍(编译)
简单查询(单实体PK)0.42 毫秒0.38 毫秒快约 10%
复杂查询(连接+聚合)3.1 毫秒2.4 毫秒快约 23%
批量更新(10k 行)145 毫秒118 毫秒快约 19%
JSON列查询2.8 毫秒1.9 毫秒快约 32%
SaveChanges(100 个实体)48 毫秒41 毫秒快约 15%

编译模型的改进是最显着的,但全面的稳定改进加起来 - 特别是在每秒执行数千个查询的高吞吐量场景中。

从 EF Core 8 升级

如果您使用 EF Core 8,升级路径相对顺利。这是一个清单:

1.更新您的软件包:

<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />

2.检查重大更改。 与某些以前的版本相比,EF Core 9 的列表相对较短。最值得注意的是:

  • 一些以前过时的 API 已被删除
  • 某些 GroupBy 查询的翻译方式发生变化(它们现在转到服务器端,如果您依赖客户端评估,这会改变行为)
  • 迁移脚手架输出的微小变化

**3.如果您正在使用编译模型,请重新生成它们。格式已更改,因此旧的编译模型将无法与 EF Core 9 一起使用。

4.运行您的测试套件。 特别注意之前在客户端上评估的查询 - 它们现在可能在服务器上评估,这通常更好,但可能会出现数据差异。

5.如果您正在使用该提供商,请检查您的 Cosmos DB 查询。改进的翻译意味着某些查询将以不同的方式执行(通常更快),但值得验证结果是否相同。

典型项目的最小升级如下所示:

dotnet add package Microsoft.EntityFrameworkCore --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
dotnet build
dotnet test

如果一切都编译并且测试通过,那么您可能处于良好状态。如果您遇到问题,EF Core 9 重大更改文档为每个更改提供了详细的迁移指南。

总结

EF Core 9 不是一个革命性的版本 - 它是一个进化的版本,而这正是它所需要的。仅 LINQ 改进就足以证明大多数项目的升级是合理的,并且 JSON 列增强、复杂类型和 HierarchyId 等功能支持以前难以或不可能的开放模式。

如果我必须选择对我的项目影响最大的三个功能:

  1. 参数化原始集合 - 因为查询计划缓存效率在规模上很重要
  2. JSON 列改进 — 因为混合关系文档模式非常有用
  3. 编译模型——因为启动时间直接影响开发人员的生产力和部署速度自 EF Core 5 以来,EF Core 团队一直走在坚实的轨道上,版本 9 延续了这一趋势。如果您已经使用 EF Core 8,则升级风险低、回报高。如果您正在使用较旧的产品,那么现在就是跳跃的最佳时机。

快乐的编码和快乐的查询。