2022年11月8日.NET 7正式發佈 .NET仍然是最快、最受歡迎、最值得信賴的平臺之一,其龐大的.NET軟體包生態系統包括33萬多個軟體包。 .NET 7為您的應用程式帶來了更高的性能和C# 11/F# 7、.NET MAUI、ASP.NET Core/Blazor、Web APIs、WinF ...
2022年11月8日.NET 7正式發佈
.NET仍然是最快、最受歡迎、最值得信賴的平臺之一,其龐大的.NET軟體包生態系統包括33萬多個軟體包。
.NET 7為您的應用程式帶來了更高的性能和C# 11/F# 7、.NET MAUI、ASP.NET Core/Blazor、Web APIs、WinForms、WPF等的新功能。有了.NET 7,你還可以輕鬆地將你的.NET 7項目容器化,在GitHub行動中設置CI/CD工作流程,並實現雲原生的可觀察性。
感謝開源的.NET社區為幫助塑造這個.NET 7版本所作的大量貢獻。在整個.NET 7版本中,有超過8900名貢獻者做出了28000項貢獻!
.NET 7的發佈是我們.NET統一之旅的第三個主要版本(自2016年的.NET 5以來)。
有了.NET 7,你可以通過一個SDK、一個運行時、一套基礎庫來構建多種類型的應用程式(雲、網路、桌面、移動、游戲、物聯網和人工智慧),一次學習並重覆使用你的技能。
.NET 7支持周期
.NET 7得到了微軟的正式支持。它被標記為一個標準期限支持(STS)版本,將被支持18個月。奇數的.NET版本是STS版本,在隨後的STS或LTS版本之後,可以獲得6個月的免費支持和補丁。
獲取
從官網獲取
安裝SDK
- sdk-7.0.100-windows-x64-installer
- sdk-7.0.100-windows-x86-installer
- sdk-7.0.100-windows-arm64-installer
安裝桌面運行時
- runtime-desktop-7.0.0-windows-x64-installer
- runtime-desktop-7.0.0-windows-x86-installer
- runtime-desktop-7.0.0-windows-arm64-installer
安裝ASP.NET Core運行時
- runtime-aspnetcore-7.0.0-windows-x64-installer
- runtime-aspnetcore-7.0.0-windows-x86-installer
- runtime-aspnetcore-7.0.0-windows-hosting-bundle-installer
安裝.NET運行時
- runtime-7.0.0-windows-x64-installer
- runtime-7.0.0-windows-x86-installer
- runtime-7.0.0-windows-arm64-installer
通過Nuget獲取
安裝SDK
winget install Microsoft.DotNet.SDK.7
安裝桌面運行時
winget install Microsoft.DotNet.DesktopRuntime.7
安裝ASP.NET Core運行時
winget install Microsoft.DotNet.AspNetCore.7
安裝.NET運行時
winget install Microsoft.DotNet.Runtime.7
在.NET 7中ASP.NET Core更新有哪些?
服務與運行時(Servers and runtime)
- 速率限制(Rating limiting):使用靈活的端點配置和策略限制處理請求的速率。
- 輸出緩存(Output caching):為響應配置緩存以更有效地處理請求。
- 請求解壓縮(Request decompression):接受具有壓縮內容的請求。
- HTTP/3:內置支持HTTP/3,這是基於新的QUIC復用傳輸協議的最新HTTP版本。
- Http/2 WebSockets支持: 使用WebSockets over HTTP/2連接。
- WebTransport(實驗性):通過對WebTransport的實驗性支持,在HTTP/3上創建流和數據克。
最小API(Minimal APIs)
- 終結點篩選器(Endpoint filters):使用端點過濾器在路由處理程式之前或之後運行橫切代碼。
- 類型化結果(Typed results):從最小的API返回強類型的結果。
- 路由組(Route groups):用一個共同的首碼來組織端點組。
遠程調用(gRPC)
- JSON轉碼(JSON transcoding):通過將你的gRPC服務暴露為基於JSON的API來擴大它們的覆蓋範圍
- JSON轉碼文檔與Swagger/OpenAPI(實驗性的):使用實驗性支持,為你的gRPC JSON轉碼服務生成OpenAPI規範。
- gRPC運行狀況檢查:報告和檢查gRPC伺服器應用程式的健康狀況。
- gRPC客戶端添加持有者令牌:創建使用承載令牌發送授權請求的客戶端。
實時應用(SignalR)
- 客戶端結果(Client results):響應伺服器的請求,向伺服器返回客戶結果。
MVC
- MVC視圖和Razor頁面中的可為空模型(Nullable view and page models):現在支持Nullable頁面和視圖模型,以改善使用空狀態檢查時的體驗。
客戶端Web應用(Blazor)
- 自定義元素(Custom elements):用Blazor構建標準的HTML自定義元素,將Blazor組件與任何基於JavaScript的應用程式整合起來。
- 處理位置更改事件(Handle location changing events):攔截位置變化事件,以創建導航時的自定義用戶體驗。
- 數據綁定(之後/獲取/設置)修改器(Bind after/get/set modifiers):在數據綁定後運行非同步邏輯,並獨立控制數據綁定如何獲取和設置數據。
- 動態認證請求(Dynamic authentication requests):在運行時用自定義參數創建動態認證請求,以處理Blazor WebAssembly應用程式中的高級認證場景。
- 在WebAssembly中改進的JavaScript互操作(Improved JavaScript interop on WebAssembly):使用新的
[JSImport]
/[JSExport]
支持,在WebAssembly上運行時優化JavaScript互操作調用。 - WebAssembly的SIMD和異常處理(WebAssembly SIMD & exception handling):使用WebAssembly SIMD和異常處理支持,提高.NET WebAssembly超時(AOT)編譯的性能。
在.NET 7中Entity Framework Core 7更新有哪些?
JSON列
大多數關係資料庫都支持包含JSON文檔的列,這些列中的JSON可以通過查詢進行鑽取。例如,這允許按文檔內的屬性進行篩選和排序,以及將文檔中的屬性投影到結果中。JSON列允許關係資料庫具有文檔資料庫的某些特征,從而在兩者之間創建有用的混合;它們還可用於消除查詢中的聯接,從而提高性能。
EF7包含對JSON列的提供程式無關支持,以及SQLServer的實現。此支持允許將從.NET類型生成的聚合映射到JSON文檔。可以在聚合上使用普通的LINQ查詢,這些查詢將轉換為鑽取到JSON所需的相應查詢構造。EF7還支持保存對JSON文檔所做的更改。
使用Linq來查詢Json
使用示例
var postsWithViews = await context.Posts.Where(post => post.Metadata!.Views > 3000)
.AsNoTracking()
.Select(
post => new
{
post.Author!.Name,
post.Metadata!.Views,
Searches = post.Metadata.TopSearches,
Commits = post.Metadata.Updates
})
.ToListAsync();
EFCore 7翻譯後的SQL語句為
SELECT [a].[Name],
CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int),
JSON_QUERY([p].[Metadata],'$.TopSearches'),
[p].[Id],
JSON_QUERY([p].[Metadata],'$.Updates')
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int) > 3000
註意的是,JSON_VALUE
、JSON_QUERY
被用來查詢Json文檔的部分。
在保存時更新Json
EF7的更改跟蹤查找需要更新的JSON文檔中最小的單個部分,併發送SQL命令以有效地相應地更新列。例如,考慮修改嵌入在JSON文檔中的單個屬性的代碼:
var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));
arthur.Contact.Address.Country = "United Kingdom";
await context.SaveChangesAsync();
EFCore 7僅為修改的值生成的一個SQL參數
@p0='["United Kingdom"]' (Nullable = false) (Size = 18)
然後使用這個參數,結合JSON_MODIFY
命令進行修改
UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Country', JSON_VALUE(@p0, '$[0]'))
OUTPUT 1
WHERE [Id] = @p1;
更多關於Json列的信息
批量更新和刪除
EFCore跟蹤實體的變化,然後在SaveChangesAsync
被調用時將更新發送到資料庫。只有那些實際發生了變化的屬性和關係才會被髮送。同時,被跟蹤的實體與發送到資料庫的變化保持同步。這種機制是一種高效、便捷的方式,可以向資料庫發送通用的插入、更新和刪除。這些變化也是分批進行的,以減少資料庫的往返次數。
然而,有時在資料庫上執行更新或刪除命令而不載入實體或涉及到變化跟蹤器是很有用的。EF7通過新的ExecuteUpdateAsync
和ExecuteDeleteAsync
方法實現了這一點。這些方法被應用於LINQ查詢,並根據查詢的結果立即更新或刪除資料庫中的實體。許多實體可以用一個命令來更新,而且這些實體不會被載入記憶體。
批量刪除
使用了ExecuteDeleteAsync
的示例代碼
var priorToDateTime = new DateTime(priorToYear, 1, 1);
await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn < priorToDateTime)).ExecuteDeleteAsync();
這將生成“立即從資料庫中刪除所有在給定年份之前發表的帖子的標簽”的SQL。
DELETE FROM [t]
FROM [Tags] AS [t]
WHERE NOT EXISTS (
SELECT 1
FROM [PostTag] AS [p]
INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
WHERE [t].[Id] = [p].[TagsId] AND [p0].[PublishedOn] < @__priorToDateTime_1)
批量更新
使用ExecuteUpdateAsync
與使用ExecuteDeleteAsync
非常相似,只是它需要額外的參數來指定對每條記錄的修改。
例如,考慮下麵這個以調用ExecuteUpdateAsync
結束的LINQ查詢。
var priorToDateTime = new DateTime(priorToYear, 1, 1);
await context.Tags
.Where(t => t.Posts.All(e => e.PublishedOn < priorToDateTime))
.ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)"));
這將生成“立即更新給定年份之前發佈的帖子的所有標簽的"文本"列”的SQL。
UPDATE [t]
SET [t].[Text] = [t].[Text] + N' (old)'
FROM [Tags] AS [t]
WHERE NOT EXISTS (
SELECT 1
FROM [PostTag] AS [p]
INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
WHERE [t].[Id] = [p].[TagsId] AND [p0].[PublishedOn] < @__priorToDateTime_1)
更新或刪除單行
雖然ExecuteUpdateAsync
和ExecuteDeleteAsync
通常用於同時更新或刪除許多行(即"批量"更改),但它們對於高效的單行更改也很有用。
例如,考慮在ASP.NETCore應用程式中刪除一個實體的常見模式。
public async Task<ActionResult> DeletePost(int id)
{
var post = await _context.Posts.FirstOrDefaultAsync(p => p.Id == id);
if (post == null)
{
return NotFound();
}
_context.Posts.Remove(post);
await _context.SaveChangesAsync();
return Ok();
}
如果使用EF 7將可以寫成
public async Task<ActionResult> DeletePost(int id)
=> await _context.Posts.Where(p => p.Id == id).ExecuteDeleteAsync() == 0
? NotFound()
: Ok();
這既是更少的代碼,也是明顯的速度,因為它只執行了一次資料庫往返。
何時使用批量更新
ExecuteUpdateAsync
和ExecuteDeleteAsync
是簡單、明確的更新和刪除的最佳選擇。
然而,請記住,
- 必須明確指定要做的具體變化;EFCore不會自動檢測到這些變化。
- 任何被跟蹤的實體都不會保持同步。
- 可能需要多個命令,而且這些命令的順序要正確,以免違反資料庫約束。例如,在刪除委托人之前,必須先刪除依賴者。
- 對
ExecuteUpdateAsync
和ExecuteDeleteAsync
的多次調用不會被自動包裹在一個事務中。
所有這些意味著ExecuteUpdateAsync
和ExecuteDeleteAsync
是對現有SaveChanges
機制的補充,而不是取代。
更多關於批量更新的信息
- What’s New: ExecuteUpdate and ExecuteDelete (Bulk updates)
- .NET Data Community Standup Video: Bulk updates
- Sample code: ExecuteUpdateAsync
- Sample code: ExecuteDeleteAsync
保存變更更快
在EF 7中,SaveChanges
和SaveChangesAsync
的性能得到了明顯的改善。在某些情況下,現在保存變化的速度比EF Core 6快四倍。這些改進來自於執行更少的往返於資料庫和生成更有效的SQL。
避免非必要的事務
所有現代關係型資料庫都保證了(大多數)單一SQL語句的事務性。也就是說,即使發生錯誤,該語句也不會只完成一部分。
EF 7避免在這些情況下啟動顯式事務。
例如,考慮以下對SaveChangesAsync
的調用,它插入了一個單一實體。
await context.AddAsync(new Blog { Name = "MyBlog" });
await context.SaveChangesAsync();
在EF Core 6中,INSERT命令被開始和提交事務的命令所包裹。
dbug: 9/29/2022 11:43:09.196 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
Began transaction with isolation level 'ReadCommitted'.
info: 9/29/2022 11:43:09.265 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (27ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [Blogs] ([Name])
VALUES (@p0);
SELECT [Id]
FROM [Blogs]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
dbug: 9/29/2022 11:43:09.297 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
Committed transaction.
EF 7檢測到這裡不需要事務,所以刪除了這些調用。
info: 9/29/2022 11:42:34.776 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (25ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
INSERT INTO [Blogs] ([Name])
OUTPUT INSERTED.[Id]
VALUES (@p0);
插入多行
在EF Core 6中,插入多條記錄的預設方法是由SQLServer對帶有觸發器的表的支持的限制所驅動的。這意味著EF Core 6不能使用一個簡單的OUTPUT子句。相反,當插入多個實體時,EF Core 6產生了一些相當複雜的涉及到臨時表的SQL。
例如,考慮對SaveChangesAsync
的調用。
for (var i = 0; i < 4; i++)
{
await context.AddAsync(new Blog { Name = "Foo" + i });
}
await context.SaveChangesAsync();
由EF Core 6生成的SQL如下
dbug: 9/30/2022 17:19:51.919 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
Began transaction with isolation level 'ReadCommitted'.
info: 9/30/2022 17:19:51.993 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (27ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
MERGE [Blogs] USING (
VALUES (@p0, 0),
(@p1, 1),
(@p2, 2),
(@p3, 3)) AS i ([Name], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name])
VALUES (i.[Name])
OUTPUT INSERTED.[Id], i._Position
INTO @inserted0;
SELECT [i].[Id] FROM @inserted0 i
ORDER BY [i].[_Position];
dbug: 9/30/2022 17:19:52.023 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
Committed transaction.
相比之下,EF 7在針對一個沒有觸發器的表時,會生成一條更簡單的命令。
info: 9/30/2022 17:40:37.612 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (4ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
MERGE [Blogs] USING (
VALUES (@p0, 0),
(@p1, 1),
(@p2, 2),
(@p3, 3)) AS i ([Name], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name])
VALUES (i.[Name])
OUTPUT INSERTED.[Id], i._Position;
事務沒有了,就像單次插入的情況一樣,因為MERGE是一個受隱式事務保護的單一語句。另外,臨時表也沒有了,OUTPUT子句現在直接把生成的ID發回給客戶端。這可能比EF Core 6上的速度快四倍,這取決於環境因素,如應用程式和資料庫之間的延遲。
這消除了兩個資料庫往返,這會使整體性能產生巨大影響,尤其是在對資料庫的調用延遲較高時。在典型的生產系統中,資料庫不與應用程式位於同一臺電腦上。這意味著延遲通常相對較高,使得這種優化在實際生產系統中特別有效。
更多關於高性能保存變更的信息
- What’s New: Faster SaveChanges
- .NET Blog: Announcing Entity Framework Core 7 Preview 6–Performance Edition
- .NET Data Community Standup Video: Performance Improvements to the EF7 Update Pipeline
- Sample code: SaveChanges performance
每個具體類型的表(TPC)的繼承映射
預設情況下,EF Core將一個.NET類型的繼承層次映射到一個資料庫表,這被稱為"每層表"(TPH)的映射策略。EF Core 5引入了table-per-type
(TPT)策略,它支持將每個.NET類型映射到一個不同的資料庫表中。EF 7引入了table-per-concrete-type
(TPC)策略。TPC也是將.NET類型映射到不同的表,但其方式是解決TPT策略的一些常見的性能問題。
TPC策略與TPT策略類似,只是為層次結構中的每個具體類型創建不同的表,但不為抽象類型創建表——因此被稱為"每具體類型表"。與TPT一樣,表本身表明瞭保存對象的類型。然而,與TPT映射不同,每個表都包含了具體類型及其基礎類型中每個屬性的列。因此,TPC資料庫模式是非規範化的。
每具體類型表(TPC)
思考以下這樣一個示例
public abstract class Animal
{
public int Id { get; set; }
public string Name { get; set; }
public abstract string Species { get; }
public Food? Food { get; set; }
}
public abstract class Pet : Animal
{
public string? Vet { get; set; }
public ICollection<Human> Humans { get; } = new List<Human>();
}
public class FarmAnimal : Animal
{
public override string Species { get; }
public decimal Value { get; set; }
}
public class Cat : Pet
{
public string EducationLevel { get; set; }
public override string Species => "Felis catus";
}
public class Dog : Pet
{
public string FavoriteToy { get; set; }
public override string Species => "Canis familiaris";
}
public class Human : Animal
{
public override string Species => "Homo sapiens";
public Animal? FavoriteAnimal { get; set; }
public ICollection<Pet> Pets { get; } = new List<Pet>();
}
這是在OnModelCreating
中使用UseTpcMappingStrategy
將其映射到TPC
表的案例:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
}
當使用SQL Server時,為這個層次結構創建的表是:
CREATE TABLE [Cats] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Vet] nvarchar(max) NULL,
[EducationLevel] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));
CREATE TABLE [Dogs] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Vet] nvarchar(max) NULL,
[FavoriteToy] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));
CREATE TABLE [FarmAnimals] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Value] decimal(18,2) NOT NULL,
[Species] nvarchar(max) NOT NULL,
CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));
CREATE TABLE [Humans] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[FavoriteAnimalId] int NULL,
CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));
每具體類型表(TPC)的查詢
TPC生成的查詢比TPT更有效,需要從更少的表中獲取數據,並且利用UNION ALL
代替JOIN
。
例如,對於查詢整個層次結構,EF7會產生:
SELECT [f].[Id], [f].[FoodId], [f].[Name], [f].[Species], [f].[Value], NULL AS [FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'FarmAnimal' AS [Discriminator]
FROM [FarmAnimals] AS [f]
UNION ALL
SELECT [h].[Id], [h].[FoodId], [h].[Name], NULL AS [Species], NULL AS [Value], [h].[FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Human' AS [Discriminator]
FROM [Humans] AS [h]
UNION ALL
SELECT [c].[Id], [c].[FoodId], [c].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
FROM [Cats] AS [c]
UNION ALL
SELECT [d].[Id], [d].[FoodId], [d].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
FROM [Dogs] AS [d]
當查詢一個類型的子集時,情況會變得更好:
SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
FROM [Cats] AS [c]
UNION ALL
SELECT [d].[Id], [d].[FoodId], [d].[Name], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
FROM [Dogs] AS [d]
TPC查詢在對單一葉子類型進行查詢時真的很有優勢:
SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel]
FROM [Cats] AS [c]
更多關於每具體類型表(TPC)的信息
- What’s New: Table-per-concrete-type (TPC) inheritance mapping
- .NET Data Community Standup Video: TPH, TPT, and TPC Inheritance mapping with EF Core
- Sample code: TPC inheritance
定製資料庫逆向工程模板
EF 7支持T4模板,用於在從資料庫逆向工程EF模型時定製支架代碼。預設的模板是通過dotnet
命令添加到項目中的。
dotnet new --install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates
然後,這些模板可以被定製,並將自動被dotnet ef dbcontext scaffold
和Scaffold-DbContext
所使用。
自定義生成後的實體類型
讓我們來看看定製模板是什麼樣子的。
預設情況下,EF Core為集合導航屬性生成了以下代碼。
public virtual ICollection<Album> Albums { get; } = new List<Album>();
對於大多數應用程式來說,使用List<T>
是一個很好的預設值。然而,如果你使用一個基於XAML的框架,如WPF、WinUI或.NET MAUI,你經常想使用ObservableCollection<T>
來實現數據綁定。
EntityType.t4
模板可以被編輯來做這個改變。
例如,下麵的代碼包含在預設模板中。
if (navigation.IsCollection)
{
#>
public virtual ICollection<<#= targetType #>> <#= navigation.Name #> { get; } = new List<<#= targetType #>>();
<#
}
這可以很容易地改成使用ObservableCollection
public virtual ICollection<<#= targetType #>> <#= navigation.Name #> { get; } = new ObservableCollection<<#= targetType #>>();
由於ObservableCollection
在System.Collections.ObjectModel
命名空間中,我們還需要在腳手架的代碼中添加一個using
指令。
var usings = new List<string>
{
"System",
"System.Collections.Generic",
"System.Collections.ObjectModel"
};
更多關於反向工程T4模板的信息
- What’s New: Custom Reverse Engineering Templates
- .NET Data Community Standup Video: Database-first with T4 Templates in EF7–An early look
定製建模轉換
EF Core使用一個元數據"模型"來描述應用程式的實體類型是如何映射到底層資料庫的。這個模型是通過一組大約60個"約定"建立的。然後可以使用映射屬性(又稱"數據註釋")和/或在OnModelCreating
中調用ModelBuilder API
來定製由慣例構建的模型。
從EF 7開始,應用程式可以刪除或替換這些約定,也可以添加新的約定。
模型構建約定是控制模型配置的一個強有力的方法。
移除轉換約定
EF 7允許刪除EF Core使用的預設約定。
例如,為外鍵(FK)列創建索引通常是有意義的,因此,有一個內置的約定用於此。
外鍵索引公約(ForeignKeyIndexConvention
)。然而,在更新行的時候,索引會增加開銷,而且為所有的FK列創建索引可能並不總是合適。
為了達到這個目的,在建立模型時可以刪除ForeignKeyIndexConvention
。
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}
增加轉換約定
EF Core用戶的一個共同要求是為所有字元串屬性設置一個預設長度。這在EF 7中可以通過編寫一個公約來實現。
public class MaxStringLengthConvention : IModelFinalizingConvention
{
private readonly int _maxLength;
public MaxStringLengthConvention(int maxLength)
{
_maxLength = maxLength;
}
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
{
foreach (var property in modelBuilder.Metadata.GetEntityTypes()
.SelectMany(
e => e.GetDeclaredProperties()
.Where(p => p.ClrType == typeof(string))))
{
property.Builder.HasMaxLength(_maxLength);
}
}
}
這個約定是非常簡單的。
它找到模型中的每個字元串屬性,並將其最大長度設置為指定值。
然而,使用這樣的約定的關鍵之處在於,那些使用[MaxLength]
或[StringLength]
屬性或OnModelCreating
中的HasMaxLength
明確設置其最大長度的屬性將保留其明確值。
換句話說,只有在沒有指定其他長度的情況下,才會使用該約定所設置的預設值。
這個新約定可以通過在ConfigureConventions
中添加它來使用。
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(_ => new MaxStringLengthConvention(256));
}
更多關於建模轉換的信息
- What’s New: Model building conventions
- .NET Data Community Standup Video: EF7 Custom Model Conventions
- Sample code: Model building conventions
插入/更新/刪除的存儲過程映射
預設情況下,EF Core生成的插入、更新和刪除命令是直接與表或可更新的視圖一起工作。
EF 7引入了對這些命令到存儲過程的映射的支持。
在OnModelCreating
中使用InsertUsingStoredProcedure
、UpdateUsingStoredProcedure
和DeleteUsingStoredProcedure
來映射存儲過程。
例如,要為Person
實體類型映射存儲過程。
modelBuilder.Entity<Person>()
.InsertUsingStoredProcedure(
"People_Insert",
storedProcedureBuilder =>
{
storedProcedureBuilder.HasParameter(a => a.Name);
storedProcedureBuilder.HasResultColumn(a => a.Id);
})
.UpdateUsingStoredProcedure(
"People_Update",
storedProcedureBuilder =>
{
storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
storedProcedureBuilder.HasParameter(person => person.Name);
storedProcedureBuilder.HasRowsAffectedResultColumn();
})
.DeleteUsingStoredProcedure(
"People_Delete",
storedProcedureBuilder =>
{
storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
storedProcedureBuilder.HasRowsAffectedResultColumn();
});
在使用SQL Server時,該配置映射到以下存儲過程。
針對插入場景
CREATE PROCEDURE [dbo].[People_Insert]
@Name [nvarchar](max)
AS
BEGIN
INSERT INTO [People] ([Name])
OUTPUT INSERTED.[Id]
VALUES (@Name);
END
針對更新場景
CREATE PROCEDURE [dbo].[People_Update]
@Id [int],
@Name_Original [nvarchar](max),
@Name [nvarchar](max)
AS
BEGIN
UPDATE [People] SET [Name] = @Name
WHERE [Id] = @Id AND [Name] = @Name_Original
SELECT @@ROWCOUNT
END
針對刪除場景
CREATE PROCEDURE [dbo].[People_Delete]
@Id [int],
@Name_Original [nvarchar](max)
AS
BEGIN
DELETE FROM [People]
OUTPUT 1
WHERE [Id] = @Id AND [Name] = @Name_Original;
END
然後在調用SaveChangesAsync
時使用這些存儲程式。
SET NOCOUNT ON;
EXEC [People_Update] @p1, @p2, @p3;
EXEC [People_Update] @p4, @p5, @p6;
EXEC [People_Delete] @p7, @p8;
EXEC [People_Delete] @p9, @p10;
更多關於存儲過程映射的信息
全新的改進後的攔截器和事件
EF Core攔截器能夠攔截、修改和/或抑制EFCore操作。EF Core還包括傳統的.NET事件和日誌記錄。
EF 7包括以下攔截器的增強功能
- 攔截創建和填充新的實體實例(又稱"實體化")
- 攔截器可以在編譯查詢之前修改LINQ表達式樹
- 攔截樂觀的併發性處理(
DbUpdateConcurrencyException
) - 在檢查連接字元串是否已被設置之前,對連接進行攔截
- 當EF Core消耗完一個結果集後,在該結果集被關閉之前進行攔截
- 對EF Core創建
DbConnection
的攔截 - 對
DbCommand
初始化後的攔截
此外,EF7還包括新的傳統的.NET事件,用於:
- 當一個實體即將被追蹤或改變狀態時,但在它實際被追蹤或改變狀態之前
- 在EF Core檢測到實體和屬性的變化之前和之後(又稱
DetectChanges
攔截)
Materialization攔截器
新的IMaterializationInterceptor
支持在實體實例被創建前後以及該實例的屬性被初始化前後進行攔截。
攔截器可以在每個點上改變或替換實體實例。這允許:
- 設置未映射的屬性或調用驗證、計算值或標誌所需的方法。
- 使用一個工廠來創建實例。
- 創建與EF通常創建的不同的實體實例,如來自緩存的實例,或代理類型的實例。
- 將服務註入到實體實例中。
例如,想象一下,我們想跟蹤一個實體從資料庫中檢索的時間,也許這樣就可以顯示給編輯數據的用戶。為此可以創建一個物化攔截器。
public class SetRetrievedInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
{
if (instance is IHasRetrieved hasRetrieved)
{
hasRetrieved.Retrieved = DateTime.UtcNow;
}
return instance;
}
}
在配置DbContext
時,這個攔截器的一個實例被註冊。
public class CustomerContext : DbContext
{
private static readonly SetRetrievedInterceptor _setRetrievedInterceptor = new();
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.AddInterceptors(_setRetrievedInterceptor)
.UseSqlite("Data Source = customers.db");
}
現在,每當從資料庫查詢一個客戶時,Retrieved
屬性將被自動設置,比如說。
await using (var context = new CustomerContext())
{
var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
Console.WriteLine($"Customer '{customer.Name}' was retrieved at '{customer.Retrieved.ToLocalTime()}'");
}
進程的輸出為
Customer 'Alice' was retrieved at '9/22/2022 5:25:54 PM'
連接字元串的延遲初始化
連接字元串通常是從配置文件中讀取的靜態資產。在配置DbContext
時,這些可以很容易地傳遞給UseSqlServer
或類似的東西。然而,連接字元串有時可以為每個上下文實例而改變。
例如,在一個多租戶系統中,每個租戶可能有不同的連接字元串。
EF 7通過對IDbConnectionInterceptor
的改進,使其更容易處理動態連接和連接字元串。
這首先是在沒有任何連接字元串的情況下配置DbContext
的能力。比如說:
services.AddDbContext<CustomerContext>(
b => b.UseSqlServer();
然後,可以實現IDbConnectionInterceptor
方法之一,在使用連接之前對其進行配置。
ConnectionOpeningAsync
是一個很好的選擇,因為它可以執行一個非同步操作來獲取連接字元串,找到一個訪問令牌,等等。
public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
{
private readonly IClientConnectionStringFactory _connectionStringFactory;
public ConnectionStringInitializationInterceptor(IClientConnectionStringFactory connectionStringFactory)
{
_connectionStringFactory = connectionStringFactory;
}
public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
DbConnection connection, ConnectionEventData eventData, InterceptionResult result,
CancellationToken cancellationToken = new())
{
if (string.IsNullOrEmpty(connection.ConnectionString))
{
connection.ConnectionString = (await _connectionStringFactory.GetConnectionStringAsync(cancellationToken));
}
return result;
}
}
請註意,只有在第一次使用連接時才會獲得連接字元串。在那之後,存儲在
DbConnection
上的連接字元串將被使用,而無需查找新的連接字元串。
更多關於攔截器和事件的信息
- What’s New: New and improved interceptors and events
- .NET Data Community Standup Video: Intercept this EF7 Preview 6 Event
- Sample code: Materialization interception
- Sample code: Connection string interception
參考
- ASP.NET Core 7.0 的新增功能
- 從ASP.NET Core 6.0遷移到7.0
- .NET Live TV
- .NET 7 is Available Today
- Announcing ASP.NET Core in .NET 7
- Announcing .NET MAUI for .NET 7 General Availability
- What’s new for WPF in .NET 7
- What’s new in Windows Forms in .NET 7.0
- Entity Framework Core 7 (EF7) is available today
- Welcome to C# 11
- Announcing F# 7
- What’s new in Orleans 7.0
- 使用Windows包管理器 (winget) 進行安裝
- 乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - 獲取.Net 7並查看.Net 7中的性能提升(簡中譯文)
- 乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - 伴隨.Net6/7與時俱進的.Net CLI命令行介面
- 乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - .NET 7預覽和RC1,內置MAUI、幫助.NET應用程式現代化升級
- 乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - 淺析ASP.NET Core遠程過程調用,HttpClientFactory和gRPC最佳實踐
- 統一的開發平臺.NET 7正式發佈
- 微軟發佈.NET 7,Visual Studio 2022 17.4原生支持Arm64架構