EF Core 9.0 中的新增功能

EF Core 9 (EF9) 是 EF Core 8 之后的下一版本,计划于 2024 年 11 月发布。 有关详细信息,请参阅规划 Entity Framework Core 9

EF9 作为日常版本提供,其中包含所有最新的 EF9 功能和 API 调整。 此处的示例使用这些日常版本。

提示

可通过从 GitHub 下载示例代码来运行和调试示例。 下面每个部分都链接到特定于该部分的源代码。

EF9 面向 .NET 8,因此可与 .NET 8 (LTS).NET 9 预览版一起使用。

提示

更新了每个预览版的新增功能文档。 已将所有示例设置为使用 EF9 日常版本,与最新预览版相比,这通常需要额外几周时间来完成工作。 强烈建议在测试新功能时使用日常版本,以便不会针对过时位执行测试。

Azure Cosmos DB for NoSQL

我们正在研究对适用于 Azure Cosmos DB for NoSQL 的 EF Core 数据库提供程序进行 EF9 中的重大更新。

基于角色的访问权限

Azure Cosmos DB for NoSQL 包含内置的基于角色的访问控制 (RBAC) 系统。 EF9 现在支持通过它对容器进行管理和使用。 应用程序代码无需更改。 有关详细信息,请参阅问题 #32197

默认情况下阻止同步访问

提示

此处显示的代码来自 CosmosSyncApisSample.cs

Azure Cosmos DB for NoSQL 不支持从应用程序代码进行同步(阻止)访问。 以前,EF 默认通过对异步调用进行阻止来过滤它。 但是,这同时鼓励了同步的使用(不好的做法),并可能导致死锁。 因此,从 EF9 开始,尝试同步访问时会引发异常。 例如:

