EF Core 9 (EF9) 是 EF Core 8 的下個版本,預定於 2024 年 11 月發行。
EF9 會以
每日組建
的形式提供,其中包含所有最新的 EF9 功能和 API 調整。 本文的範例會使用這些每日組建。
您可以從
GitHub 下載範例程式代碼
,以執行並偵錯範例。 下方每一節都會連結到該節專用的原始程式碼。
EF9 的目標是 .NET 8,因此可以搭配
.NET 8 (LTS)
或
.NET 9
使用。
每個預覽版的
最新內容
檔都會更新。 所有範例都設定為使用
EF9 每日組建
,與最新預覽版相比,通常比最新的預覽版多出幾週已完成的工作。 我們強烈建議您在測試新功能時使用每日組建,以免執行測試時使用過時的位元。
適用於 NoSQL 的 Azure Cosmos DB
EF 9.0 為 Azure Cosmos DB 的 EF Core 提供者帶來大幅改善;提供者的重要部分已重寫以提供新功能、允許新形式的查詢,以及更妥善地讓提供者與 Azure Cosmos DB 最佳做法保持一致。 主要的高階改善如下:如需完整清單,
請參閱此史詩問題
。
作為提供者改善的一部分,必須進行一些影響重大變更:如果您要升級現有的應用程式,請仔細閱讀
重大變更一節
。
使用分割區索引鍵和文件識別碼進行查詢的功能改良
儲存在 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),直接擷取具分割區鍵與 ID 的文件。 這是您可以在 Azure Cosmos DB 中執行的最有效率且符合成本效益的讀取類型;如需有關點讀取的詳細資訊 ,請參閱 Azure Cosmos DB 檔 。
若要深入瞭解使用分割區索引鍵和點讀取進行查詢, 請參閱查詢文件頁面。
階層式分割區索引鍵
此處顯示的程式代碼來自 HierarchicalPartitionKeysSample.cs。
Azure Cosmos DB 原本支援單一分割區索引鍵,但此後已擴充分割區能力,以支援透過在分割區索引鍵中指定最多三個層級的階層來進行子分割。 EF Core 9 完全支援階層式分割區索引鍵,可讓您盡情運用這項功能帶來的強化效能和成本節省。
分割區索引鍵的指定係使用建模 API,通常是 DbContext.OnModelCreating。 分割區索引鍵每個層級的實體類型都必須有對應的屬性。 我們以 UserSession
實體類型為例:
public class UserSession
// Item ID
public Guid Id { get; set; }
// Partition Key
public string TenantId { get; set; } = null!;
public Guid UserId { get; set; }
public int SessionId { get; set; }
// Other members
public string Username { get; set; } = null!;
下列程式碼會使用 TenantId
、UserId
與 SessionId
屬性來指定三層級的分割區索引鍵:
modelBuilder
.Entity<UserSession>()
.HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });
此分割區索引鍵定義遵循從 Azure Cosmos DB 檔中 選擇階層式分割區索引鍵 中所述的範例。
請注意,從 EF Core 9 開始,任何對應類型的屬性都可用於分割區索引鍵。 如果是 bool
和數值類型,例如 int SessionId
屬性,值會直接使用在分割區索引鍵。 其他類型 (例如 Guid UserId
屬性) 會自動轉換成字串。
查詢時,EF 會自動從查詢擷取分割區索引鍵值,並將其套用至 Azure Cosmos DB 查詢 API,以確保查詢會適當地限制為可能最少的數據分割數目。 例如,請考慮下列 LINQ 查詢,提供階層中的所有三個分割區索引鍵值:
var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");
var sessions = await context.Sessions
.Where(
e => e.TenantId == tenantId
&& e.UserId == userId
&& e.SessionId == sessionId
&& e.Username.Contains("a"))
.ToListAsync();
執行此查詢時,EF Core 會擷取、 tenantId
和 userId
參數的值sessionId
,並將其傳遞至 Azure Cosmos DB 查詢 API 作為分割區索引鍵值。 舉例來說,請參閱執行上述查詢的記錄:
info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))
請注意,分割區索引鍵比較結果已從 WHERE
子句中移除,並改當作分割區索引鍵來使用,以便有效率地執行:["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]
。
如需詳細資訊,請參閱關於以分割區索引鍵進行查詢的文件。
LINQ 查詢功能已大幅改良
在 EF 9.0 中,Azure Cosmos DB 提供者的 LINQ 轉譯功能已大幅擴充,而且提供者現在可以執行更多查詢類型。 查詢的完整改良項目清單因過長而無法列出,以下節錄重點:
EF 基本集合的完整支援,可讓您對 ints 或字串等集合執行 LINQ 查詢。 如需詳細資訊 ,請參閱 EF8:基本集合的新功能 。
支援對非基本集合進行任意查詢。
許多額外的 LINQ 運算子已接受支援:編製索引成集合、Length
/Count
、ElementAt
、Contains
,以及其他多種項目。
支援匯總運算子,例如 Count
和 Sum
。
其他函式翻譯(如需支援翻譯的完整清單,請參閱 函式對應檔 ):
DateTime
和DateTimeOffset
元件成員的翻譯 (DateTime.Year
, DateTimeOffset.Month
...)。
EF.Functions.IsDefined
和 EF.Functions.CoalesceUndefined
現在已可處理 undefined
值。
string.Contains
、StartsWith
和 EndsWith
已可支援 StringComparison.OrdinalIgnoreCase
。
如需查詢改善的完整清單,請參閱 此問題:
已改善與 Azure Cosmos DB 和 JSON 標準一致的模型
EF 9.0 會以更自然的方式對應至 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
做為其預設的歧視性屬性名稱 (docs)。
請注意,這是一項重大變更,自此 EF 將無法再使用舊的鑑別子屬性名稱來查詢現有文件。 如需如何還原為先前命名的詳細資訊,請參閱 重大變更附注 。
向量相似性搜尋 (預覽版)
Azure Cosmos DB 目前提供向量相似性搜尋的預覽版支援。 向量搜尋是某些應用程式類型的基本部分,包括 AI、語意搜尋等。 Azure Cosmos DB 可讓您將向量直接儲存在文件的其餘數據中,這表示您可以針對單一資料庫執行所有查詢。 這可大幅簡化您的架構,並移除堆疊中額外專用向量資料庫解決方案的需求。 若要深入瞭解 Azure Cosmos DB 向量搜尋, 請參閱檔。
正確設定 Azure Cosmos DB 容器之後,透過 EF 使用向量搜尋是新增向量屬性並加以設定的簡單事項:
public class Blog
public float[] Vector { get; set; }
public class BloggingContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity<Blog>()
.Property(b => b.Embeddings)
.IsVector(DistanceFunction.Cosine, dimensions: 1536);
完成後,使用 LINQ 查詢的 EF.Functions.VectorDistance()
函式即可執行向量相似性搜尋:
var blogs = await context.Blogs
.OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
.Take(5)
.ToListAsync();
如需詳細資訊,請參閱 向量搜尋的檔。
Azure Cosmos DB 提供者現在允許透過接續令牌對查詢結果進行分頁,這比傳統使用Skip
和Take
的方法更有效率且符合成本效益。
var firstPage = await context.Posts
.OrderBy(p => p.Id)
.ToPageAsync(pageSize: 10, continuationToken: null);
var continuationToken = firstPage.ContinuationToken;
foreach (var post in page.Values)
// Display/send the posts to the user
新的 ToPageAsync
運算子會傳回 CosmosPage
,它會公開接續權杖,有效率地於稍後繼續查詢,擷取後續 10 個項目:
var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);
如需詳細資訊,請參閱分頁的文件部分。
FromSql 提供更安全的 SQL 查詢
Azure Cosmos DB 提供者已允許透過 FromSqlRaw進行 SQL 查詢。 不過,如果使用者提供的資料差補或串連到 SQL,該 API 可能很容易受到 SQL 插入式攻擊。 您現在已可在 EF 9.0 使用新的 FromSql
方法,這個方法一律會將參數化資料整合為 SQL 外部的參數:
var maxAngle = 8;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
.ToListAsync();
如需詳細資訊,請參閱分頁的文件部分。
角色型存取
適用於 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 目前仍可使用,只要適當地設定警告層級即可。 舉例來說,在 OnConfiguring
類型的 DbContext
:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));
不過請注意,我們計畫完全取消支援 EF 11 的同步處理,因此請開始更新,盡早使用 ToListAsync
和 SaveChangesAsync
等非同步方法!
AOT 和預先編譯查詢
NativeAOT 和查詢先行編譯是高度實驗性的功能,還不適合生產環境使用。 以下所述的支援應視為基礎結構,以便在未來版本中發行最終功能。 建議您試驗目前的支援,並報告您的體驗,但建議您在生產環境中部署 EF NativeAOT 應用程式。
EF 9.0 提供 .NET NativeAOT 的初始實驗性支持,允許發行使用 EF 存取資料庫的預先編譯應用程式。 為了支援 NativeAOT 模式的 LINQ 查詢,EF 依賴 查詢先行編譯:此機制會靜態識別 EF LINQ 查詢併產生 C# 攔截器,其中包含執行每個特定查詢的程式代碼。 這可能會大幅減少應用程式的啟動時間,因為每次應用程式啟動時,處理和編譯 LINQ 查詢到 SQL 的繁重工作就不再發生。 相反地,每個查詢的攔截器都包含該查詢完成的 SQL,以及優化程式代碼,以將資料庫結果具體化為 .NET 物件。
例如,指定具有下列 EF 查詢的程式:
var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();
EF 會在專案中產生 C# 攔截器,以接管查詢執行。 攔截器沒有在每次程式啟動時處理查詢並將其轉譯為 SQL,而是將 SQL 內嵌到其中(在此案例中為 SQL Server),讓您的程式能夠更快啟動:
var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));
此外,相同的攔截器包含從資料庫結果具體化 .NET 物件的程序代碼:
var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);
這會使用另一個新的 .NET 功能 - 不安全存取子,將資料從資料庫注入到您物件的私有欄位。
如果您對 NativeAOT 感興趣,並想要實驗尖端功能,請試試看! 請注意,此功能應該視為不穩定,且目前有許多限制:我們預期會穩定它,使其更適合 EF 10 中的生產使用方式。
如需詳細資訊,請參閱 NativeAOT 文件頁面 。
LINQ 和 SQL 轉譯
與每個版本一樣,EF9 針對 LINQ 查詢功能做了大量改善。 新的查詢可以轉譯,且許多受支援案例的 SQL 轉譯都已改良,提升了效能和可讀性。
由於改良的數目過多,無法在此一一列出。 以下反白顯示一些較重要的改善,完整的 9.0 版本工作清單請參閱 此項目 。
我們想要表揚安德里亞·坎西亞尼(@ranma42),因為他對優化 EF Core 所產生的 SQL 做出了許多高品質的貢獻!
複雜類型:GroupBy 和 ExecuteUpdate 支援
此處顯示的程式代碼來自 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]
此處顯示的程式代碼來自 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 = await dotnetPosts
.Where(p => p.Id > 2)
.Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
.Skip(2).Take(10)
.ToArrayAsync();
在 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
在 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]
會為 false
或 NullableIntOne
設為 null 的實體傳回 NullableIntTwo
(而不是 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
例外狀況,因為轉譯會針對 null
或 NullableIntOne
為 null 的情況產生 NullableIntTwo
值。
EF9 現在可正確處理這些情境,產生與 LINQ to Objects 一致的結果,且在不同提供者皆然。
這項增強功能是由 @ranma42所貢獻。 由衷感謝!
和 Order
LINQ 運算符的OrderDescending
翻譯
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)
FROM [Blogs] AS [b]
在 EF9,我們移除了巢狀結構:
SELECT CASE
WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
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 可提供更多彈性。 您現在可以指定 或 UseSqlServer
,而不是使用 UseAzureSql
來設定 UseAzureSynapse
EF。
這可讓 EF 在使用 Azure SQL 或 Azure Synapse 時產生更好的 SQL。 EF 可以利用資料庫的獨特功能(例如在 Azure SQL 上的 JSON 專用類型),或克服其限制(例如在使用 Azure Synapse 時, 子句無法使用 ESCAPE
)。
其他查詢改良項目
EF8 中引進的基本集合查詢支援已擴充,以支援所有ICollection<T>
類型。 請注意,這僅適用於參數和內聯集合——屬於實體一部分的基本型別集合仍然受限於陣列、清單,以及EF9中的唯讀陣列/清單。
新增 ToHashSetAsync
函式以返回查詢結果為 HashSet
(#30033,由 @wertzui貢獻)。
TimeOnly.FromDateTime
和 FromTimeSpan
現在會在 SQL Server 上轉譯 (#33678)。
ToString
over enums 現在已翻譯(#33706,由 @Danevandy99 所貢獻)。
string.Join
現在在 SQL Server 的非聚合上下文中會轉譯為 CONCAT_WS(#28899)。
EF.Functions.PatIndex
現在會轉譯為 SQL Server PATINDEX
函式,其會傳回第一次出現模式的起始位置(#33702, @smnsht)。
Sum
和 Average
現在適用於 SQLite 上的小數點 (#33721,由 @ranma42 貢獻)。
修正和優化 string.StartsWith
和 EndsWith
(#31482)。
Convert.To*
方法現在可以接受 類型 object
自變數 (#33891,由 @imangd 所貢獻)。
Exclusive-Or (XOR) 作業現在會在 SQL Server 上轉譯 (#34071,由 @ranma42 提供)。
COLLATE
及 AT TIME ZONE
操作的可空性優化(#34263,由 @ranma42 所貢獻)。
DISTINCT
針對比 IN
和 EXISTS
集合操作進行優化(#34381,由 @ranma42 所貢獻)。
上述只是 EF9 中一些更重要的查詢改善;如需更完整的清單,請參閱 此問題 。
防止並行移轉
EF9 引進鎖定機制,以防止同時發生多個移轉執行,因為這可能會使資料庫處於損毀狀態。 使用建議的方法將移轉部署至生產環境在運行時間套用移轉,就可能發生此情況。 建議您在部署時套用移轉,而不是作為應用程式啟動的一部分,但這可能會導致更複雜的應用程式架構(例如 使用 .NET Aspire 專案時)。
如果您使用 Sqlite 資料庫,請參閱 與此功能相關聯的潛在問題。
在交易內無法執行多個移轉作業時發出警告
在移轉期間執行的大多數作業都會受到交易的保護。 這可確保如果因為某些原因而移轉失敗,資料庫最終不會處於損毀狀態。 不過,某些作業不會包裝在交易中(例如 SQL Server 記憶體優化數據表上的作業,或修改資料庫定序之類的資料庫改變作業)。 若要避免在移轉失敗時損毀資料庫,建議您使用個別移轉來隔離執行這些作業。 EF9 現在偵測到移轉包含多個作業的案例,其中一項無法包裝在交易中,併發出警告。
改善的數據植入
EF9 引進了一種方便的方式來執行數據植入,其會以初始數據填入資料庫。
DbContextOptionsBuilder
現在包含 UseSeeding
和 UseAsyncSeeding
方法,這些方法會在初始化 DbContext 時執行(作為的一 EnsureCreatedAsync
部分)。
如果應用程式先前已執行,資料庫可能已經包含範例數據(在內容的第一次初始化時新增)。 因此,在嘗試填入資料庫之前, UseSeeding
UseAsyncSeeding
應該先檢查數據是否存在。 這可藉由發出簡單的 EF 查詢來達成。
以下是如何使用這些方法的範例:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
.UseSeeding((context, _) =>
var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
if (testBlog == null)
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
context.SaveChanges();
.UseAsyncSeeding(async (context, _, cancellationToken) =>
var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
if (testBlog == null)
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
await context.SaveChangesAsync(cancellationToken);
如需詳細資訊,請參閱 這裡。
其他移轉改善
將現有的數據表變更為 SQL Server 時態表時,移轉程式代碼大小已大幅降低。
自動編譯模型
此處顯示的程式代碼來自 NewInEFCore9.CompiledModels 範例。
編譯模型可以讓具有大型模型的應用程式 (實體類型計數達上百或上千) 改善啟動時間。 在舊版 EF Core 中,編譯模型必須使用命令行手動產生。 例如:
dotnet ef dbcontext optimize
執行命令之後,您必須將 .UseModel(MyCompiledModels.BlogsContextModel.Instance)
這類的行加入 OnConfiguring
才能讓 EF Core 知道要使用編譯模型。
從 EF9 開始,如果應用程式的 .UseModel
類型位於與編譯模型相同的專案/元件,就不再需要此 DbContext
行。 相反地,系統會自動偵測並使用編譯模型。 每次建置模型,EF 記錄都會顯示此行為。 執行簡單的應用程式,就會在應用程式啟動時顯示 EF 會建置模型:
Starting application...
>> EF is building the model...
Model loaded with 2 entity types.
針對模型專案執行 dotnet ef dbcontext optimize
的輸出如下:
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> dotnet ef dbcontext optimize
Build succeeded in 0.3s
Build succeeded in 0.3s
Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered automatically, but you can also call 'options.UseModel(BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model>
請注意,記錄輸出指出 執行命令時已建置模型。 如果我們現在再次執行應用程式,重建但不進行任何程式碼變更,則輸出為:
Starting application...
Model loaded with 2 entity types.
請注意,模型在啟動應用程式時不會建置,因為系統會自動偵測並使用編譯模型。
MSBuild 整合
使用上述方法,當實體類型或 DbContext
組態變更時,您仍需要手動重新產生編譯模型。 不過,EF9 隨附 MSBuild 工作套件,可在建置模型專案時自動更新已編譯的模型! 若要開始使用,請安裝 Microsoft.EntityFrameworkCore.Tasks NuGet 套件。 例如:
dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0
請使用上述命令的套件版本,它與您使用的 EF Core 版本相符。
然後在您的EFOptimizeContext
檔案中設定 EFScaffoldModelStage
和 .csproj
屬性,以啟用整合。 例如:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFScaffoldModelStage>build</EFScaffoldModelStage>
</PropertyGroup>
接下來,如果我們建置項目,就可以看到建置階段的記錄指出編譯模型正在建置:
Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
--additionalprobingpath G:\packages
--additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages"
--runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\
--namespace NewInEfCore9
--suffix .g
--assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll
--project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model
--root-namespace NewInEfCore9
--language C#
--nullable
--working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App
--verbose
--no-color
--prefix-output
執行應用程式會顯示系統已偵測到編譯模型,因此不會再次建置模型:
Starting application...
Model loaded with 2 entity types.
現在,只要模型變更,編譯模型就會在建置專案後立即自動建置。
如需詳細資訊,請參閱 MSBuild 整合。
唯讀的基本集合
此處顯示的程式代碼來自 PrimitiveCollectionsSample.cs。
EF8 引進了對 映射陣列及可變動的基本類型清單的支援。 這項支援在 EF9 擴充為包含唯讀的集合/清單。 具體而言,EF9 支援類型為 IReadOnlyList
、IReadOnlyCollection
或 ReadOnlyCollection
的集合。 舉例來說,在下列程式碼中,系統會依慣例將 DaysVisited
對應為日期的基本集合:
public class DogWalk
public int Id { get; set; }
public string Name { get; set; }
public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
如有需要,唯讀集合可由一般的可變集合來支援。 舉例來說,在下列程式碼中,DaysVisited
可以對應為日期的基本集合,同時仍允許類別中的程式碼操作基礎清單。
public class Pub
public int Id { get; set; }
public string Name { get; set; }
public IReadOnlyCollection<string> Beers { get; set; }
private List<DateOnly> _daysVisited = new();
public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
之後,您可以使用一般方式在查詢中使用這些集合。 例如這個 LINQ 查詢:
var walksWithADrink = await context.Walks.Select(
w => new
WalkName = w.Name,
PubName = w.ClosestPub.Name,
Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
TotalCount = w.DaysVisited.Count
}).ToListAsync();
它會在 SQLite 轉譯為下列 SQL:
SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (
SELECT COUNT(*)
FROM json_each("w"."DaysVisited") AS "d"
WHERE "d"."value" IN (
SELECT "d0"."value"
FROM json_each("p"."DaysVisited") AS "d0"
)) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"
指定索引鍵和索引的填滿因數
此處顯示的程式代碼來自 ModelBuildingSample.cs。
EF9 支援在使用 EF Core 移轉創建索引鍵和索引時指定 SQL Server 填充因數 的設定。 根據 SQL Server 文件所述,「建立或重建索引後,填滿因數值會決定要在每個分頁層級頁面上填滿資料的空間百分比,進而保留每個頁面的剩餘百分比當做可用空間,以供未來成長使用。」
填滿因數可設定在單一或複合的主要和替代索引鍵和索引。 例如:
modelBuilder.Entity<User>()
.HasKey(e => e.Id)
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasAlternateKey(e => new { e.Region, e.Ssn })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Name })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Region, e.Tag })
.HasFillFactor(80);
套用至現有資料表時,它會將資料表變更為條件約束的填滿因數:
ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];
ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];
ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80);
這項增強是由 @deano獵人貢獻的。 由衷感謝!
讓現有的模型建置慣例更可擴充
此處顯示的程式代碼來自 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 中,我們不僅<c0>改善了在失敗時產生的錯誤訊息,還允許使用非公用建構函式進行實例化。 在絕不能由應用程式程式碼具現化的私人巢狀類別共同放置組態時,這種作法很有用。 例如:
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 路徑產生作業的甜頭
HierarchyId
SQL Server 類型的第一類別支援。 EF9 新增了絕佳的方法,讓您更輕鬆地在樹狀結構中建立新的子節點。 舉例來說,下列程式碼會查詢具有 HierarchyId
屬性的現有實體:
var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");
這個 HierarchyId
屬性可用來建立子節點,不需要任何明確的字串操作。 例如:
var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");
如果 daisy
具有 HierarchyId
的 /4/1/3/1/
,child1
會取得 HierarchyId
“/4/1/3/1/1/”,child2
則會取得 HierarchyId
“/4/1/3/1/2/”。
若要在這兩個子系之間建立節點,您可以使用額外的子層級。 例如:
var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");
這會建立具有 HierarchyId
的/4/1/3/1/1.5/
節點,並將它放在 和 child1
之間child2
。
這項增強功能是由 @Rezakazemi890所貢獻。 由衷感謝!
減少重建次數
dotnet ef
命令行工具預設會先建置您的專案,再執行此工具。 這是因為不在執行工具之前先重建,常常會在無法操作時造成混淆。 經驗豐富的開發人員可以使用 --no-build
選項來避免這種速度緩慢的建置方式。 不過,即使是 --no-build
選項,也會在下次專案於 EF 工具外部建置專案時經過重建。
我們相信社區貢獻來自@Suchiman已經修正了這一點。 不過,我們也意識到,MSBuild 行為的調整常常產生非預期的後果,因此我們請求各位試用,並在遭遇任何負面體驗時通知我們。