EF Core 9 的新功能

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 每日組建,相較於最新的預覽,通常還有數周完成的工作。 我們強烈建議您在測試新功能時使用每日組建,以免對過時的位執行測試。

LINQ 和 SQL 翻譯

小組正致力於 EF Core 9 中查詢管線的一些重大架構變更,作為我們持續改善 JSON 對應和文件資料庫的一部分。 這表示我們需要讓 像您 這樣的人員在這些新的內部程式上執行程序代碼。 (如果您在發行中閱讀了一份「新功能」檔,則您是社群中真正參與的一部分,謝謝!我們有超過 120,000 個測試,但還不夠! 我們需要您,在位上執行實際程式代碼的人員,以找出問題並寄送穩固的版本!

剪除傳遞至 OPENJSON 的 WITH 子句的數據行

提示

此處顯示的程式代碼來自 JsonColumnsSample.cs

呼叫 時 OPENJSON WITH,EF9 會移除不必要的數據行。 例如,請考慮使用述詞從 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

請注意, UpdatedBy此查詢中不需要 、 與 Commits 。 從 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.Max 或的查詢現在會分別使用 GREATESTMath.MinLEAST 來轉譯 Azure SQL。 例如:

var walksUsingMin = await context.Walks
    .Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
    .ToListAsync();

此查詢會在針對 SQL Server 2022 使用 EF9 執行時轉譯為下列 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();

此查詢會在針對 SQL Server 2022 使用 EF9 執行時轉譯為下列 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 中直接叫 Least 用 或 Greatest 函式。 例如:

var leastCount = await context.Pubs
    .Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
    .ToListAsync();

此查詢會在針對 SQL Server 2022 使用 EF9 執行時轉譯為下列 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 部落格” 建立常數,因為此值不會從查詢變更為查詢。 使用常數可讓資料庫引擎在建立查詢計劃時檢查此值,這可能會導致更有效率的查詢。

另一方面,的值 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 中, IQueryable 會內嵌 中的 dotnetPosts ,導致單一來回行程:

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

ExecuteUpdate在 EF7 中引進 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);
}

模型建置

指定序列的快取

提示

此處顯示的程式代碼來自 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;

UseCache如果未呼叫 或 UseNoCache ,則不會指定快取,而且資料庫會使用其預設值。 這可能是不同資料庫的不同預設值。

這項增強功能是由 @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);

注意

預覽版 2 目前有一個錯誤,其中第一次建立數據表時不會包含填滿因數。 問題 #33269 會 追蹤此專案

這項增強是由 @deano獵人貢獻的。 非常感謝!

讓現有的模型建置慣例更具擴充性

提示

此處顯示的程式代碼來自 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

Sugar for HierarchyId path generation

EF8 已新增 SQL Server HierarchyId 類型的第一類別支援。 在 EF9 中,已新增 Sugar 方法,讓您更輕鬆地在樹狀結構中建立新的子節點。 例如,下列程式代碼會查詢具有 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/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/節點,並將它放在 bteween child1child2

這項增強功能是由 @Rezakazemi890所貢獻。 非常感謝!

工具

重建次數較少

dotnet ef命令行工具預設會先建置您的專案,再執行此工具。 這是因為在執行工具之前未重建是當事情無法運作時常見的混淆來源。 經驗豐富的開發人員可以使用 --no-build 選項來避免此建置速度很慢。 不過,即使 --no-build 選項也會在下次 EF 工具外部建置專案時重新建置。

我們相信,來自@Suchiman的社區貢獻已經修正了這一點。 不過,我們也意識到,MSBuild 行為的調整有非預期的後果傾向,因此我們會要求像您這樣的人試試看,並回報您擁有的任何負面體驗。