System.InvalidOperationException: An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.SyncNotSupported':
 Azure Cosmos DB does not support synchronous I/O. Make sure to use and correctly await only async methods when using
 Entity Framework Core to access Azure Cosmos DB. See https://aka.ms/ef-cosmos-nosync for more information.
 This exception can be suppressed or logged by passing event ID 'CosmosEventId.SyncNotSupported' to the 'ConfigureWarnings'
 method in 'DbContext.OnConfiguring' or 'AddDbContext'.
   at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, Exception exception)
   at Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal.CosmosLoggerExtensions.SyncNotSupported(IDiagnosticsLogger`1 diagnostics)
   at Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal.CosmosClientWrapper.DeleteDatabase()
   at Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal.CosmosDatabaseCreator.EnsureDeleted()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureDeleted()

如异常所示,目前仍可通过适当配置警告等级来使用同步访问。 例如,在 DbContext 类型上的 OnConfiguring 中:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));

但是,请注意,我们计划完全移除 EF11 中的同步支持,因此请尽快开始更新以使用异步方法,如 ToListAsyncSaveChangesAsync

增强基元集合

提示

此处显示的代码来自 CosmosPrimitiveTypesSample.cs

自 EF Core 6 以来,Cosmos DB 提供程序以有限的形式支持基元集合。 此支持正在 EF9 中增强,从合并文档数据库中基元集合的元数据和 API 表面开始,以与关系数据库中的基元集合保持一致。 这意味着,现在可以使用模型生成 API 显式映射基元集合,从而实现配置元素类型的方面。 例如,若要映射所需(即非 null)字符串的列表:

modelBuilder.Entity<Book>()
    .PrimitiveCollection(e => e.Quotes)
    .ElementType(b => b.IsRequired());

有关模型生成 API 的详细信息,请参阅 EF8 中的新增功能:基元集合

AOT 和预编译查询

如简介中所述,我们在幕后进行了大量工作,让 EF Core 能够在没有实时 (JIT) 编译的情况下运行。 相反,EF 会预先 (AOT) 编译运行应用程序中的查询所需的所有内容。 此 AOT 编译和相关的处理将作为生成和发布应用程序的一部分进行。 目前在 EF9 版本中,没有太多功能可供像你这样的应用开发人员使用。 但是,如果感兴趣的话,EF9 中支持 AOT 和预编译查询的已完成问题包括:

在此处查看有关如何在体验结合时使用预编译查询的示例。

LINQ 和 SQL 转换

团队正在研究 EF Core 9 中查询管道的一些重大体系结构更改,作为我们对 JSON 映射和文档数据库不断改进的一部分。 这意味着我们需要让像你这样的人在这些新的内部组件上运行代码。 (如果你正在阅读该版本中的“新增功能”文档,那么你是真正参与社区的一员;谢谢!)我们有超过 120,000 个测试,但这还不够! 我们需要你,在我们的平台上运行真实代码的用户,以便发现问题并发布可靠的版本!

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]

删除传递给 OPENJSON WITH 子句的列

提示

此处显示的代码来自 JsonColumnsSample.cs

EF9 在调用 OPENJSON WITH 时会移除不必要的列。 例如,考虑一个使用谓词从 JSON 集合获取计数的查询:

var postsUpdatedOn = await context.Posts
    .Where(p => p.Metadata!.Updates.Count(e => e.UpdatedOn >= date) == 1)
    .ToListAsync();

在 EF8 中,使用 Azure SQL 数据库提供程序时,此查询会生成以下 SQL:

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 (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
        [PostedFrom] nvarchar(45) '$.PostedFrom',
        [UpdatedBy] nvarchar(max) '$.UpdatedBy',
        [UpdatedOn] date '$.UpdatedOn',
        [Commits] nvarchar(max) '$.Commits' AS JSON
    ) AS [u]
    WHERE [u].[UpdatedOn] >= @__date_0) = 1

请注意,此查询中不需要 UpdatedByCommits。 从 EF9 开始,这些列现已被删除:

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 (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
        [PostedFrom] nvarchar(45) '$.PostedFrom',
        [UpdatedOn] date '$.UpdatedOn'
    ) AS [u]
    WHERE [u].[UpdatedOn] >= @__date_0) = 1

在某些情况下,这会导致 WITH 子句被完全移除。 例如:

var tagsWithCount = await context.Tags.Where(p => p.Text.Length == 1).ToListAsync();

在 EF8 中,此查询转换为以下 SQL:

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) WITH ([value] nvarchar(max) '$') AS [t0]) = 1

在 EF9 中,已改进为:

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) AS [t0]) = 1

涉及 GREATEST/LEAST 的转换

提示

此处显示的代码来自 LeastGreatestSample.cs

引入了一些使用 GREATESTLEAST SQL 函数的新转换。

重要

GREATESTLEAST 函数已在 2022 版本中引入到 SQL Server/Azure SQL 数据库。 Visual Studio 2022 默认安装 SQL Server 2019。 建议安装 SQL Server Developer Edition 2022 以试用 EF9 中的这些新转换。

例如,使用 Math.MaxMath.Min 的查询现在分别使用 GREATESTLEAST 针对 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.MinMath.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.LeastRelationalDbFunctionsExtensions.Greatest 直接调用 SQL 中的 LeastGreatest 函数。 例如:

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 和参数:

info: 2/5/2024 15:43:13.789 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      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 值来执行。 在这种情况下创建常量会导致查询缓存受到大量仅参数值不同的查询的污染。 这对于数据库的整体性能非常不利。

一般来说,不应更改这些默认值。 但是,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 值包含常量:

info: 2/5/2024 15:43:13.812 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      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”字符串包含参数:

info: 2/5/2024 15:43:13.803 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      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

新的 ToHashSetAsync<T> 方法

提示

此处显示的代码来自 QuerySample.cs

自 .NET Core 2.0 以来,Enumerable.ToHashSet 方法已经存在。 在 EF9 中,已添加等效的异步方法。 例如:

var set1 = await context.Posts
    .Where(p => p.Tags.Count > 3)
    .ToHashSetAsync();

var set2 = await context.Posts
    .Where(p => p.Tags.Count > 3)
    .ToHashSetAsync(ReferenceEqualityComparer.Instance);

这种增强功能是由 @wertzui 贡献的。 非常感谢!

ExecuteUpdate 和 ExecuteDelete

允许将复杂类型实例传递给 ExecuteUpdate

提示

此处显示的代码来自 ExecuteUpdateSample.cs

EF7 中引入了 ExecuteUpdate API,用于对数据库执行即时、直接更新,而无需跟踪或 SaveChanges。 例如:

await context.Stores
    .Where(e => e.Region == "Germany")
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.Region, "Deutschland"));

运行此代码会执行以下查询,将 Region 更新为“Deutschland”:

UPDATE [s]
SET [s].[Region] = N'Deutschland'
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

在 EF8 中,还可以使用 ExecuteUpdate 来更新复杂类型属性的值。 但是,必须显式指定每个复杂类型的成员。 例如:

var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");

await context.Stores
    .Where(e => e.Region == "Deutschland")
    .ExecuteUpdateAsync(
        s => s.SetProperty(b => b.StoreAddress.Line1, newAddress.Line1)
            .SetProperty(b => b.StoreAddress.Line2, newAddress.Line2)
            .SetProperty(b => b.StoreAddress.City, newAddress.City)
            .SetProperty(b => b.StoreAddress.Country, newAddress.Country)
            .SetProperty(b => b.StoreAddress.PostCode, newAddress.PostCode));

运行此代码将导致以下查询执行:

UPDATE [s]
SET [s].[StoreAddress_PostCode] = @__newAddress_PostCode_4,
    [s].[StoreAddress_Country] = @__newAddress_Country_3,
    [s].[StoreAddress_City] = @__newAddress_City_2,
    [s].[StoreAddress_Line2] = NULL,
    [s].[StoreAddress_Line1] = @__newAddress_Line1_0
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Deutschland'

在 EF9 中,可以通过传递复杂类型实例本身来执行相同的更新。 也就是说,不需要显式指定每个成员。 例如:

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));

运行此代码会导致与前面的示例相同的查询执行:

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 的单次调用中。 例如:

await context.Customers
    .Where(e => e.Name == name)
    .ExecuteUpdateAsync(
        s => s.SetProperty(
                b => b.CustomerInfo.WorkAddress, new Address("Gressenhall Workhouse", null, "Beetley", "Norfolk", "NR20 4DR"))
            .SetProperty(b => b.CustomerInfo.HomeAddress, new Address("Gressenhall Farm", null, "Beetley", "Norfolk", "NR20 4DR"))
            .SetProperty(b => b.CustomerInfo.Tag, "Tog"));

运行此代码会导致与前面的示例相同的查询执行:

UPDATE [c]
SET [c].[CustomerInfo_Tag] = N'Tog',
    [c].[CustomerInfo_HomeAddress_City] = N'Beetley',
    [c].[CustomerInfo_HomeAddress_Country] = N'Norfolk',
    [c].[CustomerInfo_HomeAddress_Line1] = N'Gressenhall Farm',
    [c].[CustomerInfo_HomeAddress_Line2] = NULL,
    [c].[CustomerInfo_HomeAddress_PostCode] = N'NR20 4DR',
    [c].[CustomerInfo_WorkAddress_City] = N'Beetley',
    [c].[CustomerInfo_WorkAddress_Country] = N'Norfolk',
    [c].[CustomerInfo_WorkAddress_Line1] = N'Gressenhall Workhouse',
    [c].[CustomerInfo_WorkAddress_Line2] = NULL,
    [c].[CustomerInfo_WorkAddress_PostCode] = N'NR20 4DR'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0

迁移

改进了临时表迁移

对于 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 支持类型化为 IReadOnlyListIReadOnlyCollectionReadOnlyCollection 的集合。 例如,在以下代码中,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 允许为支持此操作的任何关系数据库提供程序设置数据库序列的缓存选项。 例如,UseCache 可用于显式启用缓存并设置缓存大小:

modelBuilder.HasSequence<int>("MyCachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseCache(3);

这会导致使用 SQL Server 时出现以下序列定义:

CREATE SEQUENCE [MyCachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE CACHE 3;

同样,UseNoCache 会显式禁用缓存:

modelBuilder.HasSequence<int>("MyUncachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseNoCache();
CREATE SEQUENCE [MyUncachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE NO CACHE;

如果 UseCacheUseNoCache 两者均未调用,则未指定缓存,并且数据库将使用其默认值。 对于不同的数据库,这可能是不同的默认值。

这种增强功能是由 @bikbov 贡献的。 非常感谢!

指定键和索引的填充因子

提示

此处显示的代码来自 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");

如果 daisyHierarchyId/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/ 的节点,并将其置于 child1child2 之间。

这种增强功能是由 @Rezakazemi890 贡献的。 非常感谢!

工具

减少重新生成次数

默认情况下,dotnet ef 命令行工具会在执行该工具之前会生成项目。 这是因为在出现故障的情况下,在运行该工具之前不进行重新生成会是一种常见的混淆源。 经验丰富的开发人员可以使用 --no-build 选项来避免这种可能会非常缓慢的生成。 但即使 --no-build 选项也可能导致下次在 EF 工具外部生成项目时重新生成该项目。

我们认为 @Suchiman社区贡献已经解决了这一问题。 但我们也意识到,围绕 MSBuild 行为的进行的调整往往会产生意外后果,因此我们要求像你这样的人尝试此操作,并报告你的任何负面体验。