乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - .NET 7正式發佈,看看ASP.NET Core 7.0和EF Core 7新增哪些功能

2022年11月8日.NET 7正式發佈

.NET仍然是最快、最受歡迎、最值得信賴的平臺之一,其龐大的.NET軟件包生態系統包括33萬多個軟件包。

image

.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項貢獻!

image

.NET 7的發佈是我們.NET統一之旅的第三個主要版本(自2016年的.NET 5以來)。

有了.NET 7,你可以通過一個SDK、一個運行時、一套基礎庫來構建多種類型的應用程序(雲、網絡、桌面、移動、遊戲、物聯網和人工智能),一次學習並重復使用你的技能。

.NET 7支持週期

image

.NET 7得到了微軟的正式支持。它被標記爲一個標準期限支持(STS)版本,將被支持18個月。奇數的.NET版本是STS版本,在隨後的STS或LTS版本之後,可以獲得6個月的免費支持和補丁。

獲取

從官網獲取

https://dotnet.microsoft.com/zh-cn/download/dotnet/7.0

image

安裝SDK

安裝桌面運行時

安裝ASP.NET Core運行時

安裝.NET運行時

通過Nuget獲取

安裝SDK

winget install Microsoft.DotNet.SDK.7

image

安裝桌面運行時

winget install Microsoft.DotNet.DesktopRuntime.7

image

安裝ASP.NET Core運行時

winget install Microsoft.DotNet.AspNetCore.7

image

安裝.NET運行時

winget install Microsoft.DotNet.Runtime.7

image

在.NET 7中ASP.NET Core更新有哪些?

ASP.NET Core 7.0 的新增功能

服務與運行時(Servers and runtime)

最小API(Minimal APIs)

遠程調用(gRPC)

實時應用(SignalR)

MVC

客戶端Web應用(Blazor)

在.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_VALUEJSON_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通過新的ExecuteUpdateAsyncExecuteDeleteAsync方法實現了這一點。這些方法被應用於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)

更新或刪除單行

雖然ExecuteUpdateAsyncExecuteDeleteAsync通常用於同時更新或刪除許多行(即"批量"更改),但它們對於高效的單行更改也很有用

例如,考慮在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();

這既是更少的代碼,也是明顯的速度,因爲它只執行了一次數據庫往返

何時使用批量更新

ExecuteUpdateAsyncExecuteDeleteAsync是簡單、明確的更新和刪除的最佳選擇

然而,請記住,

  • 必須明確指定要做的具體變化;EFCore不會自動檢測到這些變化。
  • 任何被跟蹤的實體都不會保持同步。
  • 可能需要多個命令,而且這些命令的順序要正確,以免違反數據庫約束。例如,在刪除委託人之前,必須先刪除依賴者。
  • ExecuteUpdateAsyncExecuteDeleteAsync的多次調用不會被自動包裹在一個事務中。

所有這些意味着ExecuteUpdateAsyncExecuteDeleteAsync是對現有SaveChanges機制的補充,而不是取代。

更多關於批量更新的信息

保存變更更快

在EF 7中,SaveChangesSaveChangesAsync的性能得到了明顯的改善。在某些情況下,現在保存變化的速度比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上的速度快四倍,這取決於環境因素,如應用程序和數據庫之間的延遲。

這消除了兩個數據庫往返,這會使整體性能產生巨大影響,尤其是在對數據庫的調用延遲較高時。在典型的生產系統中,數據庫不與應用程序位於同一臺計算機上。這意味着延遲通常相對較高,使得這種優化在實際生產系統中特別有效

更多關於高性能保存變更的信息

每個具體類型的表(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)的信息

定製數據庫逆向工程模板

EF 7支持T4模板,用於在從數據庫逆向工程EF模型時定製支架代碼。默認的模板是通過dotnet命令添加到項目中的。

dotnet new --install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates

然後,這些模板可以被定製,並將自動被dotnet ef dbcontext scaffoldScaffold-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 #>>();

由於ObservableCollectionSystem.Collections.ObjectModel命名空間中,我們還需要在腳手架的代碼中添加一個using指令。

var usings = new List<string>
{
    "System",
    "System.Collections.Generic",
    "System.Collections.ObjectModel"
};

更多關於反向工程T4模板的信息

定製建模轉換

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

更多關於建模轉換的信息

插入/更新/刪除的存儲過程映射

默認情況下,EF Core生成的插入、更新和刪除命令是直接與表或可更新的視圖一起工作。

EF 7引入了對這些命令到存儲過程的映射的支持

OnModelCreating中使用InsertUsingStoredProcedureUpdateUsingStoredProcedureDeleteUsingStoredProcedure來映射存儲過程。

例如,要爲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上的連接字符串將被使用,而無需查找新的連接字符串。

更多關於攔截器和事件的信息

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章