EF Core 11 新特性:更精简的 SQL 与 FullJoin
前言
EF Core 11 与 .NET 11 同时发布,并且只能运行在 .NET 11 上,不支持 .NET 10,也不支持 .NET Framework。升级 EF Core 11 也就意味着项目要升级到 .NET 11。
下面的 SQL 都不是手写的,而是同一份模型和查询分别引用 Microsoft.EntityFrameworkCore.Sqlite 10.0.15 和 11.0.0,在 SQLite 内存库上执行时记录的实际日志。示例模型如下:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int BlogTypeId { get; set; }
public BlogType BlogType { get; set; } = null!;
public int OwnerId { get; set; }
public Person Owner { get; set; } = null!;
public List<Post> Posts { get; set; } = [];
public BlogDetails Details { get; set; } = new(); // 复杂类型
}
public class BlogDetails
{
public string Description { get; set; } = "";
public string Language { get; set; } = "zh-CN";
}
public class Customer { public int Id { get; set; } public string Name { get; set; } = ""; }
public class Order { public int Id { get; set; } public int? CustomerId { get; set; } public decimal Total { get; set; } }
SQL 通过 LogTo 只捕获 CommandExecuted 事件:
protected override void OnConfiguring(DbContextOptionsBuilder b) => b
.UseSqlite("Data Source=:memory:")
.LogTo(log, [RelationalEventId.CommandExecuted]);
1. 引用导航不再产生多余的 JOIN 和排序
查询同时 Include 一个引用导航和一个集合导航,并使用拆分查询:
var blogs = await db.Blogs
.Include(b => b.BlogType)
.Include(b => b.Posts)
.AsSplitQuery()
.ToListAsync();
第二条 SQL 负责加载 Posts。EF Core 10:
SELECT "p"."Id", "p"."BlogId", "p"."Title", "b"."Id", "b0"."Id"
FROM "Blogs" AS "b"
INNER JOIN "BlogType" AS "b0" ON "b"."BlogTypeId" = "b0"."Id"
INNER JOIN "Post" AS "p" ON "b"."Id" = "p"."BlogId"
ORDER BY "b"."Id", "b0"."Id"
EF Core 11:
SELECT "p"."Id", "p"."BlogId", "p"."Title", "b"."Id"
FROM "Blogs" AS "b"
INNER JOIN "Post" AS "p" ON "b"."Id" = "p"."BlogId"
ORDER BY "b"."Id"
BlogType 已经在第一条 SQL 里加载过,第二条 SQL 只需要 Blogs 的主键来关联 Posts,那次 INNER JOIN 纯属浪费。第一条 SQL 的 ORDER BY 也从 "b"."Id", "b0"."Id" 变成了 "b"."Id"。
不拆分查询时,引用导航的键同样从 ORDER BY 中去掉了:
var blogs = await db.Blogs.Include(b => b.Owner).Include(b => b.Posts).ToListAsync();
-- EF Core 10
ORDER BY "b"."Id", "p"."Id"
-- EF Core 11
ORDER BY "b"."Id"
道理相同:Owner 由 Blogs 上的外键唯一确定,b.Id 已经足以保证结果分组。EF 团队在 SQL Server 上的测试中,拆分查询场景提升约 29%,单查询场景提升约 22%。实际收益取决于引用导航的数量和表的大小,不需要改任何代码,升级后就能生效。
2. FullJoin 翻译为 FULL JOIN
.NET 11 给 LINQ 增加了 FullJoin(见 .NET 11 基础库:值得马上用起来的新 API),EF Core 11 会把它翻译成 SQL 的 FULL JOIN:
var rows = await db.Customers
.FullJoin(db.Orders,
c => c.Id,
o => o.CustomerId,
(c, o) => new
{
Customer = c == null ? null : c.Name,
OrderId = o == null ? (int?)null : o.Id,
})
.ToListAsync();
SELECT "c"."Name" AS "Customer", "o"."Id" AS "OrderId"
FROM "Customers" AS "c"
FULL JOIN "Orders" AS "o" ON "c"."Id" = "o"."CustomerId"
张三 / 1
李四 / (无订单)
(无客户) / 2
以前在 EF Core 中实现全外连接,只能写原始 SQL,或者用左连接加右侧反连接再 Concat。注意 SQLite 从 3.39 起才支持 FULL JOIN;MySQL 不支持 FULL JOIN,对应的提供程序是否翻译要看具体实现。
结果里的 (无客户) / 2 是一张引用了不存在客户的订单,它是怎么写进数据库的,见第 5 节。
3. MaxBy / MinBy
“取帖子最多的博客”以前要写成 OrderByDescending(...).FirstOrDefaultAsync(),现在可以直接表达意图:
var top = await db.Blogs.MaxByAsync(b => b.Posts.Count());
SELECT "b"."Id", "b"."BlogTypeId", "b"."Name", "b"."OwnerId", "b"."Details_Description", "b"."Details_Language"
FROM "Blogs" AS "b"
ORDER BY (
SELECT COUNT(*)
FROM "Post" AS "p"
WHERE "b"."Id" = "p"."BlogId") DESC
LIMIT 1
生成的 SQL 与手写 OrderByDescending 相同,区别只在可读性。
4. 复杂类型:配置更顺手,可以建键和索引
EF Core 10 中,配置复杂类型里的属性要先拿到复杂类型的构建器:
mb.Entity<Blog>().ComplexProperty(b => b.Details)
.Property(d => d.Description).HasMaxLength(500);
EF Core 11 可以在 lambda 里直接用成员链,也可以在复杂类型的属性上建索引:
mb.Entity<Blog>().ComplexProperty(b => b.Details);
mb.Entity<Blog>().Property(b => b.Details.Description).HasMaxLength(500);
mb.Entity<Blog>().HasIndex(b => b.Details.Language);
GenerateCreateScript() 的输出中多了这一行:
CREATE INDEX "IX_Blogs_Details_Language" ON "Blogs" ("Details_Language");
主键同样可以指向复杂类型里的标量属性,例如强类型 ID:HasKey(c => c.CustomerId.Value)。映射为 JSON 列的复杂集合可以用字符串路径建索引,如 HasIndex("Items[].Sku")。
另外两项补齐了长期以来的限制:
- 复杂类型和 JSON 列现在可以用在 TPT/TPC 继承的实体上。
- Azure Cosmos DB 提供程序完整支持复杂类型,嵌入为文档内的嵌套对象或数组。
EF 团队这个版本修了一批复杂类型的缺陷,目的是让它能够替代 owned entity。还在用 owned entity 映射值对象的项目,可以评估迁移到复杂类型。
5. 保留关系,但不在数据库里建外键
对接老库、做数据同步时,常常需要在模型里保留导航和关系,但数据库中不能有外键约束,比如同步顺序无法保证先写主表。以前只能在每个迁移里手工删掉 AddForeignKey。
EF Core 11 提供了 ExcludeForeignKeyFromMigrations():
mb.Entity<Order>()
.HasOne<Customer>()
.WithMany()
.HasForeignKey(o => o.CustomerId)
.ExcludeForeignKeyFromMigrations();
建表脚本对比:
-- EF Core 10(未排除)
CONSTRAINT "FK_Orders_Customers_CustomerId" FOREIGN KEY ("CustomerId") REFERENCES "Customers" ("Id")
CREATE INDEX "IX_Orders_CustomerId" ON "Orders" ("CustomerId");
-- EF Core 11(已排除)
CREATE INDEX "IX_Orders_CustomerId" ON "Orders" ("CustomerId");
外键约束没有了,外键列上的索引仍然保留。EF 内部的查询、变更跟踪和导航修正照常工作。第 2 节中 CustomerId = 999 的订单就是这样写进去的;同样的数据在 EF Core 10 下会因为 FOREIGN KEY constraint failed 而无法保存。代价也很明显:数据库不再替你保证引用完整性,查询时要准备好处理找不到主表记录的情况。
6. 迁移与 dotnet ef 工具
这部分改的是团队协作和 CI 流程,没有 SQL 可以对比,按用途列出:
- 合并冲突提前暴露:模型快照会记录最新迁移的 ID。两个分支各自加了迁移,合并时快照文件必然冲突,迫使团队处理迁移树的分叉,不会悄悄留下两条并行的迁移链。
- 一步创建并应用:
dotnet ef database update AddProducts --add生成迁移、用 Roslyn 在运行时编译并立即应用,迁移文件照常写入磁盘。适合 Aspire、容器等无法重新编译的场景。 -
配置文件:在
.config/dotnet-ef.json中写好project、startupProject、context等参数,不用每条命令都重复--project ... --startup-project ...。{ "project": "src/App.Infrastructure", "startupProject": "src/App.Api", "context": "AppDbContext" } - 多个 DbContext:
migrations list、migrations script、database update、database drop的--context支持通配符"*"。 migrations remove新增--offline和--connection;PMC 的Add-Migration/Update-Database支持-NoBuild。dotnet ef现在把日志全部写到标准错误,migrations script > deploy.sql得到的就是纯 SQL,不会混入日志。
7. SQL Server 相关
- 默认兼容级别升到 160(SQL Server 2022):
LEAST、GREATEST等翻译默认启用。这是破坏性变更,还在用 SQL Server 2019 或更早版本的项目,要显式写UseCompatibilityLevel(150)。 - 向量检索:可以通过迁移创建向量索引(
HasVectorIndex),并用VectorSearch(...).Take(k).WithApproximate()做近似最近邻检索。SQL Server 端的这项功能仍是实验性的。 - 向量列默认不再加载:查询实体时不再
SELECT向量列,官方基准在 Azure SQL 上提速约 22 倍;需要向量值时显式投影即可。 - 全文检索:
HasFullTextCatalog/HasFullTextIndex可以由迁移创建,新增FreeTextTable/ContainsTable并返回排名,与向量检索结合FullJoin就能实现混合检索。 - JSON:在兼容级别 170(SQL Server 2025)下,基本类型集合的
Contains会翻译为JSON_CONTAINS,可以利用 JSON 索引;新增EF.Functions.JsonContains()、JsonPathExists()。 - 时态表:
PeriodStart/PeriodEnd可以映射到实体的普通属性,不必再写EF.Property<DateTime>(e, "PeriodStart")。 - 新增
DateTimeOffset的若干成员翻译和EF.Functions.DateTrunc()。
8. 升级前要检查的
- 目标框架必须是
net11.0。 - SQL Server 兼容级别默认值变化,见上一节。
- Cosmos DB 提供程序改用 System.Text.Json,移除了每个实体上的
__jObject影子属性,文档中未映射的字段在读写往返后不再保留。如果依赖过这种“透传”行为,要先补上映射。 - Cosmos DB 中投影到
undefined的值现在统一抛InvalidOperationException,需要用IsDefined过滤或用CoalesceUndefined提供默认值。 - 新增分析器会提示把 EF 查询上的
ToAsyncEnumerable改成AsAsyncEnumerable。
完整清单见 EF Core 11 breaking changes。