EF Core 9 新功能
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 最佳做法保持一致。 主要的高層級改良項目如下:如需完整清單,請參閱此 Epic 問題。
警告
在提供者的改良項目當中,有一系列高度影響的重大變更必須進行;如果要升級現有的應用程式,請仔細閱讀重大變更一節。
使用分割區索引鍵和文件識別碼進行查詢的功能改良
儲存在 Azure Cosmos DB 資料庫中的每個檔都有唯一的資源識別碼。 此外,每個文件都可能包含「分割區索引鍵」,可決定資料的邏輯資料分割,以便有效地調整資料庫大小。 如需選擇分割區索引鍵的詳細資訊,請參閱在 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
子句中,導致系統會對所有分割區執行查詢,造成成本增加並降低效能。
此外,如果您的查詢也提供文件識別碼屬性的值,且不包含任何其他查詢作業,提供者可以套用額外的最佳化:
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),直接擷取指定分割區索引鍵和識別碼的文件。 這是您可以在 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 會擷取、 userId
和 sessionId
參數的值tenantId
,並將其傳遞至 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 會以更自然的方式對應至 Azure Cosmos DB 檔,以 JSON 為基礎的文件資料庫,並協助與其他存取您文件的系統互操作。 儘管這是一項重大變更,但 API 的存在使所有情況下都能還原回 9.0 以前的行為。
已簡化 id
不含鑑別子的屬性
首先,舊版 EF 會將鑑別子值插入 JSON id
屬性,產生如下文件:
{
"id": "Blog|1099",
...
}
這樣做是為了允許不同類型的文件 (例如部落格和文章) 和相同的索引鍵值 (1099) 在相同容器分割區中並存。 從EF 9.0 開始,id
屬性只包含索引鍵值:
{
"id": 1099,
...
}
這是對應 JSON 的方法更自然,可讓外部工具和系統更容易與 EF 產生的 JSON 文件互動;這類外部系統通常不會察覺按照預設從 .NET 類型衍生的 EF 鑑別子值。
請注意,這是一項重大變更,EF 從此將無法再使用舊的 id
格式查詢現有文件。 我們引進 API 來還原以前的行為,請參閱重大變更備註和文件了解詳情。
鑑別子屬性重新命名為 $type
預設的鑑別子屬性過去稱為 Discriminator
。 EF 9.0 將預設變更為 $type
:
{
"id": 1099,
"$type": "Blog",
...
}
這種作法遵循了新興的 JSON 多型標準,能提升與其他工具的交互操作性。 舉例來說,NET 的 System.Text.Json 將 $type
當作預設鑑別子屬性名稱來使用,也能支援多型 (文件)。
請注意,這是一項重大變更,自此 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 提供者現在允許透過接續令牌分頁查詢結果,這比傳統的 和 Take
使用Skip
更有效率且符合成本效益:
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();
如需詳細資訊,請參閱分頁的相關文件章節。
角色型存取
適用於 NoSQL 的 Azure Cosmos DB 包含內建的角色型存取控制 (RBAC) 系統。 EF9 現在支援所有數據平面作業。 不過,Azure Cosmos DB SDK 不支援在 Azure Cosmos DB 中管理平面作業的 RBAC。 使用 Azure 管理 API,而不 EnsureCreatedAsync
搭配 RBAC 使用。
同步 I/O 現在預設為封鎖
適用於 NoSQL 的 Azure Cosmos DB 不支援應用程式程式碼的同步 (封鎖) 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 在沒有 Just-In-Time (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();
此查詢會在針對 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.Min
和 Math.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.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();
此查詢會在針對 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 和參數:
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
計算 LatestPostRating
在轉譯為 SQL 時需要子查詢的每個 Post
。 稍後在查詢中,這些結果會使用 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")
可為 Null 值的比較運算適用的 C# 語意
在 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 的實體傳回 false
(而不是 C# 預期的 true
)。 對 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 所貢獻。 由衷感謝!
和 OrderDescending
LINQ 運算符的Order
翻譯
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
、否定和其他各種建構方面帶來了許多優化;其中大部分是由安德里亞·坎西亞尼(@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 投影否定的 bool 屬性時:
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 中,此翻譯已經過簡化,現在使用位 NOT (~
):
SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]
更好的 Azure SQL 和 Azure Synapse 支援
在指定目標 SQL Server 類型時,EF9 可提供更多彈性。 您現在可以指定 或 UseAzureSynapse
,而不是使用 UseSqlServer
來設定 UseAzureSql
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
現在已會轉譯 (#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
Null 性優化 (#34263,由 @ranma42 所AT TIME ZONE
貢獻)。 - 過度
IN
和設定作業的DISTINCT
優化 (#34381,由 @ranma42 所EXISTS
貢獻)。
上述僅僅是 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);
}
防止並行移轉
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);
}
});
您可以在這裡找到詳細資訊。
模型建置
自動編譯模型
提示
此處所示的程式碼來自 NewInEFCore9.CompiledModels 範例。
編譯模型可以讓具有大型模型的應用程式 (實體類型計數達上百或上千) 改善啟動時間。 在舊版 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;
}
之後,您可以使用一般方式在查詢中使用這些集合。 例如這個 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。
使用 EF Core 移轉來建立索引鍵和索引時,EF9 支援 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 中依屬性 (attribute) 對應屬性 (property) 的程式碼如下:
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 路徑產生作業的甜頭
EF8 已新增 SQL Server HierarchyId
類型的第一類別支援。 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
具有 /4/1/3/1/
的 HierarchyId
,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/
節點,並將它放在 和 child2
之間child1
。
這項增強功能是由 @Rezakazemi890 所貢獻。 由衷感謝!
Tooling
減少重建次數
dotnet ef
命令行工具依預設會先建置您的專案,再執行工具。 這是因為不在執行工具之前先重建,常常會在無法操作時造成混淆。 經驗豐富的開發人員可以使用 --no-build
選項來避免這種速度緩慢的建置方式。 不過,即使是 --no-build
選項,也會在下次專案於 EF 工具外部建置專案時經過重建。
我們認為 @Suchiman 的社群貢獻修正了這個問題。 不過,我們也意識到,MSBuild 行為的調整常常產生非預期的後果,因此我們請求各位試用,並在遭遇任何負面體驗時通知我們。