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 提供程序带来了实质性的改进;已重写提供程序的重要部分,以提供新功能、允许新形式的查询,并更好地使提供程序与 Cosmos DB 最佳做法保持一致。 主要的高级改进如下:有关完整列表,请参阅此长篇故事问题。
警告
作为提供程序改进的一部分,必须进行一些影响重大的重大更改:如果要升级现有应用程序,请仔细阅读重大更改部分。
改进了使用分区键和文档 ID 进行查询
存储在 Cosmos 数据库中的每个文档都具有唯一的资源 ID。 此外,每个文档可以包含一个“分区键”,用于确定数据的逻辑分区,以便有效地缩放数据库。 有关选择分区键的详细信息,请参阅 Azure Cosmos DB 中的分区和水平缩放。
在 EF 9.0 中,Cosmos DB 提供程序在识别 LINQ 查询中的分区键比较方面明显更好,并将其提取出来,使查询只发送到相关分区;这可以大大提高查询的性能并降低成本。 例如:
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 的文档。 这是可以在 Cosmos DB 中执行的最高效且经济高效的读取类型;有关点读取的详细信息,请参阅 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 会自动从查询中提取分区键值,并将其应用于 Cosmos 查询 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
参数的值,并将其作为分区键值传递给 Cosmos 查询 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 中,Cosmos DB 提供程序的 LINQ 转换功能得到了极大的扩展,该提供程序现在可以执行更多的查询类型。 查询改进的完整列表太长,无法列出,但以下是主要亮点:
- Cosmos 提供程序现在完全支持 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
。
- 已添加对
有关查询改进的完整列表,请参阅此问题:
改进的建模符合 Cosmos 和 JSON 标准
EF 9.0 以对基于 JSON 的文档数据库更自然的方式映射到 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、语义搜索等。 Cosmos DB 对矢量搜索的支持允许在单个数据库中存储数据和矢量,并执行查询,这可以大大简化体系结构,并消除在堆栈中额外使用专用矢量数据库解决方案的需要。 若要了解有关 Cosmos DB 矢量搜索的详细信息,请参阅文档。
正确设置 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();
有关详细信息,请参阅有关矢量搜索的文档。
分页支持
Cosmos DB 提供程序现在允许通过延续令牌对查询结果进行分页,这比传统使用 Skip
和 Take
更高效、更具成本效益:
var firstPage = await context.Posts
.OrderBy(p => p.Id)
.ToPageAsync(pageSize: 10, continuationToken: null);
var continuationToken = page.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 查询
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 现在支持通过它对容器进行管理和使用。 应用程序代码无需更改。 有关详细信息,请参阅问题 #32197。
默认情况下,同步 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 和预编译查询
如简介中所述,我们在幕后进行了大量工作,让 EF Core 能够在没有实时 (JIT) 编译的情况下运行。 相反,EF 会预先 (AOT) 编译运行应用程序中的查询所需的所有内容。 此 AOT 编译和相关的处理将作为生成和发布应用程序的一部分进行。 目前在 EF9 版本中,没有太多功能可供像你这样的应用开发人员使用。 但是,如果感兴趣的话,EF9 中支持 AOT 和预编译查询的已完成问题包括:
- 编译的模型:对属性和字段使用静态绑定而不是反射
- 编译的模型:生成用于更改跟踪的 Lambda
- 使更改跟踪和更新管道与 AOT/剪裁兼容
- 使用拦截器将查询重定向到预编译代码
- 使所有 SQL 表达式节点都可引用
- 生成期间生成编译的模型
- 自动发现编译的模型
- 使 ParameterExtractingExpressionVisitor 能够将路径提取到树中可评估的片段
- 在编译的模型中生成表达式树(查询筛选器、值转换器)
- 使 LinqToCSharpSyntaxTranslator 对嵌套范围中同一变量的多个声明更具复原力
- 优化 ParameterExtractingExpressionVisitor
在此处查看有关如何在体验结合时使用预编译查询的示例。
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
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
内联无关子查询
提示
此处显示的代码来自 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
优化了使用 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 提供。 非常感谢!
改进了逻辑非运算符 (!) 的转换
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] ^ CAST(1 AS bit) AS [Active]
FROM [Posts] AS [p]
其他查询改进
- 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 提供)。
以上只是 EF9 中一些更重要的查询改进;有关更完整的列表,请参阅此问题。
迁移
改进了临时表迁移
对于 EF9,将现有表更改为临时表时创建的迁移已缩小。 例如,在 EF8 中,将单个现有表设为临时表会导致以下迁移:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterTable(
name: "Blogs")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AlterColumn<string>(
name: "SiteUri",
table: "Blogs",
type: "nvarchar(max)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Blogs",
type: "nvarchar(max)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "Blogs",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("SqlServer:Identity", "1, 1")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart")
.OldAnnotation("SqlServer:Identity", "1, 1");
migrationBuilder.AddColumn<DateTime>(
name: "PeriodEnd",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AddColumn<DateTime>(
name: "PeriodStart",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
}
在 EF9 中,相同的操作现在会导致更小的迁移:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterTable(
name: "Blogs")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AddColumn<DateTime>(
name: "PeriodEnd",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:TemporalIsPeriodEndColumn", true);
migrationBuilder.AddColumn<DateTime>(
name: "PeriodStart",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:TemporalIsPeriodStartColumn", true);
}
模型构建
自动编译的模型
提示
此处显示的代码来自 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-preview.4.24205.3
提示
使用上述命令中与你使用的 EF Core 版本匹配的包版本。
然后,通过将 EFOptimizeContext
属性设置为 .csproj
文件来启用集成。 例如:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
</PropertyGroup>
还有一些额外的可选 MSBuild 属性用于控制模型的生成方式,相当于通过命令行向 dotnet ef dbcontext optimize
传递的选项。 这些设置包括:
MSBuild 属性 | 说明 |
---|---|
EFOptimizeContext | 设置为 true 以启用自动编译的模型。 |
DbContextName | 要使用的 DbContext 类。 仅类名或完全限定命名的空间。 如果省略此选项,EF Core 将查找上下文类。 如果有多个上下文类,则此选项是必需的。 |
EFStartupProject | 启动项目的相对路径。 默认值是当前文件夹。 |
EFTargetNamespace | 要用于所有生成的类的命名空间。 默认设置为从根命名空间和输出目录以及 CompiledModels 生成。 |
在我们的示例中,我们需要指定启动项目:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFStartupProject>..\App\App.csproj</EFStartupProject>
</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 --startup-assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.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.
现在,只要模型发生更改,编译的模型就会在项目生成后立即自动重新生成。
[注意!] 我们正在处理对 EF8 和 EF9 中的编译模型的更改中的一些性能问题。 有关详细信息,请参阅问题 33483#。
只读基元集合
提示
此处显示的代码来自 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 行为的进行的调整往往会产生意外后果,因此我们要求像你这样的人尝试此操作,并报告你的任何负面体验。