EF Core 9.0 中的新增功能
EF Core 9 (EF9) 是 EF Core 8 之后的下一版本,计划于 2024 年 11 月发布。
EF9 作为日常版本提供,其中包含所有最新的 EF9 功能和 API 调整。 此处的示例使用这些日常版本。
提示
可通过从 GitHub 下载示例代码来运行和调试示例。 下面每个部分都链接到特定于该部分的源代码。
EF9 面向 .NET 8,因此可与 .NET 8 (LTS) 或 .NET 9 一起使用。
提示
更新了每个预览版的新增功能文档。 已将所有示例设置为使用 EF9 日常版本,与最新预览版相比,这通常需要额外几周时间来完成工作。 强烈建议在测试新功能时使用日常版本,以便不会针对过时位执行测试。
Azure Cosmos DB for NoSQL
EF 9.0 为适用于 Azure Cosmos DB 的 EF Core 提供程序带来了实质性的改进;已重写提供程序的重要部分,以提供新功能、允许新形式的查询,并更好地使提供程序与 Azure Cosmos DB 最佳做法保持一致。 主要的高级改进如下:有关完整列表,请参阅此长篇故事问题。
警告
作为提供程序改进的一部分,必须进行一些影响重大的重大更改:如果要升级现有应用程序,请仔细阅读重大更改部分。
改进了使用分区键和文档 ID 进行查询
存储在 Azure Cosmos DB 数据库中的每个文档都具有唯一的资源 ID。 此外,每个文档可以包含一个“分区键”,用于确定数据的逻辑分区,以便有效地缩放数据库。 有关选择分区键的详细信息,请参阅 Azure Cosmos DB 中的分区和水平缩放。
在 EF 9.0 中,Azure Cosmos DB 提供程序在识别 LINQ 查询中的分区键比较方面明显更好,并将其提取出来,确保查询只发送到相关分区;这可以大大提高查询的性能并降低 RU 费用。 例如:
var sessions = await context.Sessions
.Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
.ToListAsync();
在此查询中,提供程序会自动识别比较 PartitionKey
;如果检查日志,将看到以下内容:
Executed ReadNext (189.8434 ms, 2.8 RU) ActivityId='8cd669ed-2ca5-4f2b-8923-338899071361', Container='test', Partition='["someValue"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE STARTSWITH(c["Username"], "x")
请注意,WHERE
子句不包含 PartitionKey
:该比较已被“取消”,仅用于对相关分区执行查询。 在以前的版本中,在很多情况下,该比较保留在 WHERE
子句中,导致对所有分区执行查询,从而导致成本增加并降低性能。
此外,如果查询还为文档的 ID 属性提供了一个值,并且不包括任何其他查询操作,则提供程序可以应用额外的优化:
var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
.Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
.SingleAsync();
日志显示了此查询的以下内容:
Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'
在这里,根本不发送 SQL 查询。 相反,提供程序会执行一个极其高效的点读取 (ReadItem
API),它直接获取给定分区键和 ID 的文档。 这是可以在 Azure Cosmos DB 中执行的最高效且经济高效的读取类型;有关点读取的详细信息,请参阅 Azure Cosmos DB 文档。
若要了解有关使用分区键和点读取进行查询的详细信息,请参阅查询文档页。
分层分区键
提示
此处显示的代码来自 HierarchicalPartitionKeysSample.cs。
Azure Cosmos DB 最初支持单个分区键,但后来扩展了分区功能,还通过在分区键中指定最多三个层次结构级别的规范来支持子分区。 EF Core 9 为分层分区键提供完全支持,使你能够利用与此功能关联的更好的性能和成本节省。
分区键是使用模型生成 API 指定的,通常位于 DbContext.OnModelCreating。 对于分区键的每个级别,实体类型中必须有一个映射的属性。 例如,考虑 UserSession
实体类型:
public class UserSession
{
// Item ID
public Guid Id { get; set; }
// Partition Key
public string TenantId { get; set; } = null!;
public Guid UserId { get; set; }
public int SessionId { get; set; }
// Other members
public string Username { get; set; } = null!;
}
以下代码使用 TenantId
、UserId
和 SessionId
属性指定三级分区键:
modelBuilder
.Entity<UserSession>()
.HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });
提示
此分区键定义遵循从 Azure Cosmos DB 文档中选择分层分区键中给出的示例。
请注意,从 EF Core 9 开始,任何映射类型的属性都可以在分区键中使用。 对于 bool
和数值类型(如 int SessionId
属性),该值直接在分区键中使用。 其他类型(如 Guid UserId
属性)会自动转换为字符串。
查询时,EF 会自动从查询中提取分区键值,并将其应用于 Azure Cosmos DB 查询 API,以确保查询被适当地限制在尽可能少的分区中。 例如,考虑以下提供层次结构中所有三个分区键值的 LINQ 查询:
var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");
var sessions = await context.Sessions
.Where(
e => e.TenantId == tenantId
&& e.UserId == userId
&& e.SessionId == sessionId
&& e.Username.Contains("a"))
.ToListAsync();
执行此查询时,EF Core 将提取 tenantId
、userId
和 sessionId
参数的值,并将其作为分区键值传递给 Azure Cosmos DB 查询 API。 例如,请参阅执行上述查询的日志:
info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))
请注意,分区键比较已从 WHERE
子句中删除,而是用作高效执行的分区键:["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]
。
有关详细信息,请参阅有关使用分区键进行查询的文档。
显著提高了 LINQ 查询功能
在 EF 9.0 中,Azure Cosmos DB 提供程序的 LINQ 转换功能得到了极大的扩展,该提供程序现在可以执行更多的查询类型。 查询改进的完整列表太长,无法列出,但以下是主要亮点:
- 完全支持 EF 的基元集合,允许对 ints 或字符串等集合执行 LINQ 查询。 有关详细信息,请参阅 EF8 中的新增功能:基元集合。
- 对非基元集合的任意查询的支持。
- 现在支持许多其他 LINQ 运算符:索引到集合、
Length
/Count
、ElementAt
、Contains
和许多其他运算符。 - 对聚合运算符(如
Count
和Sum
)的支持。 - 其他函数转换(有关支持的转换的完整列表,请参阅函数映射文档):
DateTime
和DateTimeOffset
组件成员(DateTime.Year
、DateTimeOffset.Month
…)的转换。EF.Functions.IsDefined
和EF.Functions.CoalesceUndefined
现在允许处理undefined
值。string.Contains
、StartsWith
和EndsWith
现在支持StringComparison.OrdinalIgnoreCase
。
有关查询改进的完整列表,请参阅此问题:
改进的建模符合 Azure Cosmos DB 和 JSON 标准
EF 9.0 以对基于 JSON 的文档数据库更自然的方式映射到 Azure Cosmos DB 文档,并帮助与其他访问文档的系统进行互操作。 尽管这需要重大更改,但存在允许在所有情况下恢复到 9.0 之前行为的 API。
无鉴别器的简化 id
属性
首先,以前版本的 EF 将鉴别器值插入 JSON id
属性,并生成如下文档:
{
"id": "Blog|1099",
...
}
这样做是为了允许不同类型的文档(例如博客和帖子)和相同的键值 (1099) 存在于同一容器分区中。 从 EF 9.0 开始,id
属性仅包含键值:
{
"id": 1099,
...
}
这是一种更自然的映射到 JSON 的方式,使外部工具和系统更容易与 EF 生成的 JSON 文档进行交互;这样的外部系统通常不知道 EF 鉴别器值,默认情况下,EF 鉴别器值来自 .NET 类型。
请注意,这是一项重大更改,因为 EF 将无法再查询旧 id
格式的现有文档。 引入了 API 以恢复到以前的行为。有关更多详细信息,请参阅重大更改说明和 文档。
鉴别器属性已重命名为 $type
默认的鉴别器属性以前命名为 Discriminator
。 EF 9.0 将默认值更改为 $type
:
{
"id": 1099,
"$type": "Blog",
...
}
这遵循了 JSON 多态性的新兴标准,允许与其他工具更好的互操作性。 例如,.NET 的 System.Text.Json 还支持多态性,使用 $type
作为其默认的鉴别器属性名称 (docs)。
请注意,这是一项重大更改,因为 EF 将无法再使用旧的鉴别器属性名查询现有文档。 有关如何恢复到以前的命名的详细信息,请参阅重大更改说明。
矢量相似性搜索(预览版)
Azure Cosmos DB 对矢量相似性搜索的支持现为预览版。 矢量搜索是某些应用程序类型的基本部分,包括 AI、语义搜索等。 Azure Cosmos DB 允许将矢量与其他数据一起直接存储在文档中,这意味着可以对单个数据库执行所有查询。 这可以大大简化体系结构,并消除对堆栈中额外的专用矢量数据库解决方案的需求。 若要了解有关 Azure Cosmos DB 矢量搜索的详细信息,请参阅文档。
正确设置 Azure Cosmos DB 容器后,通过 EF 使用矢量搜索只需添加矢量属性并对其进行配置即可:
public class Blog
{
...
public float[] Vector { get; set; }
}
public class BloggingContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Embeddings)
.IsVector(DistanceFunction.Cosine, dimensions: 1536);
}
}
完成后,使用 LINQ 查询中的 EF.Functions.VectorDistance()
函数执行矢量相似性搜索:
var blogs = await context.Blogs
.OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
.Take(5)
.ToListAsync();
有关详细信息,请参阅有关矢量搜索的文档。
分页支持
Azure Cosmos DB 提供程序现在允许通过延续令牌对查询结果进行分页,这比传统使用 Skip
和 Take
更高效、更具成本效益:
var firstPage = await context.Posts
.OrderBy(p => p.Id)
.ToPageAsync(pageSize: 10, continuationToken: null);
var continuationToken = firstPage.ContinuationToken;
foreach (var post in page.Values)
{
// Display/send the posts to the user
}
新 ToPageAsync
运算符返回 CosmosPage
,它公开一个延续令牌,可用于在稍后有效地恢复查询,获取接下来的 10 个项目:
var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);
有关详细信息,请参阅有关分页的文档部分。
FromSql 实现更安全的 SQL 查询
Azure Cosmos DB 提供程序允许通过 FromSqlRaw 进行 SQL 查询。 但是,当用户提供的数据内插或连接到 SQL 时,该 API 可能容易受到 SQL 注入攻击。 在 EF 9.0 中,现在可以使用新 FromSql
方法,该方法始终将参数化数据作为 SQL 外部的参数进行集成:
var maxAngle = 8;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
.ToListAsync();
有关详细信息,请参阅有关分页的文档部分。
基于角色的访问权限
Azure Cosmos DB for NoSQL 包含内置的基于角色的访问控制 (RBAC) 系统。 EF9 现在支持进行所有数据平面操作。 但是,Azure Cosmos DB SDK 不支持在 Azure Cosmos DB 中执行管理平面操作的 RBAC。 使用 Azure 管理 API,而不是使用 EnsureCreatedAsync
与 RBAC。
默认情况下,同步 I/O 现在被阻止
Azure Cosmos DB for NoSQL 不支持应用程序代码中的同步(阻止)API。 以前,EF 通过在异步调用上阻止来掩盖这一点。 但是,这既鼓励使用同步 I/O,这是一种不好的做法,并且可能会导致死锁。。 因此,从 EF 9 开始,尝试同步访问时会引发异常。 例如:
通过适当配置警告级别,目前仍可使用同步 I/O。 例如,在 DbContext
类型上的 OnConfiguring
中:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));
但请注意,我们计划在 EF 11 中完全删除同步支持,因此请尽快开始更新以使用异步方法,如 ToListAsync
和 SaveChangesAsync
!
AOT 和预编译查询
警告
NativeAOT 和查询预编译是高度实验性的功能,尚不适合生产用途。 下面所述的支持应被视为基础结构,以便使用 EF 10 发布最终功能。 我们鼓励你尝试当前的支持并报告你的体验,但建议在生产环境中部署 EF NativeAOT 应用程序。
EF 9.0 为 .NET NativeAOT 带来了初始的实验性支持,允许发布利用 EF 访问数据库的预先编译的应用程序。 为了支持 NativeAOT 模式下的 LINQ 查询,EF 依赖于 查询预编译:此机制静态标识 EF LINQ 查询并生成 C# 侦听器,其中包含执行每个特定查询的代码。 这可以显著减少应用程序的启动时间,因为每次启动应用程序时,处理和编译 LINQ 查询的繁重都不再发生。 相反,每个查询的拦截器都包含该查询的最终 SQL,以及优化代码,以将数据库结果具体化为 .NET 对象。
例如,给定具有以下 EF 查询的程序:
var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();
EF 将在项目中生成 C# 拦截器,这将接管查询执行。 侦听器将 SQL 嵌入到 SQL 中(在本例中为 SQL Server),这样程序就可以更快地启动它,而不是处理查询并将其转换为 SQL:
var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));
此外,同一侦听器包含用于从数据库结果中具体化 .NET 对象的代码:
var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);
这使用另一个新的 .NET 功能 - 不安全的访问器,将数据从数据库注入到对象的专用字段中。
如果你对 NativeAOT 感兴趣并想要试验尖端功能,请试一试! 请注意,该功能应被视为不稳定,目前存在许多限制:我们希望稳定它,使其更适合 EF 10 中的生产使用情况。
有关更多详细信息, 请参阅 NativeAOT 文档页 。
LINQ 和 SQL 转换
与每个版本一样,EF9 包括对 LINQ 查询功能的大量改进。 新的查询可以被转换,并且许多受支持方案的 SQL 翻译已经得到了改进,以获得更好的性能和可读性。
改进的数量太多了,无法在这里一一列出。 下面重点介绍了一些更重要的改进;有关在 9.0 中完成的工作的更完整列表,请参阅此问题。
我们要感谢 Andrea Canciani (@ranma42) 为优化 EF Core 生成的 SQL 做出的众多高质量贡献!
复杂类型:GroupBy 和 ExecuteUpdate 支持
GroupBy
提示
此处显示的代码来自 ComplexTypesSample.cs。
EF9 支持按复杂类型实例进行分组。 例如:
var groupedAddresses = await context.Stores
.GroupBy(b => b.StoreAddress)
.Select(g => new { g.Key, Count = g.Count() })
.ToListAsync();
EF 将它转换为按复杂类型的每个成员进行分组,这与复杂类型作为值对象的语义相一致。 例如,在 Azure SQL 上:
SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode], COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]
ExecuteUpdate
提示
此处显示的代码来自 ExecuteUpdateSample.cs。
同样,在 EF9 ExecuteUpdate
中也进行了改进,以接受复杂类型属性。 但是,必须显式指定每个复杂类型的成员。 例如:
var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");
await context.Stores
.Where(e => e.Region == "Germany")
.ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, newAddress));
这将生成 SQL,更新映射到复杂类型的每个列:
UPDATE [s]
SET [s].[StoreAddress_City] = @__complex_type_newAddress_0_City,
[s].[StoreAddress_Country] = @__complex_type_newAddress_0_Country,
[s].[StoreAddress_Line1] = @__complex_type_newAddress_0_Line1,
[s].[StoreAddress_Line2] = NULL,
[s].[StoreAddress_PostCode] = @__complex_type_newAddress_0_PostCode
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'
以前,必须在 ExecuteUpdate
调用中手动列出复杂类型的不同属性。
从 SQL 中删除不需要的元素
以前,EF 有时会生成包含实际不需要的元素的 SQL;在大多数情况下,这些可能是在 SQL 处理的早期阶段需要的,并且被抛在了后面。 EF9 现在删除了大多数此类元素,从而使 SQL 更紧凑,在某些情况下更高效。
表删除
作为第一个示例,EF 生成的 SQL 有时包含查询中实际上不需要的表的 JOIN。 请考虑以下模型,该模型使用每个类型一个表 (TPT) 继承映射:
public class Order
{
public int Id { get; set; }
...
public Customer Customer { get; set; }
}
public class DiscountedOrder : Order
{
public double Discount { get; set; }
}
public class Customer
{
public int Id { get; set; }
...
public List<Order> Orders { get; set; }
}
public class BlogContext : DbContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().UseTptMappingStrategy();
}
}
如果我们然后执行以下查询以获取至少有一个订单的所有客户:
var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync();
EF8 生成了以下 SQL:
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id]
WHERE [c].[Id] = [o].[CustomerId])
请注意,尽管查询中没有引用任何列,但它包含了对 DiscountedOrders
表的联接。 EF9 生成一个没有联接的删除后的 SQL:
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
WHERE [c].[Id] = [o].[CustomerId])
投影删除
同样,让我们检查以下查询:
var orders = await context.Orders
.Where(o => o.Amount > 10)
.Take(5)
.CountAsync();
在 EF8 上,此查询生成了以下 SQL:
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) [o].[Id]
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [t]
请注意,子查询中不需要 [o].[Id]
投影,因为外部 SELECT 表达式只是对行进行计数。 EF9 将改为生成以下内容:
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) 1 AS empty
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [s]
...并且投影为空。 这可能看起来不多,但在某些情况下,它可以显著简化 SQL;欢迎你滚动浏览测试中的一些 SQL 更改,以查看效果。
涉及 GREATEST/LEAST 的转换
提示
此处显示的代码来自 LeastGreatestSample.cs。
引入了一些使用 GREATEST
和 LEAST
SQL 函数的新转换。
重要
GREATEST
和 LEAST
函数已在 2022 版本中引入到 SQL Server/Azure SQL 数据库。 Visual Studio 2022 默认安装 SQL Server 2019。 建议安装 SQL Server Developer Edition 2022 以试用 EF9 中的这些新转换。
例如,使用 Math.Max
或 Math.Min
的查询现在分别使用 GREATEST
和 LEAST
针对 Azure SQL 进行转换。 例如:
var walksUsingMin = await context.Walks
.Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
.ToListAsync();
当使用 EF9 针对 SQL Server 2022 执行时,此查询将转换为以下 SQL:
SELECT [w].[Id], [w].[ClosestPubId], [w].[DaysVisited], [w].[Name], [w].[Terrain]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
WHERE LEAST((
SELECT COUNT(*)
FROM OPENJSON([w].[DaysVisited]) AS [d]), (
SELECT COUNT(*)
FROM OPENJSON([p].[Beers]) AS [b])) >
Math.Min
和 Math.Max
也可以用于基元集合的值。 例如:
var pubsInlineMax = await context.Pubs
.SelectMany(e => e.Counts)
.Where(e => Math.Max(e, threshold) > top)
.ToListAsync();
当使用 EF9 针对 SQL Server 2022 执行时,此查询将转换为以下 SQL:
SELECT [c].[value]
FROM [Pubs] AS [p]
CROSS APPLY OPENJSON([p].[Counts]) WITH ([value] int '$') AS [c]
WHERE GREATEST([c].[value], @__threshold_0) > @__top_1
最后,可以使用 RelationalDbFunctionsExtensions.Least
和 RelationalDbFunctionsExtensions.Greatest
直接调用 SQL 中的 Least
或 Greatest
函数。 例如:
var leastCount = await context.Pubs
.Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
.ToListAsync();
当使用 EF9 针对 SQL Server 2022 执行时,此查询将转换为以下 SQL:
SELECT LEAST((
SELECT COUNT(*)
FROM OPENJSON([p].[Counts]) AS [c]), (
SELECT COUNT(*)
FROM OPENJSON([p].[DaysVisited]) AS [d]), (
SELECT COUNT(*)
FROM OPENJSON([p].[Beers]) AS [b]))
FROM [Pubs] AS [p]
强制或防止查询参数化
提示
此处显示的代码来自 QuerySample.cs。
除某些特殊情况外,EF Core 将参数化 LINQ 查询中使用的变量,但在生成的 SQL 中包含常量。 例如,考虑以下查询方法:
async Task<List<Post>> GetPosts(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == id)
.ToListAsync();
使用 Azure SQL 时,这会转换为以下 SQL 和参数:
Executed DbCommand (1ms) [Parameters=[@__id_0='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = @__id_0
请注意,EF 在 SQL 中为“.NET Blog”创建了一个常量,因为该值不会因查询而异。 使用常量允许数据库引擎在创建查询计划时检查该值,这可能可以提高查询的效率。
另一方面,id
的值是参数化的,因为同一查询可能会使用许多不同的 id
值来执行。 在这种情况下创建常量将导致查询缓存受到大量查询的污染,这些查询仅在 id
值上有所不同。 这对于数据库的整体性能非常不利。
一般来说,不应更改这些默认值。 但是,EF Core 8.0.2 引入了一种 EF.Constant
方法,该方法强制 EF 使用常量,即使默认情况下使用参数也是如此。 例如:
async Task<List<Post>> GetPostsForceConstant(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
.ToListAsync();
现在,转换为 id
值包含常量:
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = 1
EF.Parameter
方法
EF9 引入了 EF.Parameter
方法来执行相反的操作。 也就是说,强制 EF 使用参数,即使该值是代码中的常量。 例如:
async Task<List<Post>> GetPostsForceParameter(int id)
=> await context.Posts
.Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
.ToListAsync();
转换现在为“.NET Blog”字符串包含参数:
Executed DbCommand (1ms) [Parameters=[@__p_0='.NET Blog' (Size = 4000), @__id_1='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1
参数化基元集合
EF8 更改了使用基元集合的某些查询的转换方式。 当 LINQ 查询包含参数化基元集合时,EF 将其内容转换为 JSON,并将其作为单个参数值传递给查询:
async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
.ToListAsync();
这将在 SQL Server 上产生以下转换:
Executed DbCommand (5ms) [Parameters=[@__ids_0='[1,2,3]' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (
SELECT [i].[value]
FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)
这允许对不同的参数化集合使用相同的 SQL 查询(只更改参数值);但在某些情况下,由于数据库无法对查询进行最佳规划,这可能会导致性能问题。 EF.Constant
方法可用于还原到以前的转换。
以下查询使用 EF.Constant
来实现这一效果:
async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
=> await context.Posts
.Where(
e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
.ToListAsync();
生成的 SQL 如下所示:
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (1, 2, 3)
此外,EF9 引入了 TranslateParameterizedCollectionsToConstants
上下文选项,可用于防止所有查询的基元集合参数化。 我们还添加了一个补充 TranslateParameterizedCollectionsToParameters
,用于强制显式地对原始集合进行参数化(这是默认行为)。
提示
EF.Parameter
方法替代上下文选项。 如果要防止对大多数查询(但不是全部)对基元集合进行参数化,可以设置上下文选项 TranslateParameterizedCollectionsToConstants
,并对要参数化的查询或单个变量使用 EF.Parameter
。
内联无关子查询
提示
此处显示的代码来自 QuerySample.cs。
在 EF8 中,可将另一个查询中引用的 IQueryable 作为单独的数据库往返执行。 例如,请考虑下面这个 LINQ 查询:
var dotnetPosts = context
.Posts
.Where(p => p.Title.Contains(".NET"));
var results = dotnetPosts
.Where(p => p.Id > 2)
.Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
.Skip(2).Take(10)
.ToArray();
在 EF8 中,对 dotnetPosts
的查询作为一次往返执行,然后将最终结果作为第二次查询执行。 例如,在 SQL Server 上:
SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY
在 EF9 中,dotnetPosts
中的 IQueryable
是内联的,从而产生单一数据库往返:
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata], (
SELECT COUNT(*)
FROM [Posts] AS [p0]
WHERE [p0].[Title] LIKE N'%.NET%')
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
SQL Server 上的子查询和聚合上的聚合函数
EF9 使用由子查询组成的聚合函数或其他聚合函数改进了一些复杂查询的转换。 下面是此类查询的一个示例:
var latestPostsAverageRatingByLanguage = await context.Blogs
.Select(x => new
{
x.Language,
LatestPostRating = x.Posts.OrderByDescending(xx => xx.PublishedOn).FirstOrDefault()!.Rating
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.LatestPostRating))
.ToListAsync();
首先,Select
为每个 Post
计算 LatestPostRating
,这在转换为 SQL 时需要一个子查询。 稍后在查询中使用 Average
操作聚合这些结果。 在 SQL Server 上运行时,生成的 SQL 如下所示:
SELECT AVG([s].[Rating])
FROM [Blogs] AS [b]
OUTER APPLY (
SELECT TOP(1) [p].[Rating]
FROM [Posts] AS [p]
WHERE [b].[Id] = [p].[BlogId]
ORDER BY [p].[PublishedOn] DESC
) AS [s]
GROUP BY [b].[Language]
在以前的版本中,EF Core 将为类似的查询生成无效的 SQL,试图直接在子查询上应用聚合操作。 这在 SQL Server 上是不允许的,并会导致异常。 同样的原则也适用于使用聚合对另一个聚合的查询:
var topRatedPostsAverageRatingByLanguage = await context.Blogs.
Select(x => new
{
x.Language,
TopRating = x.Posts.Max(x => x.Rating)
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.TopRating))
.ToListAsync();
注意
此更改不会影响 Sqlite,它支持通过子查询(或其他聚合)进行聚合,并且不支持 LATERAL JOIN
(APPLY
)。 下面是在 Sqlite 上运行的第一个查询的 SQL:
SELECT ef_avg((
SELECT "p"."Rating"
FROM "Posts" AS "p"
WHERE "b"."Id" = "p"."BlogId"
ORDER BY "p"."PublishedOn" DESC
LIMIT 1))
FROM "Blogs" AS "b"
GROUP BY "b"."Language"
优化了使用 Count != 0 的查询
提示
此处显示的代码来自 QuerySample.cs。
在 EF8 中,以下 LINQ 查询已转换为使用 SQL COUNT
函数:
var blogsWithPost = await context.Blogs
.Where(b => b.Posts.Count > 0)
.ToListAsync();
EF9 现在使用 EXISTS
生成更高效的转换:
SELECT "b"."Id", "b"."Name", "b"."SiteUri"
FROM "Blogs" AS "b"
WHERE EXISTS (
SELECT 1
FROM "Posts" AS "p"
WHERE "b"."Id" = "p"."BlogId")
C# 对可为 null 值进行比较操作的语义
在 EF8 中,对于某些方案,可以为 null 的元素之间的比较没有正确执行。 在 C# 中,如果一个或两个操作数都为 null,则比较操作的结果为 false;否则,将比较操作数的包含值。 在 EF8 中,我们使用数据库 null 语义来转换比较。 这将产生与使用 LINQ to Objects 的类似查询不同的结果。 此外,在筛选器与投影中完成比较时,我们将产生不同的结果。 某些查询还会在 Sql Server 和 Sqlite/Postgres 之间产生差异结果。
例如,查询:
var negatedNullableComparisonFilter = await context.Entities
.Where(x => !(x.NullableIntOne > x.NullableIntTwo))
.Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();
将生成以下 SQL:
SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])
其中筛选出其 NullableIntOne
或 NullableIntTwo
设置为 null 的实体。
在 EF9 中,我们生成:
SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE CASE
WHEN [e].[NullableIntOne] > [e].[NullableIntTwo] THEN CAST(0 AS bit)
ELSE CAST(1 AS bit)
END = CAST(1 AS bit)
在投影中进行了类似的比较:
var negatedNullableComparisonProjection = await context.Entities.Select(x => new
{
x.NullableIntOne,
x.NullableIntTwo,
Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();
结果为以下 SQL:
SELECT [e].[NullableIntOne], [e].[NullableIntTwo], CASE
WHEN NOT ([e].[NullableIntOne] > [e].[NullableIntTwo]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [Operation]
FROM [Entities] AS [e]
对于 NullableIntOne
或 NullableIntTwo
设置为 null 的实体(而不是 C# 中预期的 true
),它返回 false
。 在 Sqlite 上运行相同的方案:
SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne" > "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"
这会导致 Nullable object must have a value
异常,因为对于 NullableIntOne
或 NullableIntTwo
为 null 的情况,转换会产生 null
值。
EF9 现在可正确处理这些方案,从而产生与 LINQ to Objects 一致且跨不同提供程序的结果。
此增强功能由 @ranma42 提供。 非常感谢!
Order
和 OrderDescending
LINQ 运算符的转换
EF9 支持 LINQ 简化排序操作(Order
和 OrderDescending
)的转换。 它们的作用类似于 OrderBy
/OrderByDescending
,但不需要参数。 相反,它们会应用默认排序 - 对于实体,这意味着基于主键值进行排序,对于其他类型,则基于值本身进行排序。
下面是利用简化排序运算符的示例查询:
var orderOperation = await context.Blogs
.Order()
.Select(x => new
{
x.Name,
OrderedPosts = x.Posts.OrderDescending().ToList(),
OrderedTitles = x.Posts.Select(xx => xx.Title).Order().ToList()
})
.ToListAsync();
此查询等效于以下内容:
var orderByEquivalent = await context.Blogs
.OrderBy(x => x.Id)
.Select(x => new
{
x.Name,
OrderedPosts = x.Posts.OrderByDescending(xx => xx.Id).ToList(),
OrderedTitles = x.Posts.Select(xx => xx.Title).OrderBy(xx => xx).ToList()
})
.ToListAsync();
并生成以下 SQL:
SELECT [b].[Name], [b].[Id], [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata], [p0].[Title], [p0].[Id]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Posts] AS [p0] ON [b].[Id] = [p0].[BlogId]
ORDER BY [b].[Id], [p].[Id] DESC, [p0].[Title]
注意
Order
和 OrderDescending
方法仅支持实体、复杂类型或标量的集合,它们不适用于更复杂的投影,例如包含多个属性的匿名类型的集合。
这一改进是由 EF 团队的校友 @bricelam 做出的。 非常感谢!
改进了逻辑非运算符 (!) 的转换
EF9 围绕 SQL CASE/WHEN
、COALESCE
、否定和各种其他构造带来了许多优化;其中大部分是由 Andrea Canciani (@ranma42) 提供的——非常感谢做出的所有贡献! 下面,我们将详细介绍围绕逻辑非的一些优化。
让我们检查以下查询:
var negatedContainsSimplification = await context.Posts
.Where(p => !p.Content.Contains("Announcing"))
.Select(p => new { p.Content }).ToListAsync();
在 EF8 中,我们将生成以下 SQL:
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)
在 EF9 中,我们将“推送”NOT
操作纳入比较:
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0
另一个适用于 SQL Server 的示例是否定的条件操作。
var caseSimplification = await context.Blogs
.Select(b => !(b.Id > 5 ? false : true))
.ToListAsync();
在 EF8 中,用于生成嵌套 CASE
块:
SELECT CASE
WHEN CASE
WHEN [b].[Id] > 5 THEN CAST(0 AS bit)
ELSE CAST(1 AS bit)
END = CAST(0 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
在 EF9 中,我们删除了嵌套:
SELECT CASE
WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
在 SQL Server 上,投影否定的布尔属性时:
var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();
EF8 将生成 CASE
块,因为比较不能直接显示在 SQL Server 查询中的投影中:
SELECT [p].[Title], CASE
WHEN [p].[Archived] = CAST(0 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [Active]
FROM [Posts] AS [p]
在 EF9 中,此转换已简化,现在使用位非运算 (~
):
SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]
更好地支持 Azure SQL 和 Azure Synapse
EF9 在指定目标 SQL Server 的类型时提供了更大的灵活性。 现在可以指定 UseAzureSql
或 UseAzureSynapse
,而不是使用 UseSqlServer
配置 EF。
这允许 EF 在使用 Azure SQL 或 Azure Synapse 时生成更好的 SQL。 EF 可以利用数据库特定的功能(例如 Azure SQL 上的 JSON 专用类型),或者绕过其限制(例如,ESCAPE
子句在 Azure Synapse 上使用 LIKE
时不可用)。
其他查询改进
- EF8 中引入的基元集合查询支持已扩展,以支持所有
ICollection<T>
类型。 请注意,这仅适用于参数和内联集合 - 作为实体一部分的基元集合仍然仅限于数组、列表和 EF9 中的只读数组/列表。 - 新
ToHashSetAsync
函数将查询结果作为HashSet
返回(#30033,由 @wertzui 提供)。 TimeOnly.FromDateTime
和FromTimeSpan
现在已在 SQL Server 上转换 (#33678)。ToString
over enums 现在已转换(#33706,由 @Danevandy99 提供)。string.Join
现在在 SQL Server 的非聚合上下文中转换为 CONCAT_WS (#28899)。EF.Functions.PatIndex
现在转换为 SQL ServerPATINDEX
函数,该函数返回模式第一次出现的起始位置(#33702、@smnsht)。Sum
和Average
现在在 SQLite 上支持小数(#33721,由 @ranma42 提供)。- 修复和优化了
string.StartsWith
和EndsWith
(#31482)。 Convert.To*
方法现在可以接受类型object
的参数(#33891,由 @imangd 提供)。- 异或 (XOR) 操作现在在 SQL Server 上转换(#34071,由 @ranma42 提供)。
- 围绕
COLLATE
和AT TIME ZONE
操作的 Null 性进行优化(#34263,由 @ranma42 提供)。 - 通过
IN
、EXISTS
和集合操作对DISTINCT
的优化(#34381,由 @ranma42 提供)。
以上只是 EF9 中一些更重要的查询改进;有关更完整的列表,请参阅此问题。
迁移
防止并发迁移
EF9 引入了一种锁定机制,以防止同时执行多个迁移,因为这可能会使数据库处于损坏状态。 当使用推荐的方法将迁移部署到生产环境时,不会发生这种情况;但如果在运行时使用 DbContext.Database.Migrate()
方法应用迁移,则会发生这种情况。 我们建议在部署时应用迁移,而不是作为应用程序启动的一部分,但这可能会导致更复杂的应用程序体系结构(例如 ,使用 .NET Aspire 项目时)。
注意
如果使用的是 Sqlite 数据库,请参阅与此功能相关的潜在问题。
在事务中无法运行多个迁移操作时发出警告
迁移期间执行的大多数操作都受事务保护。 这确保了如果由于某种原因迁移失败,数据库不会最终处于损坏状态。 但是,有些操作没有封装在事务中(例如,SQL Server 内存优化表上的操作,或修改数据库排序规则等数据库更改操作)。 为了避免在迁移失败时损坏数据库,建议使用单独的迁移以隔离方式执行这些操作。 EF9 现在检测到迁移包含多个操作的情况,其中一个操作无法打包在事务中,并发出警告。
改进了数据种子设定
EF9 引入了一种执行数据种子设定的便捷方法,即用初始数据填充数据库。 DbContextOptionsBuilder
现在包含 UseSeeding
和 UseAsyncSeeding
方法,这些方法在 DbContext 初始化时执行(作为 EnsureCreatedAsync
的一部分)。
注意
如果应用程序以前运行过,则数据库可能已包含示例数据(这些数据将在上下文的第一次初始化时添加)。 因此,在尝试填充数据库之前,UseSeeding
UseAsyncSeeding
应检查数据是否存在。 这可以通过发出简单的 EF 查询来实现。
下面是如何使用这些方法的示例:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
.UseSeeding((context, _) =>
{
var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
context.SaveChanges();
}
})
.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
await context.SaveChangesAsync(cancellationToken);
}
});
可在此处找到详细信息。
其他迁移改进
- 将现有表更改为 SQL Server 临时表时,迁移代码大小已显著减少。
模型构建
自动编译的模型
提示
此处显示的代码来自 NewInEFCore9.CompiledModels 示例。
编译的模型可以改善大型模型(即实体类型计数在 100 个或 1000 个以上)的应用程序启动时间。 在以前版本的 EF Core 中,必须使用命令行手动生成编译的模型。 例如:
dotnet ef dbcontext optimize
运行该命令后,必须将类似于 .UseModel(MyCompiledModels.BlogsContextModel.Instance)
的代码行添加到 OnConfiguring
,以便让 EF Core 使用编译的模型。
从 EF9 开始,当应用程序的 DbContext
类型与编译的模型位于同一项目/程序集中时,不再需要此 .UseModel
行。 相反,将自动检测和使用编译的模型。 只要让 EF 在生成模型时记录日志,就可以看到这一点。 运行一个简单的应用程序,然后在应用程序启动时显示生成模型的 EF:
Starting application...
>> EF is building the model...
Model loaded with 2 entity types.
在模型项目上运行 dotnet ef dbcontext optimize
的输出为:
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> dotnet ef dbcontext optimize
Build succeeded in 0.3s
Build succeeded in 0.3s
Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered automatically, but you can also call 'options.UseModel(BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model>
请注意,日志输出指示模型在命令运行时生成。 如果现在再次运行应用程序,则在不进行任何代码更改的情况下重新生成后,输出为:
Starting application...
Model loaded with 2 entity types.
请注意,启动应用程序时未生成模型,因为自动检测并使用了编译的模型。
MSBuild 集成
使用上述方法时,在更改实体类型或 DbContext
配置时,仍需手动重新生成编译的模型。 但是,EF9 附带一个 MSBuild 任务包,可在生成模型项目时自动更新已编译的模型! 若要开始,请安装 Microsoft.EntityFrameworkCore.Tasks NuGet 包。 例如:
dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0
提示
使用上述命令中与你使用的 EF Core 版本匹配的包版本。
然后,通过在文件中设置 EFOptimizeContext
和 EFScaffoldModelStage
属性 .csproj
来启用集成。 例如:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFScaffoldModelStage>build</EFScaffoldModelStage>
</PropertyGroup>
现在,如果生成项目,我们可以在生成时看到日志记录,它指示正在生成编译的模型:
Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
--additionalprobingpath G:\packages
--additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages"
--runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\
--namespace NewInEfCore9
--suffix .g
--assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll
--project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model
--root-namespace NewInEfCore9
--language C#
--nullable
--working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App
--verbose
--no-color
--prefix-output
运行应用程序时会显示已检测到编译的模型,因此该模型不会再次生成:
Starting application...
Model loaded with 2 entity types.
现在,只要模型发生更改,编译的模型就会在项目生成后立即自动重新生成。
有关详细信息,请参阅 MSBuild 集成。
只读基元集合
提示
此处显示的代码来自 PrimitiveCollectionsSample.cs。
EF8 引入了对映射数组和基元类型的可变列表的支持。 这已在 EF9 中扩展,以包含只读集合/列表。 具体而言,EF9 支持类型化为 IReadOnlyList
、IReadOnlyCollection
或 ReadOnlyCollection
的集合。 例如,在以下代码中,DaysVisited
将按约定映射为日期的基元集合:
public class DogWalk
{
public int Id { get; set; }
public string Name { get; set; }
public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}
如果需要,只读集合可以由普通可变集合提供支持。 例如,在以下代码中,DaysVisited
可以映射为日期的基元集合,同时仍允许类中的代码操作基础列表。
public class Pub
{
public int Id { get; set; }
public string Name { get; set; }
public IReadOnlyCollection<string> Beers { get; set; }
private List<DateOnly> _daysVisited = new();
public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
}
然后,可以按正常方式在查询中使用这些集合。 例如,使用 SQL Server 时,以下 LINQ 查询:
var walksWithADrink = await context.Walks.Select(
w => new
{
WalkName = w.Name,
PubName = w.ClosestPub.Name,
Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
TotalCount = w.DaysVisited.Count
}).ToListAsync();
这会转换为 SQLite 上的以下 SQL:
SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (
SELECT COUNT(*)
FROM json_each("w"."DaysVisited") AS "d"
WHERE "d"."value" IN (
SELECT "d0"."value"
FROM json_each("p"."DaysVisited") AS "d0"
)) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"
指定键和索引的填充因子
提示
此处显示的代码来自 ModelBuildingSample.cs。
EF9 支持使用 EF Core 迁移创建密钥和索引时 SQL Server 填充因子的规范。 在 SQL Server 文档中,“创建或重新生成索引时,填充因子值确定了要使用数据填充的每个叶级页面上空间的百分比,从而将每个页面上的剩余空间保留为将来增长的可用空间。”
可以在单个或复合主键和备用键与索引上设置填充因子。 例如:
modelBuilder.Entity<User>()
.HasKey(e => e.Id)
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasAlternateKey(e => new { e.Region, e.Ssn })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Name })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Region, e.Tag })
.HasFillFactor(80);
应用于现有表时,这会将表更改为约束的填充因子:
ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];
ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];
ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80);
这种增强功能是由 @deano-hunter 贡献的。 非常感谢!
使现有的模型构建约定更具可扩展性
提示
此处显示的代码来自 CustomConventionsSample.cs。
应用程序的公共模型构建约定是在 EF7 中引入的。 在 EF9 中,我们简化了一些现有约定的扩展。 例如,在 EF7 中按特性映射属性的代码如下:
public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
: base(dependencies)
{
}
public override void ProcessEntityTypeAdded(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionContext<IConventionEntityTypeBuilder> context)
=> Process(entityTypeBuilder);
public override void ProcessEntityTypeBaseTypeChanged(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionEntityType? newBaseType,
IConventionEntityType? oldBaseType,
IConventionContext<IConventionEntityType> context)
{
if ((newBaseType == null
|| oldBaseType != null)
&& entityTypeBuilder.Metadata.BaseType == newBaseType)
{
Process(entityTypeBuilder);
}
}
private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
{
foreach (var memberInfo in GetRuntimeMembers())
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
{
entityTypeBuilder.Property(memberInfo);
}
else if (memberInfo is PropertyInfo propertyInfo
&& Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
{
entityTypeBuilder.Ignore(propertyInfo.Name);
}
}
IEnumerable<MemberInfo> GetRuntimeMembers()
{
var clrType = entityTypeBuilder.Metadata.ClrType;
foreach (var property in clrType.GetRuntimeProperties()
.Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
{
yield return property;
}
foreach (var property in clrType.GetRuntimeFields())
{
yield return property;
}
}
}
}
在 EF9 中,这可以简化为以下内容:
public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
: PropertyDiscoveryConvention(dependencies)
{
protected override bool IsCandidatePrimitiveProperty(
MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping)
{
if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping))
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
{
return true;
}
structuralType.Builder.Ignore(memberInfo.Name);
}
mapping = null;
return false;
}
}
更新 ApplyConfigurationsFromAssembly 以调用非公共构造函数
在 EF Core 的早期版本中,ApplyConfigurationsFromAssembly
方法仅使用公共无参数构造函数实例化配置类型。 在 EF9 中,我们改进了失败时生成的错误消息,并且还启用了非公共构造函数的实例化。 当在私有嵌套类中共同定位配置时,这非常有用,而该私有嵌套类永远不应由应用程序代码实例化。 例如:
public class Country
{
public int Code { get; set; }
public required string Name { get; set; }
private class FooConfiguration : IEntityTypeConfiguration<Country>
{
private FooConfiguration()
{
}
public void Configure(EntityTypeBuilder<Country> builder)
{
builder.HasKey(e => e.Code);
}
}
}
顺便说一句,有些人认为这种模式令人厌恶,因为它将实体类型与配置耦合在一起。 其他人则认为它非常有用,因为它将配置与实体类型放在一起。 我们不要在这里讨论这个问题。 :-)
SQL Server HierarchyId
提示
此处显示的代码来自 HierarchyIdSample.cs。
用于 HierarchyId 路径生成的简便方法
对 SQL Server HierarchyId
类型的第一类支持是在 EF8 中添加的。 在 EF9 中,添加了一种简便方法,以便更轻松地在树结构中创建新的子节点。 例如,以下代码会查询具有 HierarchyId
属性的现有实体:
var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");
然后,可将此 HierarchyId
属性用于创建子节点,而无需任何显式字符串操作。 例如:
var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");
如果 daisy
的 HierarchyId
为 /4/1/3/1/
,则 child1
会获取 HierarchyId
“/4/1/3/1/1/”,child2
会获取 HierarchyId
“/4/1/3/1/2/”。
要在这两个子级之间创建节点,可以使用其他子级别。 例如:
var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");
这会创建一个 HierarchyId
为 /4/1/3/1/1.5/
的节点,并将其置于 child1
和 child2
之间。
这种增强功能是由 @Rezakazemi890 贡献的。 非常感谢!
工具
减少重新生成次数
默认情况下,dotnet ef
命令行工具会在执行该工具之前会生成项目。 这是因为在出现故障的情况下,在运行该工具之前不进行重新生成会是一种常见的混淆源。 经验丰富的开发人员可以使用 --no-build
选项来避免这种可能会非常缓慢的生成。 但即使 --no-build
选项也可能导致下次在 EF 工具外部生成项目时重新生成该项目。
我们认为 @Suchiman 的社区贡献已经解决了这一问题。 但我们也意识到,围绕 MSBuild 行为的进行的调整往往会产生意外后果,因此我们要求像你这样的人尝试此操作,并报告你的任何负面体验。