EF Core 6.0 的新功能

EF Core 6.0 已 寄送至 NuGet 。 此頁面包含此版本中引進的有趣變更概觀。

提示

您可以從 GitHub 下載範例程式碼,以執行並偵錯如下所示 的範例。

SQL Server 時態表

GitHub 問題: #4693

SQL Server 時態表 會自動追蹤儲存在資料表中的所有資料,即使該資料已更新或刪除。 建立平行的「歷程記錄資料表」,每當對主資料表進行變更時,就會儲存時間戳記歷程記錄資料。 這可允許查詢歷程記錄資料,例如稽核或還原,例如在意外突變或刪除之後進行復原。

EF Core 現在支援:

  • 使用移轉建立時態表
  • 使用移轉再次將現有資料表轉換成時態表
  • 查詢歷程記錄資料
  • 從過去某個時間點還原資料

設定時態表

模型產生器可用來將資料表設定為時態。 例如:

modelBuilder
    .Entity<Employee>()
    .ToTable("Employees", b => b.IsTemporal());

使用 EF Core 建立資料庫時,新資料表會設定為時態表,並具有時間戳記和歷程記錄資料表的 SQL Server 預設值。 例如,請考慮實體 Employee 類型:

public class Employee
{
    public Guid EmployeeId { get; set; }
    public string Name { get; set; }
    public string Position { get; set; }
    public string Department { get; set; }
    public string Address { get; set; }
    public decimal AnnualSalary { get; set; }
}

建立的時態表看起來會像這樣:

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [PeriodEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    [PeriodStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([PeriodStart], [PeriodEnd])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistory]))');

請注意,SQL Server 會建立兩個稱為 PeriodEndPeriodStart 的隱藏 datetime2 資料行。 這些「期間資料行」代表資料列中的資料存在的時間範圍。 這些資料行會對應至 EF Core 模型中的陰影屬性 ,使其可在查詢中使用,如稍後所示。

重要

這些資料行中的時間一律是 SQL Server 所產生的 UTC 時間。 UTC 時間用於涉及時態表的所有作業,例如在如下所示的查詢中。

另請注意,系統會自動建立名為 EmployeeHistory 的相關聯歷程記錄資料表。 期間資料行和歷程記錄資料表的名稱可以透過模型產生器的其他組態來變更。 例如:

modelBuilder
    .Entity<Employee>()
    .ToTable(
        "Employees",
        b => b.IsTemporal(
            b =>
            {
                b.HasPeriodStart("ValidFrom");
                b.HasPeriodEnd("ValidTo");
                b.UseHistoryTable("EmployeeHistoricalData");
            }));

這會反映在 SQL Server 所建立的資料表中:

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [ValidFrom] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    [ValidTo] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([ValidFrom], [ValidTo])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistoricalData]))');

使用時態表

大部分時候,時態表會和任何其他資料表一樣使用。 也就是說,SQL Server 會以透明方式處理期間資料行和歷程記錄資料,讓應用程式可以忽略這些資料行。 例如,新的實體可以正常方式儲存到資料庫:

context.AddRange(
    new Employee
    {
        Name = "Pinky Pie",
        Address = "Sugarcube Corner, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Party Organizer",
        AnnualSalary = 100.0m
    },
    new Employee
    {
        Name = "Rainbow Dash",
        Address = "Cloudominium, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Ponyville weather patrol",
        AnnualSalary = 900.0m
    },
    new Employee
    {
        Name = "Fluttershy",
        Address = "Everfree Forest, Equestria",
        Department = "DevDiv",
        Position = "Animal caretaker",
        AnnualSalary = 30.0m
    });

context.SaveChanges();

然後,就可以以一般方式查詢、更新和刪除此資料。 例如:

var employee = context.Employees.Single(e => e.Name == "Rainbow Dash");
context.Remove(employee);
context.SaveChanges();

此外,在一般 追蹤查詢 之後,可以從 追蹤的實體 存取目前資料句點資料行的值。 例如:

var employees = context.Employees.ToList();
foreach (var employee in employees)
{
    var employeeEntry = context.Entry(employee);
    var validFrom = employeeEntry.Property<DateTime>("ValidFrom").CurrentValue;
    var validTo = employeeEntry.Property<DateTime>("ValidTo").CurrentValue;

    Console.WriteLine($"  Employee {employee.Name} valid from {validFrom} to {validTo}");
}

這會列印:

Starting data:
  Employee Pinky Pie valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Rainbow Dash valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Fluttershy valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM

請注意,資料 ValidTo 行 (預設稱為 PeriodEnd ) 包含 datetime2 最大值。 這一律是資料表中目前資料列的情況。 資料 ValidFrom 行(預設稱為 PeriodStart )包含插入資料列的 UTC 時間。

查詢歷程記錄資料

EF Core 支援透過數個新的查詢運算子包含歷程記錄資料的查詢:

  • TemporalAsOf:傳回在指定 UTC 時間作用中(目前)的資料列。 這是目前資料表或記錄資料表中指定主鍵的單一資料列。
  • TemporalAll:傳回曆程記錄資料中的所有資料列。 這通常是來自記錄資料表和/或指定主鍵之目前資料表的許多資料列。
  • TemporalFromTo:傳回在兩個指定 UTC 時間之間作用中的所有資料列。 這可能是來自記錄資料表和/或指定主鍵之目前資料表的許多資料列。
  • TemporalBetween:與 TemporalFromTo 相同,不同之處在于,在上限上變成作用中的資料列除外。
  • TemporalContainedIn:傳回開始使用中且結束在兩個指定 UTC 時間之間使用的所有資料列。 這可能是來自記錄資料表和/或指定主鍵之目前資料表的許多資料列。

例如,在對資料進行一些更新和刪除之後,我們可以使用 來執行查詢 TemporalAll 來查看歷程記錄資料:

var history = context
    .Employees
    .TemporalAll()
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToList();

foreach (var pointInTime in history)
{
    Console.WriteLine(
        $"  Employee {pointInTime.Employee.Name} was '{pointInTime.Employee.Position}' from {pointInTime.ValidFrom} to {pointInTime.ValidTo}");
}

請注意 EF 的方式 。屬性方法 可用來存取句點資料行的值。 這會用於 OrderBy 子句來排序資料,然後在投影中,將這些值包含在傳回的資料中。

此查詢會帶回下列資料:

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM

請注意,在 2021 年 8 月 26 日下午 4:44:59,最後一個資料列已停止作用中。 這是因為當時已從主資料表中刪除彩虹虛線的資料列。 我們稍後將瞭解如何還原此資料。

類似的查詢可以使用 、 TemporalBetweenTemporalContainedIn 撰寫 TemporalFromTo 。 例如:

var history = context
    .Employees
    .TemporalBetween(timeStamp2, timeStamp3)
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToList();

此查詢會傳回下列資料列:

Historical data for Rainbow Dash between 8/26/2021 4:41:14 PM and 8/26/2021 4:42:44 PM:
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM

還原歷程記錄資料

如上所述,彩虹虛線已從 Employees 資料表中刪除。 這顯然是個錯誤,因此讓我們回到某個時間點,並從該時間還原遺漏的資料列。

var employee = context
    .Employees
    .TemporalAsOf(timeStamp2)
    .Single(e => e.Name == "Rainbow Dash");

context.Add(employee);
context.SaveChanges();

此查詢會傳回彩虹虛線的單一資料列,如同在指定的 UTC 時間一樣。 根據預設,所有使用時態運算子的查詢都不會追蹤,因此不會追蹤此處傳回的實體。 這很合理,因為它目前不存在於主資料表中。 若要將實體重新插入主資料表中,我們只會將其標示為 Added ,然後呼叫 SaveChanges

重新插入資料列彩虹虛線之後,查詢歷程記錄資料會顯示資料列在指定的 UTC 時間還原:

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:44:59 PM to 12/31/9999 11:59:59 PM

移轉套件組合

GitHub 問題: #19693

EF Core 移轉可用來根據 EF 模型的變更來產生資料庫架構更新。 這些架構更新應該在應用程式部署期間套用,通常是持續整合/持續部署(C.I./C.D.) 系統的一部分。

EF Core 現在包含套用這些架構更新的新方法:移轉套件組合。 移轉套件組合是一個小型可執行檔,其中包含移轉,以及將這些移轉套用至資料庫所需的程式碼。

注意

如需有關移轉、套件組合和部署的深入討論,請參閱 .NET 部落格上的 DevOps 易記 EF Core 移 轉套件組合簡介。

移轉套件組合是使用命令列工具建立的 dotnet ef 。 請確定您已安裝 最新版的工具 ,再繼續進行。

套件組合需要移轉才能包含。 這些是使用 dotnet ef migrations add 建立的,如移轉檔 中所述 。 當您準備好部署移轉之後,請使用 dotnet ef migrations bundle 建立套件組合。 例如:

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

輸出是適合目標作業系統的可執行檔。 在此情況下,這是 Windows x64,所以我會在本機資料夾中取得 efbundle.exe 卸載。 執行此可執行檔會套用其內含的移轉:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903083845_MyMigration'.
Done.
PS C:\local\AllTogetherNow\SixOh>

只有在尚未套用移轉時,才會將移轉套用至資料庫。 例如,再次執行相同的套件組合不會執行任何動作,因為沒有要套用的新移轉:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
No migrations were applied. The database is already up to date.
Done.
PS C:\local\AllTogetherNow\SixOh>

不過,如果對模型進行變更,並使用 產生 dotnet ef migrations add 更多移轉,則這些變更可以組合成可套用的新可執行檔。 例如:

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add SecondMigration
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add Number3
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle --force
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

請注意, --force 選項可用來以新的套件組合覆寫現有的套件組合。

執行這個新的套件組合會將這兩個新的移轉套用至資料庫:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

根據預設,套件組合會使用應用程式組態中的資料庫連接字串。 不過,您可以藉由在命令列上傳遞連接字串來移轉不同的資料庫。 例如:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe --connection "Data Source=(LocalDb)\MSSQLLocalDB;Database=SixOhProduction"
Applying migration '20210903083845_MyMigration'.
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

請注意,這次會套用這三個移轉,因為它們都尚未套用至生產資料庫。

其他選項可以傳遞至命令列。 一些常見的選項包括:

  • --output ,指定要建立的可執行檔路徑。
  • --context 指定當專案包含多個內容類型時要使用的 DbCoNtext 類型。
  • --project 表示指定要使用的專案。 預設為目前的工作目錄。
  • --startup-project 表示指定要使用的啟動專案。 預設為目前的工作目錄。
  • --no-build 表示在執行 命令之前,無法建置專案。 只有當已知專案為最新狀態時,才應該使用此專案。
  • --verbose 以查看命令執行作業的詳細資訊。 在 Bug 報告中包含資訊時,請使用此選項。

使用 dotnet ef migrations bundle --help 以查看所有可用的選項。

請注意,根據預設,每個移轉都會在自己的交易中套用。 請參閱 GitHub 問題 #22616 ,以討論此領域的可能未來增強功能。

預先慣例模型設定

GitHub 問題: #12229

舊版 EF Core 要求當對應與預設值不同時,明確設定指定類型之每個屬性的對應。 這包括「Facet」,例如字串的最大長度和十進位有效位數,以及屬性類型的值轉換。

這需要:

  • 每個屬性的模型產生器組態
  • 每個屬性上的對應屬性
  • 在建置模型時,明確反復查看所有實體類型的所有屬性,以及使用低階中繼資料 API。

請注意,明確反復專案易發生錯誤且難以執行,因為此反復專案發生時,實體類型和對應的屬性清單可能不是最終專案。

EF Core 6.0 允許指定型別指定此對應組態一次。 然後,它會套用至模型中該類型的所有屬性。 這稱為「慣例前模型組態」,因為它會設定模型建置慣例接著使用的模型層面。 透過覆寫 ConfigureConventions 您的 DbContext 來套用這類組態:

public class SomeDbContext : DbContext
{
    protected override void ConfigureConventions(
        ModelConfigurationBuilder configurationBuilder)
    {
        // Pre-convention model configuration goes here
    }
}

例如,請考慮下列實體類型:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public Money AccountValue { get; set; }

    public Session CurrentSession { get; set; }

    public ICollection<Order> Orders { get; } = new List<Order>();
}

public class Order
{
    public int Id { get; set; }
    public string SpecialInstructions { get; set; }
    public DateTime OrderDate { get; set; }
    public bool IsComplete { get; set; }
    public Money Price { get; set; }
    public Money? Discount { get; set; }

    public Customer Customer { get; set; }
}

所有字串屬性都可以設定為 ANSI (而非 Unicode),且長度上限為 1024:

configurationBuilder
    .Properties<string>()
    .AreUnicode(false)
    .HaveMaxLength(1024);

所有 DateTime 屬性都可以轉換成資料庫中的 64 位整數,使用從 DateTimes 到 longs 的預設轉換:

configurationBuilder
    .Properties<DateTime>()
    .HaveConversion<long>();

所有 bool 屬性都可以轉換成整數 0 ,或使用 1 其中一個內建值轉換器:

configurationBuilder
    .Properties<bool>()
    .HaveConversion<BoolToZeroOneConverter<int>>();

假設 Session 是實體的暫時性屬性,而且不應該保存,則可以在模型中的任何地方忽略它:

configurationBuilder
    .IgnoreAny<Session>();

使用值物件時,預先慣例模型組態非常有用。 例如,上述模型中的類型 Money 是由唯讀結構表示:

public readonly struct Money
{
    [JsonConstructor]
    public Money(decimal amount, Currency currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public override string ToString()
        => (Currency == Currency.UsDollars ? "$" : "£") + Amount;

    public decimal Amount { get; }
    public Currency Currency { get; }
}

public enum Currency
{
    UsDollars,
    PoundsSterling
}

接著,這會使用自訂值轉換器從 JSON 序列化至 JSON 和從中序列化:

public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter()
        : base(
            v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
            v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null))
    {
    }
}

您可以針對所有 Money 用途設定此值轉換器一次:

configurationBuilder
    .Properties<Money>()
    .HaveConversion<MoneyConverter>()
    .HaveMaxLength(64);

另請注意,可以針對序列化 JSON 儲存所在的字串資料行指定其他 Facet。 在此情況下,資料行的長度上限為 64。

使用移轉為 SQL Server 建立的資料表會顯示設定如何套用至所有對應的資料行:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] varchar(1024) NULL,
    [IsActive] int NOT NULL,
    [AccountValue] nvarchar(64) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
CREATE TABLE [Order] (
    [Id] int NOT NULL IDENTITY,
    [SpecialInstructions] varchar(1024) NULL,
    [OrderDate] bigint NOT NULL,
    [IsComplete] int NOT NULL,
    [Price] nvarchar(64) NOT NULL,
    [Discount] nvarchar(64) NULL,
    [CustomerId] int NULL,
    CONSTRAINT [PK_Order] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Order_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id])
);

您也可以指定指定類型的預設類型對應。 例如:

configurationBuilder
    .DefaultTypeMapping<string>()
    .IsUnicode(false);

這很少需要,但如果在查詢中使用型別的方式與模型的任何對應屬性無關,就很有用。

注意

如需預先慣例模型設定的更多討論和範例,請參閱 宣佈 Entity Framework Core 6.0 Preview 6:在 .NET 部落格上設定慣例

已編譯的模型

GitHub 問題: #1906

已編譯的模型可以改善具有大型模型之應用程式的 EF Core 啟動時間。 大型模型通常表示 100 到 1000 個實體類型和關聯性。

啟動時程表示當應用程式第一次使用該 DbCoNtext 類型時,在 DbCoNtext 上執行第一個作業的時間。 請注意,只要建立 DbCoNtext 實例並不會使 EF 模型初始化。 相反地,導致模型初始化的典型第一個作業包括呼叫 DbContext.Add 或執行第一個查詢。

編譯的模型是使用 dotnet ef 命令列工具建立的。 請確定您已安裝 最新版的工具 ,再繼續進行。

新的 dbcontext optimize 命令可用來產生已編譯的模型。 例如:

dotnet ef dbcontext optimize

--output-dir--namespace 選項可用來指定將產生編譯模型所在的目錄和命名空間。 例如:

PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>

執行此命令的輸出包含一段程式碼,以複製並貼到 DbCoNtext 組態中,讓 EF Core 使用編譯的模型。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseModel(MyCompiledModels.BlogsContextModel.Instance)
        .UseSqlite(@"Data Source=test.db");

已編譯的模型啟動載入

通常不需要查看產生的開機載入程式代碼。 不過,有時候自訂模型或其載入可能會很有用。 開機載入程式代碼看起來像這樣:

[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
    private static BlogsContextModel _instance;
    public static IModel Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BlogsContextModel();
                _instance.Initialize();
                _instance.Customize();
            }

            return _instance;
        }
    }

    partial void Initialize();

    partial void Customize();
}

這是具有部分方法的部分類別,可視需要實作以自訂模型。

此外,您可以針對 DbCoNtext 類型產生多個編譯模型,這些類型可能會根據某些執行時間組態使用不同的模型。 這些應該放在不同的資料夾和命名空間中,如上所示。 接著可以檢查執行時間資訊,例如連接字串,並視需要傳回正確的模型。 例如:

public static class RuntimeModelCache
{
    private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
        = new();

    public static IModel GetOrCreateModel(string connectionString)
        => _runtimeModels.GetOrAdd(
            connectionString, cs =>
            {
                if (cs.Contains("X"))
                {
                    return BlogsContextModel1.Instance;
                }

                if (cs.Contains("Y"))
                {
                    return BlogsContextModel2.Instance;
                }

                throw new InvalidOperationException("No appropriate compiled model found.");
            });
}

限制

編譯的模型有一些限制:

  • 不支援 全域查詢篩選。
  • 不支援 延遲載入和變更追蹤 Proxy。
  • 不支援自訂 IModelCacheKeyFactory 實作。 不過,您可以編譯多個模型,並視需要載入適當的模型。
  • 每次模型定義或組態變更 時,都必須重新產生模型,以手動同步處理模型。

由於這些限制,只有在 EF Core 啟動時間太慢時,才應該使用已編譯的模型。 編譯小型模型通常不值得。

如果支援上述任何功能對於您的成功至關重要,請投票處理上述連結的適當問題。

效能評定

提示

您可以從 GitHub 下載範例程式碼,以嘗試編譯大型模型並執行 基準測試。

上述 GitHub 存放庫中的模型包含 449 個實體類型、6390 屬性和 720 關聯性。 這是中等大型模型。 使用 BenchmarkDotNet 測量時,第一次查詢的平均時間在相當強大的膝上型電腦上是 1.02 秒。 使用已編譯的模型,這可讓相同硬體上的毫秒降到 117 毫秒。 8x 到 10 倍的改善,就像模型大小增加一樣保持相對不變。

Compiled model performance improvement

注意

請參閱 宣佈 Entity Framework Core 6.0 Preview 5:.NET 部落格上的已編譯模型 ,以深入瞭解 EF Core 啟動效能和已編譯的模型。

改善 TechEmpower 財富的效能

GitHub 問題: #23611

我們已大幅改善 EF Core 6.0 的查詢效能。 具體而言:

  • 與 5.0 相比,業界標準 TechEmpower Fortunes 基準的 EF Core 6.0 效能現在快 70%。
    • 這是完整的堆疊效能改善,包括效能評定程式碼、.NET 執行時間等的改善。
  • EF Core 6.0 本身的執行未追蹤查詢速度加快 31%。
  • 執行查詢時,堆積配置已減少 43%。

經過這些改進,TechEmpower Fortunes 基準中流行的「微型 ORM」 Dapper 和 EF Core 之間的差距從 55% 縮小到略低於 5%。

注意

如需 EF Core 6.0 中查詢效能改善的詳細討論,請參閱 .NET 部落格上的宣佈 Entity Framework Core 6.0 Preview 4:Performance Edition

Azure Cosmos DB 提供者增強功能

EF Core 6.0 包含 Azure Cosmos DB 資料庫提供者的許多改善。

預設為隱含擁有權

GitHub 問題: #24803

建置 Azure Cosmos DB 提供者的模型時,EF Core 6.0 預設會將子實體類型標示為其父實體所擁有的。 這可移除 Azure Cosmos DB 模型中大部分 OwnsManyOwnsOne 呼叫的需求。 這可讓您更輕鬆地將子類型內嵌至父型別的檔,這通常是在檔資料庫中建立父系和子系模型的適當方式。

例如,請考慮下列實體類型:

public class Family
{
    [JsonPropertyName("id")]
    public string Id { get; set; }
    
    public string LastName { get; set; }
    public bool IsRegistered { get; set; }
    
    public Address Address { get; set; }

    public IList<Parent> Parents { get; } = new List<Parent>();
    public IList<Child> Children { get; } = new List<Child>();
}

public class Parent
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
}

public class Child
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
    public int Grade { get; set; }

    public string Gender { get; set; }

    public IList<Pet> Pets { get; } = new List<Pet>();
}

在 EF Core 5.0 中,這些類型會針對具有下列設定的 Azure Cosmos DB 進行模型化:

modelBuilder.Entity<Family>()
    .HasPartitionKey(e => e.LastName)
    .OwnsMany(f => f.Parents);

modelBuilder.Entity<Family>()
    .OwnsMany(f => f.Children)
    .OwnsMany(c => c.Pets);

modelBuilder.Entity<Family>()
    .OwnsOne(f => f.Address);        

在 EF Core 6.0 中,擁有權是隱含的,因此會將模型組態減少為:

modelBuilder.Entity<Family>().HasPartitionKey(e => e.LastName);

產生的 Azure Cosmos DB 檔具有家族內嵌在家庭檔中的父母、兒童、寵物和位址。 例如:

{
  "Id": "Wakefield.7",
  "LastName": "Wakefield",
  "Discriminator": "Family",
  "IsRegistered": true,
  "id": "Family|Wakefield.7",
  "Address": {
    "City": "NY",
    "County": "Manhattan",
    "State": "NY"
  },
  "Children": [
    {
      "FamilyName": "Merriam",
      "FirstName": "Jesse",
      "Gender": "female",
      "Grade": 8,
      "Pets": [
        {
          "GivenName": "Goofy"
        },
        {
          "GivenName": "Shadow"
        }
      ]
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Lisa",
      "Gender": "female",
      "Grade": 1,
      "Pets": []
    }
  ],
  "Parents": [
    {
      "FamilyName": "Wakefield",
      "FirstName": "Robin"
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Ben"
    }
  ],
  "_rid": "x918AKh6p20CAAAAAAAAAA==",
  "_self": "dbs/x918AA==/colls/x918AKh6p20=/docs/x918AKh6p20CAAAAAAAAAA==/",
  "_etag": "\"00000000-0000-0000-adee-87f30c8c01d7\"",
  "_attachments": "attachments/",
  "_ts": 1632121802
}

注意

請務必記住, OwnsOne/OwnsMany 如果您需要進一步設定這些擁有的類型,則必須使用設定。

基本類型集合

GitHub 問題: #14762

使用 Azure Cosmos DB 資料庫提供者時,EF Core 6.0 會以原生方式對應基本類型的集合。 例如,請考慮以下的實體類型:

public class Book
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IList<string> Quotes { get; set; }
    public IDictionary<string, string> Notes { get; set; }
}

清單和字典都可使用一般方式填入並插入資料庫:

using var context = new BooksContext();

var book = new Book
{
    Title = "How It Works: Incredible History",
    Quotes = new List<string>
    {
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    },
    Notes = new Dictionary<string, string>
    {
        { "121", "Fridges" },
        { "144", "Peter Higgs" },
        { "48", "Saint Mark's Basilica" },
        { "36", "The Terracotta Army" }
    }
};

context.Add(book);
context.SaveChanges();

這會產生下列 JSON 文件:

{
    "Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
    "Discriminator": "Book",
    "Notes": {
        "36": "The Terracotta Army",
        "48": "Saint Mark's Basilica",
        "121": "Fridges",
        "144": "Peter Higgs"
    },
    "Quotes": [
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    ],
    "Title": "How It Works: Incredible History",
    "id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
    "_rid": "t-E3AIxaencBAAAAAAAAAA==",
    "_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
    "_attachments": "attachments/",
    "_ts": 1630075016
}

然後,您可以再次使用一般方式更新這些集合:

book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";

context.SaveChanges();

限制:

  • 僅支援包含字串索引鍵的字典
  • 目前不支援查詢基本集合的內容。 如果這些功能對您很重要,請投票支持 #16926#25700#25701

內建函式的翻譯

GitHub 問題: #16143

Azure Cosmos DB 提供者現在會將更多基類程式庫 (BCL) 方法轉譯為 Azure Cosmos DB 內建函式。 下表顯示 EF Core 6.0 中新增的翻譯。

字串翻譯

BCL 方法 內建函式 備註
String.Length LENGTH
String.ToLower LOWER
String.TrimStart LTRIM
String.TrimEnd RTRIM
String.Trim TRIM
String.ToUpper UPPER
String.Substring SUBSTRING
+ 運算元 CONCAT
String.IndexOf INDEX_OF
String.Replace REPLACE
String.Equals STRINGEQUAL 僅限不區分大小寫的呼叫

LOWERLTRIM 、、 RTRIMTRIMUPPERSUBSTRING 的翻譯是由 @Marusyk 所提供。 非常感謝!

例如:

var stringResults = await context.Triangles.Where(
        e => e.Name.Length > 4
             && e.Name.Trim().ToLower() != "obtuse"
             && e.Name.TrimStart().Substring(2, 2).Equals("uT", StringComparison.OrdinalIgnoreCase))
    .ToListAsync();

其轉譯為:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((LENGTH(c["Name"]) > 4) AND (LOWER(TRIM(c["Name"])) != "obtuse")) AND STRINGEQUALS(SUBSTRING(LTRIM(c["Name"]), 2, 2), "uT", true)))

數學翻譯

BCL 方法 內建函式
Math.AbsMathF.Abs ABS
Math.AcosMathF.Acos ACOS
Math.AsinMathF.Asin ASIN
Math.AtanMathF.Atan ATAN
Math.Atan2MathF.Atan2 ATN2
Math.CeilingMathF.Ceiling CEILING
Math.CosMathF.Cos COS
Math.ExpMathF.Exp EXP
Math.FloorMathF.Floor FLOOR
Math.LogMathF.Log LOG
Math.Log10MathF.Log10 LOG10
Math.PowMathF.Pow POWER
Math.RoundMathF.Round ROUND
Math.SignMathF.Sign SIGN
Math.SinMathF.Sin SIN
Math.SqrtMathF.Sqrt SQRT
Math.TanMathF.Tan TAN
Math.TruncateMathF.Truncate TRUNC
DbFunctions.Random RAND

這些翻譯是由 @Marusyk 貢獻的。 非常感謝!

例如:

var hypotenuse = 42.42;
var mathResults = await context.Triangles.Where(
        e => (Math.Round(e.Angle1) == 90.0
              || Math.Round(e.Angle2) == 90.0)
             && (hypotenuse * Math.Sin(e.Angle1) > 30.0
                 || hypotenuse * Math.Cos(e.Angle2) > 30.0))
    .ToListAsync();

其轉譯為:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((ROUND(c["Angle1"]) = 90.0) OR (ROUND(c["Angle2"]) = 90.0)) AND (((@__hypotenuse_0 * SIN(c["Angle1"])) > 30.0) OR ((@__hypotenuse_0 * COS(c["Angle2"])) > 30.0))))

DateTime 翻譯

BCL 方法 內建函式
DateTime.UtcNow GetCurrentDateTime

這些翻譯是由 @Marusyk 貢獻的。 非常感謝!

例如:

var timeResults = await context.Triangles.Where(
        e => e.InsertedOn <= DateTime.UtcNow)
    .ToListAsync();

其轉譯為:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (c["InsertedOn"] <= GetCurrentDateTime()))

使用 FromSql 的原始 SQL 查詢

GitHub 問題: #17311

有時候必須執行原始 SQL 查詢,而不是使用 LINQ。 Azure Cosmos DB 提供者現在透過使用 FromSql 方法來支援此功能。 這的運作方式與它一律與關聯式提供者一樣。 例如:

var maxAngle = 60;
var results = await context.Triangles.FromSqlRaw(
        @"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}", maxAngle)
    .ToListAsync();

這會以下列方式執行:

SELECT c
FROM (
    SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
) c

相異查詢

GitHub 問題: #16144

現在會翻譯使用 Distinct 的簡單查詢。 例如:

var distinctResults = await context.Triangles
    .Select(e => e.Angle1).OrderBy(e => e).Distinct()
    .ToListAsync();

其轉譯為:

SELECT DISTINCT c["Angle1"]
FROM root c
WHERE (c["Discriminator"] = "Triangle")
ORDER BY c["Angle1"]

診斷

GitHub 問題: #17298

Azure Cosmos DB 提供者現在會記錄更多診斷資訊,包括插入、查詢、更新和刪除資料庫中資料的事件。 每當適當時,要求單位 (RU) 就會包含在這些事件中。

注意

此處顯示的記錄會使用 EnableSensitiveDataLogging() ,以便顯示識別碼值。

將專案插入 Azure Cosmos DB 資料庫會產生 CosmosEventId.ExecutedCreateItem 事件。 例如,此程式碼:

var triangle = new Triangle
{
    Name = "Impossible",
    PartitionKey = "TrianglesPartition",
    Angle1 = 90,
    Angle2 = 90,
    InsertedOn = DateTime.UtcNow
};
context.Add(triangle);
context.SaveChanges();

記錄下列診斷事件:

info: 8/30/2021 14:41:13.356 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed CreateItem (5 ms, 7.43 RU) ActivityId='417db46f-fcdd-49d9-a7f0-77210cd06f84', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

使用查詢從 Azure Cosmos DB 資料庫擷取專案會產生 CosmosEventId.ExecutingSqlQuery 事件,然後讀取專案的一或多個 CosmosEventId.ExecutedReadNext 事件。 例如,此程式碼:

var equilateral = context.Triangles.Single(e => e.Name == "Equilateral");

記錄下列診斷事件:

info: 8/30/2021 14:41:13.475 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing SQL query for container 'Shapes' in partition '(null)' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2
info: 8/30/2021 14:41:13.651 CosmosEventId.ExecutedReadNext[30102] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadNext (169.6126 ms, 2.93 RU) ActivityId='4e465fae-3d49-4c1f-bd04-142bc5d0b0a1', Container='Shapes', Partition='(null)', Parameters=[]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2

使用 Find 搭配資料分割索引鍵從 Azure Cosmos DB 資料庫擷取單一專案會產生 CosmosEventId.ExecutingReadItemCosmosEventId.ExecutedReadItem 事件。 例如,此程式碼:

var isosceles = context.Triangles.Find("Isosceles", "TrianglesPartition");

記錄下列診斷事件:

info: 8/30/2021 14:53:39.326 CosmosEventId.ExecutingReadItem[30101] (Microsoft.EntityFrameworkCore.Database.Command)
      Reading resource 'Isosceles' item from container 'Shapes' in partition 'TrianglesPartition'.
info: 8/30/2021 14:53:39.330 CosmosEventId.ExecutedReadItem[30103] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadItem (1 ms, 1 RU) ActivityId='3c278643-4e7f-4bb2-9953-6055b5f1288f', Container='Shapes', Id='Isosceles', Partition='TrianglesPartition'

將更新的專案儲存至 Azure Cosmos DB 資料庫會產生 CosmosEventId.ExecutedReplaceItem 事件。 例如,此程式碼:

triangle.Angle2 = 89;
context.SaveChanges();

記錄下列診斷事件:

info: 8/30/2021 14:53:39.343 CosmosEventId.ExecutedReplaceItem[30105] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReplaceItem (6 ms, 10.67 RU) ActivityId='1525b958-fea1-49e8-89f9-d429d0351fdb', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

從 Azure Cosmos DB 資料庫刪除專案會產生 CosmosEventId.ExecutedDeleteItem 事件。 例如,此程式碼:

context.Remove(triangle);
context.SaveChanges();

記錄下列診斷事件:

info: 8/30/2021 14:53:39.359 CosmosEventId.ExecutedDeleteItem[30106] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DeleteItem (6 ms, 7.43 RU) ActivityId='cbc54463-405b-48e7-8c32-2c6502a4138f', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

設定輸送量

GitHub 問題: #17301

現在可以使用手動或自動調整輸送量來設定 Azure Cosmos DB 模型。 這些值會在資料庫上布建輸送量。 例如:

modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);

此外,可以設定個別實體類型來布建對應容器的輸送量。 例如:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasManualThroughput(5000);
        entityTypeBuilder.HasAutoscaleThroughput(3000);
    });

設定存留時間

GitHub 問題: #17307

Azure Cosmos DB 模型中的實體類型現在可以針對分析存放區設定預設存留時間和存留時間。 例如:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasDefaultTimeToLive(100);
        entityTypeBuilder.HasAnalyticalStoreTimeToLive(200);
    });

解決 HTTP 用戶端處理站

GitHub 問題: #21274 。 此功能是由 @dnperfors 所貢獻。 非常感謝!

HttpClientFactory現在可以明確設定 Azure Cosmos DB 提供者所使用的 。 這在測試期間特別有用,例如在 Linux 上使用 Azure Cosmos DB 模擬器時略過憑證驗證:

optionsBuilder
    .EnableSensitiveDataLogging()
    .UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        "PrimitiveCollections",
        cosmosOptionsBuilder =>
        {
            cosmosOptionsBuilder.HttpClientFactory(
                () => new HttpClient(
                    new HttpClientHandler
                    {
                        ServerCertificateCustomValidationCallback =
                            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
                    }));
        });

注意

如需將 Azure Cosmos DB 提供者套用至現有應用程式的詳細範例,請參閱 在 .NET 部落格上取得適用于試用產品 EF Core Azure Cosmos DB 提供者的範例。

現有資料庫的 Scaffolding 改善

EF Core 6.0 在從現有資料庫反向工程 EF 模型時,包含數項改善。

建立多對多關聯性

GitHub 問題: #22475

EF Core 6.0 會偵測簡單的聯結資料表,並自動為其產生多對多對應。 例如,請考慮 和 TagsPosts 資料表,以及連接它們的聯結資料表 PostTag

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]));

CREATE TABLE [PostTag] (
    [PostsId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE);

您可以從命令列建構這些資料表。 例如:

dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=BloggingWithNRTs" Microsoft.EntityFrameworkCore.SqlServer

這會導致 Post 的類別:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

以及 Tag 的類別:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

但是資料表沒有類別 PostTag 。 相反地,會建立多對多關聯性的組態:

entity.HasMany(d => d.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity<Dictionary<string, object>>(
        "PostTag",
        l => l.HasOne<Tag>().WithMany().HasForeignKey("PostsId"),
        r => r.HasOne<Post>().WithMany().HasForeignKey("TagsId"),
        j =>
            {
                j.HasKey("PostsId", "TagsId");
                j.ToTable("PostTag");
                j.HasIndex(new[] { "TagsId" }, "IX_PostTag_TagsId");
            });

Scaffold C# 可為 Null 的參考型別

GitHub 問題: #15520

EF Core 6.0 現在會建構 EF 模型和使用 C# 可為 Null 參考型別的實體類型(NRT)。 在 C# 專案中啟用 NRT 支援時,NRT 使用方式會自動進行 Scaffold 處理。

例如,下表 Tags 同時包含可為 Null 的不可為 Null 字串資料行:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

這會導致產生的類別中對應的可為 Null 和不可為 Null 的字串屬性:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

同樣地,下表 Posts 包含資料表的必要關聯性 Blogs

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]));

這會導致部落格之間不可為 Null 的 (必要) 關聯性 Scaffold:

public partial class Blog
{
    public Blog()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public virtual ICollection<Post> Posts { get; set; }
}

文章:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

最後,產生的 DbCoNtext 中的 DbSet 屬性會以 NRT 易記的方式建立。 例如:

public virtual DbSet<Blog> Blogs { get; set; } = null!;
public virtual DbSet<Post> Posts { get; set; } = null!;
public virtual DbSet<Tag> Tags { get; set; } = null!;

資料庫批註會建構為程式碼批註

GitHub 問題: #19113 。 此功能是由 @ErikEJ 所貢獻。 非常感謝!

SQL 資料表和資料行的批註現在會建構成從現有 SQL Server 資料庫反向工程 EF Core 模型 所建立的實體類型。

/// <summary>
/// The Blog table.
/// </summary>
public partial class Blog
{
    /// <summary>
    /// The primary key.
    /// </summary>
    [Key]
    public int Id { get; set; }
}

LINQ 查詢增強功能

EF Core 6.0 包含 LINQ 查詢翻譯和執行的數項改善。

改善 GroupBy 支援

GitHub 問題: #12088 #13805 #22609

EF Core 6.0 包含更佳的 GroupBy 查詢支援。 具體來說,EF Core 現在:

  • 翻譯 GroupBy 後面 FirstOrDefault 接著群組 (或類似的)
  • 支援從群組選取前 N 個結果
  • 在套用運算子之後 GroupBy 展開導覽

以下是來自客戶報表及其在 SQL Server 上翻譯的範例查詢。

範例 1:

var people = context.People
    .Include(e => e.Shoes)
    .GroupBy(e => e.FirstName)
    .Select(
        g => g.OrderBy(e => e.FirstName)
            .ThenBy(e => e.LastName)
            .FirstOrDefault())
    .ToList();
SELECT [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t].[FirstName], [s].[Id], [s].[Age], [s].[PersonId], [s].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName], [p0].[LastName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
LEFT JOIN [Shoes] AS [s] ON [t0].[Id] = [s].[PersonId]
ORDER BY [t].[FirstName], [t0].[FirstName]

範例 2:

var group = context.People
    .Select(
        p => new
        {
            p.FirstName,
            FullName = p.FirstName + " " + p.MiddleInitial + " " + p.LastName
        })
    .GroupBy(p => p.FirstName)
    .Select(g => g.First())
    .First();
SELECT [t0].[FirstName], [t0].[FullName], [t0].[c]
FROM (
    SELECT TOP(1) [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[FirstName], [t1].[FullName], [t1].[c]
    FROM (
        SELECT [p0].[FirstName], (((COALESCE([p0].[FirstName], N'') + N' ') + COALESCE([p0].[MiddleInitial], N'')) + N' ') + COALESCE([p0].[LastName], N'') AS [FullName], 1 AS [c], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]

範例 3:

var people = context.People
    .Where(e => e.MiddleInitial == "Q" && e.Age == 20)
    .GroupBy(e => e.LastName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e.Length)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))
FROM [People] AS [p]
WHERE ([p].[MiddleInitial] = N'Q') AND ([p].[Age] = 20)
GROUP BY [p].[LastName]
ORDER BY CAST(LEN((
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))) AS int)

範例 4:

var results = (from person in context.People
               join shoes in context.Shoes on person.Age equals shoes.Age
               group shoes by shoes.Style
               into people
               select new
               {
                   people.Key,
                   Style = people.Select(p => p.Style).FirstOrDefault(),
                   Count = people.Count()
               })
    .ToList();
SELECT [s].[Style] AS [Key], (
    SELECT TOP(1) [s0].[Style]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
    WHERE ([s].[Style] = [s0].[Style]) OR ([s].[Style] IS NULL AND [s0].[Style] IS NULL)) AS [Style], COUNT(*) AS [Count]
FROM [People] AS [p]
INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
GROUP BY [s].[Style]

範例 5:

var results = context.People
    .GroupBy(e => e.FirstName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))
FROM [People] AS [p]
GROUP BY [p].[FirstName]
ORDER BY (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))

範例 6:

var results = context.People.Where(e => e.Age == 20)
    .GroupBy(e => e.Id)
    .Select(g => g.First().MiddleInitial)
    .OrderBy(e => e)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))
FROM [People] AS [p]
WHERE [p].[Age] = 20
GROUP BY [p].[Id]
ORDER BY (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))

範例 7:

var size = 11;
var results
    = context.People
        .Where(
            p => p.Feet.Size == size
                 && p.MiddleInitial != null
                 && p.Feet.Id != 1)
        .GroupBy(
            p => new
            {
                p.Feet.Size,
                p.Feet.Person.LastName
            })
        .Select(
            g => new
            {
                g.Key.LastName,
                g.Key.Size,
                Min = g.Min(p => p.Feet.Size),
            })
        .ToList();
Executed DbCommand (12ms) [Parameters=[@__size_0='11'], CommandType='Text', CommandTimeout='30']
SELECT [p0].[LastName], [f].[Size], MIN([f0].[Size]) AS [Min]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
LEFT JOIN [People] AS [p0] ON [f].[Id] = [p0].[Id]
LEFT JOIN [Feet] AS [f0] ON [p].[Id] = [f0].[Id]
WHERE (([f].[Size] = @__size_0) AND [p].[MiddleInitial] IS NOT NULL) AND (([f].[Id] <> 1) OR [f].[Id] IS NULL)
GROUP BY [f].[Size], [p0].[LastName]

範例 8:

var result = context.People
    .Include(x => x.Shoes)
    .Include(x => x.Feet)
    .GroupBy(
        x => new
        {
            x.Feet.Id,
            x.Feet.Size
        })
    .Select(
        x => new
        {
            Key = x.Key.Id + x.Key.Size,
            Count = x.Count(),
            Sum = x.Sum(el => el.Id),
            SumOver60 = x.Sum(el => el.Id) / (decimal)60,
            TotalCallOutCharges = x.Sum(el => el.Feet.Size == 11 ? 1 : 0)
        })
    .Count();
SELECT COUNT(*)
FROM (
    SELECT [f].[Id], [f].[Size]
    FROM [People] AS [p]
    LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
    GROUP BY [f].[Id], [f].[Size]
) AS [t]

範例 9:

var results = context.People
    .GroupBy(n => n.FirstName)
    .Select(g => new
    {
        Feet = g.Key,
        Total = g.Sum(n => n.Feet.Size)
    })
    .ToList();
SELECT [p].[FirstName] AS [Feet], COALESCE(SUM([f].[Size]), 0) AS [Total]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
GROUP BY [p].[FirstName]

範例 10:

var results = from Person person1
                  in from Person person2
                         in context.People
                     select person2
              join Shoes shoes
                  in context.Shoes
                  on person1.Age equals shoes.Age
              group shoes by
                  new
                  {
                      person1.Id,
                      shoes.Style,
                      shoes.Age
                  }
              into temp
              select
                  new
                  {
                      temp.Key.Id,
                      temp.Key.Age,
                      temp.Key.Style,
                      Values = from t
                                   in temp
                               select
                                   new
                                   {
                                       t.Id,
                                       t.Style,
                                       t.Age
                                   }
                  };
SELECT [t].[Id], [t].[Age], [t].[Style], [t0].[Id], [t0].[Style], [t0].[Age], [t0].[Id0]
FROM (
    SELECT [p].[Id], [s].[Age], [s].[Style]
    FROM [People] AS [p]
    INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
    GROUP BY [p].[Id], [s].[Style], [s].[Age]
) AS [t]
LEFT JOIN (
    SELECT [s0].[Id], [s0].[Style], [s0].[Age], [p0].[Id] AS [Id0]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
) AS [t0] ON (([t].[Id] = [t0].[Id0]) AND (([t].[Style] = [t0].[Style]) OR ([t].[Style] IS NULL AND [t0].[Style] IS NULL))) AND ([t].[Age] = [t0].[Age])
ORDER BY [t].[Id], [t].[Style], [t].[Age], [t0].[Id0]

範例 11:

var grouping = context.People
    .GroupBy(i => i.LastName)
    .Select(g => new { LastName = g.Key, Count = g.Count() , First = g.FirstOrDefault(), Take = g.Take(2)})
    .OrderByDescending(e => e.LastName)
    .ToList();
SELECT [t].[LastName], [t].[c], [t0].[Id], [t2].[Id], [t2].[Age], [t2].[FirstName], [t2].[LastName], [t2].[MiddleInitial], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial]
FROM (
    SELECT [p].[LastName], COUNT(*) AS [c]
    FROM [People] AS [p]
    GROUP BY [p].[LastName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[LastName] ORDER BY [p0].[Id]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[LastName] = [t0].[LastName]
LEFT JOIN (
    SELECT [t3].[Id], [t3].[Age], [t3].[FirstName], [t3].[LastName], [t3].[MiddleInitial]
    FROM (
        SELECT [p1].[Id], [p1].[Age], [p1].[FirstName], [p1].[LastName], [p1].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p1].[LastName] ORDER BY [p1].[Id]) AS [row]
        FROM [People] AS [p1]
    ) AS [t3]
    WHERE [t3].[row] <= 2
) AS [t2] ON [t].[LastName] = [t2].[LastName]
ORDER BY [t].[LastName] DESC, [t0].[Id], [t2].[LastName], [t2].[Id]

範例 12:

var grouping = context.People
    .Include(e => e.Shoes)
    .OrderBy(e => e.FirstName)
    .ThenBy(e => e.LastName)
    .GroupBy(e => e.FirstName)
    .Select(g => new { Name = g.Key, People = g.ToList()})
    .ToList();
SELECT [t].[FirstName], [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t0].[Id0], [t0].[Age0], [t0].[PersonId], [t0].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], [s].[Id] AS [Id0], [s].[Age] AS [Age0], [s].[PersonId], [s].[Style]
    FROM [People] AS [p0]
    LEFT JOIN [Shoes] AS [s] ON [p0].[Id] = [s].[PersonId]
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
ORDER BY [t].[FirstName], [t0].[Id]

範例 13:

var grouping = context.People
    .GroupBy(m => new {m.FirstName, m.MiddleInitial })
    .Select(am => new
    {
        Key = am.Key,
        Items = am.ToList()
    })
    .ToList();
SELECT [t].[FirstName], [t].[MiddleInitial], [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial]
FROM (
    SELECT [p].[FirstName], [p].[MiddleInitial]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName], [p].[MiddleInitial]
) AS [t]
LEFT JOIN [People] AS [p0] ON (([t].[FirstName] = [p0].[FirstName]) OR ([t].[FirstName] IS NULL AND [p0].[FirstName] IS NULL)) AND (([t].[MiddleInitial] = [p0].[MiddleInitial]) OR ([t].[MiddleInitial] IS NULL AND [p0].[MiddleInitial] IS NULL))
ORDER BY [t].[FirstName], [t].[MiddleInitial]

型號

這些範例所使用的實體類型如下:

public class Person
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MiddleInitial { get; set; }
    public Feet Feet { get; set; }
    public ICollection<Shoes> Shoes { get; } = new List<Shoes>();
}

public class Shoes
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string Style { get; set; }
    public Person Person { get; set; }
}

public class Feet
{
    public int Id { get; set; }
    public int Size { get; set; }
    public Person Person { get; set; }
}

使用多個引數翻譯 String.Concat

GitHub 問題: #23859 。 此功能是由 @wmeints 所貢獻。 非常感謝!

從 EF Core 6.0 開始,使用多個引數呼叫 String.Concat 現在會轉譯為 SQL。 例如,下列查詢:

var shards = context.Shards
    .Where(e => string.Concat(e.Token1, e.Token2, e.Token3) != e.TokensProcessed).ToList();

在使用 SQL Server 時,將會轉譯成下列 SQL:

SELECT [s].[Id], [s].[Token1], [s].[Token2], [s].[Token3], [s].[TokensProcessed]
FROM [Shards] AS [s]
WHERE (([s].[Token1] + ([s].[Token2] + [s].[Token3])) <> [s].[TokensProcessed]) OR [s].[TokensProcessed] IS NULL

Smoother 與 System.Linq.Async 整合

GitHub 問題: #24041

System.Linq.Async 套件會新增用戶端非同步 LINQ 處理。 由於非同步 LINQ 方法的命名空間衝突,所以搭配舊版 EF Core 使用此套件很麻煩。 在 EF Core 6.0 中,我們已利用 C# 模式比 IAsyncEnumerable<T> 對,讓公開的 EF Core DbSet<TEntity> 不需要直接實作介面。

請注意,大部分的應用程式都不需要使用 System.Linq.Async,因為 EF Core 查詢通常會在伺服器上完整轉譯。

GitHub 問題: #23921

在 EF Core 6.0 中,我們已放寬 和 Contains 的參數需求 FreeText(DbFunctions, String, String) 。 這可讓這些函式與二進位資料行搭配使用,或使用值轉換器對應的資料行。 例如,假設實體類型具有 Name 定義為 value 物件的 屬性:

public class Customer
{
    public int Id { get; set; }

    public Name Name{ get; set; }
}

public class Name
{
    public string First { get; set; }
    public string MiddleInitial { get; set; }
    public string Last { get; set; }
}

這會對應至資料庫中的 JSON:

modelBuilder.Entity<Customer>()
    .Property(e => e.Name)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<Name>(v, (JsonSerializerOptions)null));

現在可以使用 或 FreeText 來執行 Contains 查詢,即使 屬性 Name 的類型不是 也一 string 樣。 例如:

var result = context.Customers.Where(e => EF.Functions.Contains(e.Name, "Martin")).ToList();

這會在使用 SQL Server 時產生下列 SQL:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE CONTAINS([c].[Name], N'Martin')

在 SQLite 上翻譯 ToString

GitHub 問題: #17223 。 這項功能是由 @ralmsdeveloper 所貢獻。 非常感謝!

ToString()使用 SQLite 資料庫提供者時,對 的呼叫現在會轉譯為 SQL。 這對於涉及非字串資料行的文字搜尋很有用。 例如,請考慮將 User 電話號碼儲存為數值的實體類型:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public long PhoneNumber { get; set; }
}

ToString 可用來將數位轉換成資料庫中的字串。 然後,我們可以搭配函式使用此字串,例如 LIKE 尋找符合模式的數位。 例如,若要尋找包含 555 的所有數位:

var users = context.Users.Where(u => EF.Functions.Like(u.PhoneNumber.ToString(), "%555%")).ToList();

使用 SQLite 資料庫時,這會轉譯為下列 SQL:

SELECT "u"."Id", "u"."PhoneNumber", "u"."Username"
FROM "Users" AS "u"
WHERE CAST("u"."PhoneNumber" AS TEXT) LIKE '%555%'

請注意,EF Core 5.0 中已經支援 SQL Server 的轉譯 ToString() ,其他資料庫提供者也可能支援。

英 孚。Functions.Random

GitHub 問題: #16141 。 這項功能是由 @RaymondHuy 所貢獻。 非常感謝!

EF.Functions.Random 會對應至資料庫函式,傳回介於 0 到 1 的虛擬亂數。 已在 SQL Server、SQLite 和 Azure Cosmos DB 的 EF Core 存放庫中實作翻譯。 例如,請考慮 User 具有 屬性的 Popularity 實體類型:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public int Popularity { get; set; }
}

Popularity 可以包含 1 到 5 的值。 我們 EF.Functions.Random 可以使用 撰寫查詢,以傳回具有隨機播放人氣的所有使用者:

var users = context.Users.Where(u => u.Popularity == (int)(EF.Functions.Random() * 4.0) + 1).ToList();

這會在使用 SQL Server 資料庫時轉譯為下列 SQL:

SELECT [u].[Id], [u].[Popularity], [u].[Username]
FROM [Users] AS [u]
WHERE [u].[Popularity] = (CAST((RAND() * 4.0E0) AS int) + 1)

改善 IsNullOrWhitespace 的 SQL Server 翻譯

GitHub 問題: #22916 。 這項功能是由 @Marusyk 所貢獻。 非常感謝!

請考慮以下查詢:

var users = context.Users.Where(
    e => string.IsNullOrWhiteSpace(e.FirstName)
         || string.IsNullOrWhiteSpace(e.LastName)).ToList();

在 EF Core 6.0 之前,這會轉譯為 SQL Server 上的下列內容:

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR (LTRIM(RTRIM([u].[FirstName])) = N'')) OR ([u].[LastName] IS NULL OR (LTRIM(RTRIM([u].[LastName])) = N''))

EF Core 6.0 已改善此翻譯為:

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR ([u].[FirstName] = N'')) OR ([u].[LastName] IS NULL OR ([u].[LastName] = N''))

定義記憶體內部提供者的查詢

GitHub 問題: #24600

新的方法 ToInMemoryQuery 可用來針對指定實體類型的記憶體內部資料庫撰寫定義查詢。 這最適用于在記憶體內部資料庫上建立對等檢視,特別是當這些檢視傳回無索引鍵實體類型時。 例如,針對以英國為基礎的客戶,請考慮客戶資料庫。 每位客戶都有位址:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

現在,假設我們想要檢視此資料,其中顯示每個郵遞區號區域中有多少客戶。 我們可以建立無索引鍵實體類型來表示:

public class CustomerDensity
{
    public string Postcode { get; set; }
    public int CustomerCount { get; set; }
}

並在 DbCoNtext 上定義它的 DbSet 屬性,以及其他最上層實體類型的集合:

public DbSet<Customer> Customers { get; set; }
public DbSet<CustomerDensity> CustomerDensities { get; set; }

然後,在 中 OnModelCreating ,我們可以撰寫 LINQ 查詢,以定義要針對 CustomerDensities 傳回的資料:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<CustomerDensity>()
        .HasNoKey()
        .ToInMemoryQuery(
            () => Customers
                .GroupBy(c => c.Address.Postcode.Substring(0, 3))
                .Select(
                    g =>
                        new CustomerDensity
                        {
                            Postcode = g.Key,
                            CustomerCount = g.Count()
                        }));
}

然後,就像任何其他 DbSet 屬性一樣,即可查詢此專案:

var results = context.CustomerDensities.ToList();

使用單一參數轉譯子字串

GitHub 問題: #20173 。 這項功能是由 @stevendarby 所貢獻。 非常感謝!

EF Core 6.0 現在會轉譯 搭配單一引數的 string.Substring 用法。 例如:

var result = context.Customers
    .Select(a => new { Name = a.Name.Substring(3) })
    .FirstOrDefault(a => a.Name == "hur");

這會在使用 SQL Server 時轉譯為下列 SQL:

SELECT TOP(1) SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) AS [Name]
FROM [Customers] AS [c]
WHERE SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) = N'hur'

非導覽集合的分割查詢

GitHub 問題: #21234

EF Core 支援將單一 LINQ 查詢分割成多個 SQL 查詢。 在 EF Core 6.0 中,此支援已擴充為包含查詢投影中非導覽集合的情況。

以下是範例查詢,其中顯示 SQL Server 上的轉譯成單一查詢或多個查詢。

範例 1:

LINQ 查詢:

context.Customers
    .Select(
        c => new
        {
            c,
            Orders = c.Orders
                .Where(o => o.Id > 1)
        })
    .ToList();

單一 SQL 查詢:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

多個 SQL 查詢:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

範例 2:

LINQ 查詢:

context.Customers
    .Select(
        c => new
        {
            c,
            OrderDates = c.Orders
                .Where(o => o.Id > 1)
                .Select(o => o.OrderDate)
        })
    .ToList();

單一 SQL 查詢:

SELECT [c].[Id], [t].[OrderDate], [t].[Id]
FROM [Customers] AS [c]
  LEFT JOIN (
  SELECT [o].[OrderDate], [o].[Id], [o].[CustomerId]
  FROM [Order] AS [o]
  WHERE [o].[Id] > 1
  ) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

多個 SQL 查詢:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

範例 3:

LINQ 查詢:

context.Customers
    .Select(
        c => new
        {
            c,
            OrderDates = c.Orders
                .Where(o => o.Id > 1)
                .Select(o => o.OrderDate)
                .Distinct()
        })
    .ToList();

單一 SQL 查詢:

SELECT [c].[Id], [t].[OrderDate]
FROM [Customers] AS [c]
  OUTER APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

多個 SQL 查詢:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
  CROSS APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

加入集合時移除最後一個 ORDER BY 子句

GitHub 問題: #19828

載入相關的一對多實體時,EF Core 會新增 ORDER BY 子句,以確保指定實體的所有相關實體會分組在一起。 不過,EF 不需要最後一個 ORDER BY 子句來產生所需的群組,而且可能會影響效能。 因此,會移除 EF Core 6.0 這個子句。

例如,請考量以下的查詢:

context.Customers
    .Select(
        e => new
        {
            e.Id,
            FirstOrder = e.Orders.Where(i => i.Id == 1).ToList()
        })
    .ToList();

在 SQL Server 上使用 EF Core 5.0 時,此查詢會轉譯為:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id], [t].[Id]

使用 EF Core 6.0,它會轉譯為:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

使用檔案名和行號標記查詢

GitHub 問題: #14176 。 此功能是由 @michalczerwinski 所提供。 非常感謝!

查詢標籤允許將文字標記新增至 LINQ 查詢,使其接著包含在產生的 SQL 中。 在 EF Core 6.0 中,這可用來標記具有 LINQ 程式碼檔案名和行號的查詢。 例如:

var results1 = context
    .Customers
    .TagWithCallSite()
    .Where(c => c.Name.StartsWith("A"))
    .ToList();

這會導致使用 SQL Server 時產生下列 SQL:

-- file: C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\TagWithFileAndLineSample.cs:21

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE [c].[Name] IS NOT NULL AND ([c].[Name] LIKE N'A%')

擁有的選擇性相依處理變更

GitHub 問題: #24558

當選擇性相依實體與其主體實體共用資料表時,就變得棘手。 這是因為資料表中有一個資料列,因為主體需要它,而不論相依是否存在。 明確處理這個方法的方法,是確保相依性至少有一個必要屬性。 由於必要的屬性不可以是 Null,所以表示如果該屬性的資料行中值為 null,則相依實體不存在。

例如,請考慮每個 Customer 客戶擁有 的 Address 類別:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

    [Required]
    public string Postcode { get; set; }
}

位址是選擇性的,這表示儲存沒有位址的客戶是有效的:

context.Customers1.Add(
    new()
    {
        Name = "Foul Ole Ron"
    });

不過,如果客戶有位址,則該位址至少必須有非 Null 郵遞區號:

context.Customers1.Add(
    new()
    {
        Name = "Havelock Vetinari",
        Address = new()
        {
            Postcode = "AN1 1PL",
        }
    });

這可藉由將 Postcode 屬性標示為 Required 來確保。

現在當查詢客戶時,如果 Postcode 資料行為 Null,則這表示客戶沒有位址,且 Customer.Address 導覽屬性會保留 Null。 例如,逐一查看客戶,並檢查位址是否為 Null:

foreach (var customer in context.Customers1)
{
    Console.Write(customer.Name);

    if (customer.Address == null)
    {
        Console.WriteLine(" has no address.");
    }
    else
    {
        Console.WriteLine($" has postcode {customer.Address.Postcode}.");
    }
}

產生下列結果:

Foul Ole Ron has no address.
Havelock Vetinari has postcode AN1 1PL.

請改為考慮不需要位址的屬性:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

現在,可以同時儲存沒有位址的客戶,以及具有所有位址屬性為 Null 位址的客戶:

context.Customers2.Add(
    new()
    {
        Name = "Foul Ole Ron"
    });

context.Customers2.Add(
    new()
    {
        Name = "Havelock Vetinari",
        Address = new()
    });

不過,在資料庫中,這兩個案例是無可區分的,因為我們可以直接查詢資料庫資料行來查看:

Id  Name               House   Street  City    Postcode
1   Foul Ole Ron       NULL    NULL    NULL    NULL
2   Havelock Vetinari  NULL    NULL    NULL    NULL

基於這個理由,EF Core 6.0 現在會在儲存選擇性相依專案時發出警告,其中所有屬性都是 Null。 例如:

warn: 9/27/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) 類型為 'Address' 的實體,其主鍵值 {CustomerId: -2147482646} 是選擇性的依賴資料表共用。 實體沒有任何屬性具有非預設值,可識別實體是否存在。 這表示當查詢時,不會建立任何物件實例,而不是將所有屬性設定為預設值的實例。 任何巢狀相依性也會遺失。 請勿儲存任何只有預設值的實例,或將傳入導覽標示為模型中的必要專案。

這變得更加棘手,其中選擇性相依本身會針對進一步的選擇性相依性執行主體,也會對應至相同的資料表。 EF Core 6.0 不只警告巢狀選擇性相依專案的情況。 例如,請考慮下列模型,其中 ContactInfo 是 由 Customer 擁有,而 Address 是由 所擁有 ContactInfo

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class ContactInfo
{
    public string Phone { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

現在如果 ContactInfo.Phone 為 null,則即使位址本身可能有資料,EF Core 也不會建立 的實例 Address 。 針對這類模型,EF Core 6.0 會擲回下列例外狀況:

System.InvalidOperationException:實體類型 'ContactInfo' 是選擇性的相依專案,使用資料表共用並包含其他相依專案,不需要任何必要的非共用屬性,即可識別實體是否存在。 如果資料庫中所有可為 Null 的屬性都包含 Null 值,則不會在查詢中建立物件實例,導致巢狀相依值遺失。 新增必要的屬性,以建立具有 Null 值的其他屬性的實例,或將傳入導覽標示為一律建立實例所需的值。

這裡的底線是避免選擇性相依專案可以包含所有可為 Null 的屬性值,並與其主體共用資料表的情況。 有三個簡單的方法可以避免這種情況:

  1. 讓相依專案成為必要專案。 這表示相依實體在查詢之後一律會有值,即使其所有屬性都是 Null 也一樣。
  2. 請確定相依專案至少包含一個必要屬性,如上所述。
  3. 將選擇性相依專案儲存至自己的資料表,而不是與主體共用資料表。

您可以在其導覽上使用 屬性來要求 Required 相依專案:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

或者,在 中 OnModelCreating 指定它是必要的:

modelBuilder.Entity<WithRequiredNavigation.Customer>(
    b =>
        {
            b.OwnsOne(e => e.Address);
            b.Navigation(e => e.Address).IsRequired();
        });

相依專案可以藉由指定要在 中使用的 OnModelCreating 資料表來儲存至不同的資料表:

modelBuilder
    .Entity<WithDifferentTable.Customer>(
        b =>
            {
                b.ToTable("Customers");
                b.OwnsOne(
                    e => e.Address,
                    b => b.ToTable("CustomerAddresses"));
            });

如需選擇性相依專案的更多範例,請參閱 GitHub 中的 OptionalDependentsSample ,包括巢狀選擇性相依性案例。

新的對應屬性

EF Core 6.0 包含數個新屬性,可套用至程式碼以變更對應至資料庫的方式。

UnicodeAttribute

GitHub 問題: #19794 。 這項功能是由 @RaymondHuy 所貢獻。 非常感謝!

從 EF Core 6.0 開始,字串屬性現在可以使用對應屬性 對應至非 Unicode 資料行,而不需要直接 指定資料庫類型。 例如,假設實體 Book 類型具有 「ISBN 978-3-16-148410-0」 格式的 國際標準書號 (ISBN) 屬性:

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }

    [Unicode(false)]
    [MaxLength(22)]
    public string Isbn { get; set; }
}

因為 ISBN 不能包含任何非 Unicode 字元,所以 Unicode 屬性會導致使用非 Unicode 字串類型。 此外, MaxLength 用來限制資料庫資料行的大小。 例如,使用 SQL Server 時,這會產生 的資料庫資料行 varchar(22)

CREATE TABLE [Book] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NULL,
    [Isbn] varchar(22) NULL,
    CONSTRAINT [PK_Book] PRIMARY KEY ([Id]));

注意

EF Core 預設會將字串屬性對應至 Unicode 資料行。 UnicodeAttribute 當資料庫系統僅支援 Unicode 類型時,會忽略 。

PrecisionAttribute

GitHub 問題: #17914 。 這項功能是由 @RaymondHuy 所貢獻。 非常感謝!

現在可以使用對應屬性 來設定資料庫資料行的有效位數和小數位數,而不需要直接 指定資料庫類型。 例如,請考慮具有 Product decimal Price 屬性的實體類型:

public class Product
{
    public int Id { get; set; }

    [Precision(precision: 10, scale: 2)]
    public decimal Price { get; set; }
}

EF Core 會將此屬性對應至有效位數為 10 和小數位數 2 的資料庫資料行。 例如,在 SQL Server 上:

CREATE TABLE [Product] (
    [Id] int NOT NULL IDENTITY,
    [Price] decimal(10,2) NOT NULL,
    CONSTRAINT [PK_Product] PRIMARY KEY ([Id]));

EntityTypeConfigurationAttribute

GitHub 問題: #23163 。 此功能是由 @KaloyanIT 所貢獻。 非常感謝!

IEntityTypeConfiguration<TEntity> 實例允許 ModelBuilder 每個實體類型的組態包含在它自己的組態類別中。 例如:

public class BookConfiguration : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder
            .Property(e => e.Isbn)
            .IsUnicode(false)
            .HasMaxLength(22);
    }
}

一般而言,這個組態類別必須具 DbContext.OnModelCreating 現化,並從 中呼叫 。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    new BookConfiguration().Configure(modelBuilder.Entity<Book>());
}

從 EF Core 6.0 開始, EntityTypeConfigurationAttribute 可以將 放在實體類型上,讓 EF Core 能夠尋找及使用適當的組態。 例如:

[EntityTypeConfiguration(typeof(BookConfiguration))]
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Isbn { get; set; }
}

這個屬性工作表示每當模型中包含實體類型時, Book EF Core 都會使用指定的 IEntityTypeConfiguration 實作。 實體類型會使用其中一個一般機制包含在模型中。 例如,藉由建立 DbSet<TEntity> 實體類型的 屬性:

public class BooksContext : DbContext
{
    public DbSet<Book> Books { get; set; }

    //...

或者,在 中 OnModelCreating 註冊它:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Book>();
}

注意

EntityTypeConfigurationAttribute 不會在元件中自動探索類型。 實體類型必須先新增至模型,才能在該實體類型上探索屬性。

模型建置改善

除了新的對應屬性之外,EF Core 6.0 還包含數個對模型建置程式的其他改善。

支援 SQL Server 疏鬆資料行

GitHub 問題: #8023

SQL Server 疏鬆資料行 是已優化以儲存 Null 值的一般資料行。 當使用 TPH 繼承對應 時,很少使用子類型的屬性會導致資料表中大部分資料列的 Null 資料行值時,這非常有用。 例如,請考慮 ForumModeratorForumUser 延伸的類別:

public class ForumUser
{
    public int Id { get; set; }
    public string Username { get; set; }
}

public class ForumModerator : ForumUser
{
    public string ForumName { get; set; }
}

可能有數百萬使用者,其中只有少數是仲裁者。 這表示將 對應 ForumName 為疏鬆可能很合理。 現在可以使用 在 中 OnModelCreating 設定此設定 IsSparse 。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<ForumModerator>()
        .Property(e => e.ForumName)
        .IsSparse();
}

EF Core 移轉接著會將資料行標示為疏鬆。 例如:

CREATE TABLE [ForumUser] (
    [Id] int NOT NULL IDENTITY,
    [Username] nvarchar(max) NULL,
    [Discriminator] nvarchar(max) NOT NULL,
    [ForumName] nvarchar(max) SPARSE NULL,
    CONSTRAINT [PK_ForumUser] PRIMARY KEY ([Id]));

注意

疏鬆資料行有限制。 請務必閱讀 SQL Server 疏鬆資料行檔 ,以確保疏鬆資料行是您案例的正確選擇。

HasConversion API 的改善

GitHub 問題: #25468

在 EF Core 6.0 之前,方法的 HasConversion 泛型多載會使用泛型參數來指定要 轉換 的類型。 例如,請考慮 Currency 列舉:

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

EF Core 可以設定為使用 將這個列舉的值儲存為字串 「UsDollars」、「PoundsStirling」 和 「Euros」。 HasConversion<string> 例如:

modelBuilder.Entity<TestEntity1>()
    .Property(e => e.Currency)
    .HasConversion<string>();

從 EF Core 6.0 開始,泛型型別可以改為指定 值轉換器類型 。 這可以是其中一個內建值轉換器。 例如,若要將列舉值儲存為資料庫中的 16 位數位:

modelBuilder.Entity<TestEntity2>()
    .Property(e => e.Currency)
    .HasConversion<EnumToNumberConverter<Currency, short>>();

或者,它可以是自訂值轉換器類型。 例如,請考慮將列舉值儲存為其貨幣符號的轉換器:

public class CurrencyToSymbolConverter : ValueConverter<Currency, string>
{
    public CurrencyToSymbolConverter()
        : base(
            v => v == Currency.PoundsSterling ? "£" : v == Currency.Euros ? "€" : "$",
            v => v == "£" ? Currency.PoundsSterling : v == "€" ? Currency.Euros : Currency.UsDollars)
    {
    }
}

現在可以使用泛型 HasConversion 方法來設定:

modelBuilder.Entity<TestEntity3>()
    .Property(e => e.Currency)
    .HasConversion<CurrencyToSymbolConverter>();

多對多關聯性的設定較少

GitHub 問題: #21535

慣例會探索兩個實體類型之間的明確多對多關聯性。 必要時或如有需要,可以明確指定導覽。 例如:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats);

在這兩種情況下,EF Core 會根據 Dictionary<string, object> 建立共用實體,以作為兩種類型之間的聯結實體。 從 EF Core 6.0 開始, UsingEntity 可以新增至組態,只變更此類型,而不需要額外的設定。 例如:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>();

此外,聯結實體類型也可以額外設定,而不需要明確指定左右關聯性。 例如:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>(
        e => e.HasKey(e => new { e.CatsId, e.HumansId }));

最後,可以提供完整的組態。 例如:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>(
        e => e.HasOne<Human>().WithMany().HasForeignKey(e => e.CatsId),
        e => e.HasOne<Cat>().WithMany().HasForeignKey(e => e.HumansId),
        e => e.HasKey(e => new { e.CatsId, e.HumansId }));

允許值轉換器轉換 Null

GitHub 問題: #13850

重要

由於以下所述的問題,允許轉換 Null 的建構 ValueConverter 函式已針對 EF Core 6.0 版本標示 [EntityFrameworkInternal] 為 。 使用這些建構函式現在會產生組建警告。

值轉換器通常不允許將 Null 轉換成其他值。 這是因為相同的值轉換器可用於可為 Null 和不可為 Null 的類型,這對 FK/FK 組合非常有用,其中 FK 通常可為 Null 且 PK 不是。

從 EF Core 6.0 開始,可以建立可轉換 Null 的值轉換器。 然而,這項功能的驗證表明,在實務上有許多陷阱是非常有問題的。 例如:

這些不是簡單的問題,而且對於查詢問題來說,它們不容易偵測。 因此,我們已將此功能標示為 EF Core 6.0 的內部功能。 您仍然可以使用它,但會收到編譯器警告。 您可以使用 停用 #pragma warning disable EF1001 警告。

轉換 Null 的其中一個範例很有用,就是當資料庫包含 Null 時,實體類型想要使用屬性的一些其他預設值。 例如,請考慮其預設值為 「Unknown」 的列舉:

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

不過,當品種未知時,資料庫可能會有 Null 值。 在 EF Core 6.0 中,值轉換器可用來考慮下列各項:

    public class BreedConverter : ValueConverter<Breed, string>
    {
#pragma warning disable EF1001
        public BreedConverter()
            : base(
                v => v == Breed.Unknown ? null : v.ToString(),
                v => v == null ? Breed.Unknown : Enum.Parse<Breed>(v),
                convertsNulls: true)
        {
        }
#pragma warning restore EF1001
    }

具有「未知」品種的貓,其 Breed 資料行會在資料庫中設定為 null。 例如:

context.AddRange(
    new Cat { Name = "Mac", Breed = Breed.Unknown },
    new Cat { Name = "Clippy", Breed = Breed.Burmese },
    new Cat { Name = "Sid", Breed = Breed.Tonkinese });

context.SaveChanges();

這會在 SQL Server 上產生下列 insert 語句:

info: 9/27/2021 19:43:55.966 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (16ms) [Parameters=[@p0=NULL (Size = 4000), @p1='Mac' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Burmese' (Size = 4000), @p1='Clippy' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Tonkinese' (Size = 4000), @p1='Sid' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

DbCoNtext Factory 改善

AddDbCoNtextFactory 也會直接註冊 DbCoNtext

GitHub 問題: #25164

有時候,在應用程式相依性插入 (D.I.) 容器中註冊的 DbCoNtext 型 別和 處理站都註冊該類型的內容相當實用。 例如,這可讓 DbCoNtext 的範圍實例從要求範圍解析,而處理站可用來在需要時建立多個獨立實例。

為了支援此功能, AddDbContextFactory 現在也會將 DbCoNtext 類型註冊為範圍服務。 例如,請考慮在應用程式的 D.I. 容器中註冊:

var container = services
    .AddDbContextFactory<SomeDbContext>(
        builder => builder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample"))
    .BuildServiceProvider();

透過此註冊,處理站可以從根 D.I. 容器解析,就像先前的版本一樣:

var factory = container.GetService<IDbContextFactory<SomeDbContext>>();
using (var context = factory.CreateDbContext())
{
    // Contexts obtained from the factory must be explicitly disposed
}

請注意,處理站所建立的內容實例必須明確處置。

此外,也可以直接從容器範圍解析 DbCoNtext 實例:

using (var scope = container.CreateScope())
{
    var context = scope.ServiceProvider.GetService<SomeDbContext>();
    // Context is disposed when the scope is disposed
}

在此情況下,在處置容器範圍時,會處置內容實例;不應明確處置內容。

在較高層級,這表示 Factory 的 DbCoNtext 可以插入其他 D.I 類型。 例如:

private class MyController2
{
    private readonly IDbContextFactory<SomeDbContext> _contextFactory;

    public MyController2(IDbContextFactory<SomeDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public void DoSomething()
    {
        using var context1 = _contextFactory.CreateDbContext();
        using var context2 = _contextFactory.CreateDbContext();

        var results1 = context1.Blogs.ToList();
        var results2 = context2.Blogs.ToList();
        
        // Contexts obtained from the factory must be explicitly disposed
    }
}

或:

private class MyController1
{
    private readonly SomeDbContext _context;

    public MyController1(SomeDbContext context)
    {
        _context = context;
    }

    public void DoSomething()
    {
        var results = _context.Blogs.ToList();

        // Injected context is disposed when the request scope is disposed
    }
}

DbCoNtextFactory 會忽略 DbCoNtext 無參數建構函式

GitHub 問題: #24124

EF Core 6.0 現在同時允許無參數 DbCoNtext 建構函式,以及透過 註冊 AddDbContextFactory 處理站時,用於相同內容類型的建構 DbContextOptions 函式。 例如,上述範例中使用的內容包含這兩個建構函式:

public class SomeDbContext : DbContext
{
    public SomeDbContext()
    {
    }

    public SomeDbContext(DbContextOptions<SomeDbContext> options)
        : base(options)
    {
    }
    
    public DbSet<Blog> Blogs { get; set; }
}

不需要相依性插入,就可以使用 DbCoNtext 共用

GitHub 問題: #24137

PooledDbContextFactory 類型已公開,因此可以做為 DbCoNtext 實例的獨立集區,而不需要您的應用程式具有相依性插入容器。 集區是使用 的實例 DbContextOptions 來建立,其將用來建立內容實例:

var options = new DbContextOptionsBuilder<SomeDbContext>()
    .EnableSensitiveDataLogging()
    .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample")
    .Options;

var factory = new PooledDbContextFactory<SomeDbContext>(options);

然後,Factory 可用來建立和集區實例。 例如:

for (var i = 0; i < 2; i++)
{
    using var context1 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context1.ContextId}");

    using var context2 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context2.ContextId}");
}

實例在處置時會傳回至集區。

其他改善

最後,EF Core 包含上述未涵蓋區域的數項改進。

建立資料表時,請使用 [ColumnAttribute.Order]

GitHub 問題: #10059

Order 屬性 ColumnAttribute 現在可以在建立具有移轉的資料表時,用來排序資料行。 例如,請考慮下列模型:

public class EntityBase
{
    public int Id { get; set; }
    public DateTime UpdatedOn { get; set; }
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    public string Department { get; set; }
    public decimal AnnualSalary { get; set; }
    public Address Address { get; set; }
}

[Owned]
public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

    [Required]
    public string Postcode { get; set; }
}

根據預設,EF Core 會先排序主鍵資料行,然後依實體類型和擁有類型的屬性,最後排序基底型別的屬性。 例如,下表是在 SQL Server 上建立:

CREATE TABLE [EmployeesWithoutOrdering] (
    [Id] int NOT NULL IDENTITY,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [Address_House] nvarchar(max) NULL,
    [Address_Street] nvarchar(max) NULL,
    [Address_City] nvarchar(max) NULL,
    [Address_Postcode] nvarchar(max) NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    CONSTRAINT [PK_EmployeesWithoutOrdering] PRIMARY KEY ([Id]));

在 EF Core 6.0 中, ColumnAttribute 可用來指定不同的資料行順序。 例如:

public class EntityBase
{
    [Column(Order = 1)]
    public int Id { get; set; }

    [Column(Order = 98)]
    public DateTime UpdatedOn { get; set; }

    [Column(Order = 99)]
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    [Column(Order = 2)]
    public string FirstName { get; set; }

    [Column(Order = 3)]
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    [Column(Order = 20)]
    public string Department { get; set; }

    [Column(Order = 21)]
    public decimal AnnualSalary { get; set; }

    public Address Address { get; set; }
}

[Owned]
public class Address
{
    [Column("House", Order = 10)]
    public string House { get; set; }

    [Column("Street", Order = 11)]
    public string Street { get; set; }

    [Column("City", Order = 12)]
    public string City { get; set; }

    [Required]
    [Column("Postcode", Order = 13)]
    public string Postcode { get; set; }
}

在 SQL Server 上,產生的資料表現在是:

CREATE TABLE [EmployeesWithOrdering] (
    [Id] int NOT NULL IDENTITY,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    [House] nvarchar(max) NULL,
    [Street] nvarchar(max) NULL,
    [City] nvarchar(max) NULL,
    [Postcode] nvarchar(max) NULL,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    CONSTRAINT [PK_EmployeesWithOrdering] PRIMARY KEY ([Id]));

這會將 FistNameLastName 資料行移至頂端,即使它們定義在基底類型中也一樣。 請注意,資料行順序值可以有間距,允許範圍一律在結尾放置資料行,即使由多個衍生類型使用也一樣。

此範例也會示範如何使用相同的 ColumnAttribute 方法來指定資料行名稱和順序。

您也可以使用 中的 OnModelCreating API 來 ModelBuilder 設定資料行順序。 例如:

modelBuilder.Entity<UsingModelBuilder.Employee>(
    entityBuilder =>
    {
        entityBuilder.Property(e => e.Id).HasColumnOrder(1);
        entityBuilder.Property(e => e.FirstName).HasColumnOrder(2);
        entityBuilder.Property(e => e.LastName).HasColumnOrder(3);

        entityBuilder.OwnsOne(
            e => e.Address,
            ownedBuilder =>
            {
                ownedBuilder.Property(e => e.House).HasColumnName("House").HasColumnOrder(4);
                ownedBuilder.Property(e => e.Street).HasColumnName("Street").HasColumnOrder(5);
                ownedBuilder.Property(e => e.City).HasColumnName("City").HasColumnOrder(6);
                ownedBuilder.Property(e => e.Postcode).HasColumnName("Postcode").HasColumnOrder(7).IsRequired();
            });

        entityBuilder.Property(e => e.Department).HasColumnOrder(8);
        entityBuilder.Property(e => e.AnnualSalary).HasColumnOrder(9);
        entityBuilder.Property(e => e.UpdatedOn).HasColumnOrder(10);
        entityBuilder.Property(e => e.CreatedOn).HasColumnOrder(11);
    });

在模型產生器上排序, HasColumnOrder 其優先順序高於 使用 ColumnAttribute 所指定的任何順序。 這表示 HasColumnOrder 可以用來覆寫使用屬性所做的排序,包括解決不同屬性上的屬性指定相同的訂單號碼時所發生的任何衝突。

重要

請注意,在一般情況下,大部分的資料庫只支援建立資料表時排序資料行。 這表示資料行順序屬性無法用來重新排序現有資料表中的資料行。 其中一個值得注意的例外是 SQLite,其中移轉會使用新的資料行順序重建整個資料表。

EF Core 最低 API

GitHub 問題: #25192

.NET Core 6.0 包含已更新的範本,其功能簡化的「最小 API」,可移除許多傳統上在 .NET 應用程式中所需的重複使用程式碼。

EF Core 6.0 包含新的擴充方法,可註冊 DbCoNtext 類型,並在單行中提供資料庫提供者的組態。 例如:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlite<MyDbContext>("Data Source=mydatabase.db");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlServer<MyDbContext>(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCosmos<MyDbContext>(
    "https://localhost:8081",
    "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==");

這些完全相當於:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlite("Data Source=mydatabase.db"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="));

注意

EF Core 最小 API 僅支援 DbCoNtext 和提供者的基本註冊和設定。 使用 AddDbContextAddDbContextPoolAddDbContextFactory 等來存取 EF Core 中可用的所有註冊和組態類型。

請參閱這些資源,以深入瞭解最少的 API:

在 SaveChangesAsync 中保留同步處理內容

GitHub 問題: #23971

我們已 在 5.0 版本中 變更 EF Core 程式碼,以在非同步程式碼的所有位置 await 設定 Task.ConfigureAwaitfalse 。 這通常是 EF Core 使用量的較佳選擇。 不過,這是一個特殊案例, SaveChangesAsync 因為 EF Core 會在非同步資料庫作業完成之後,將產生的值設定為追蹤的實體。 然後,這些變更可能會觸發通知,例如,可能需要在 U.I. 執行緒上執行。 因此,我們只會針對 方法還原 EF Core 6.0 中的 SaveChangesAsync 這項變更。

記憶體內部資料庫:驗證必要的屬性不是 Null

GitHub 問題: #10613 。 這項功能是由 @fagnercarvalho 所貢獻。 非常感謝!

如果嘗試儲存標示為必要屬性的 Null 值,EF Core 記憶體內部資料庫現在會擲回例外狀況。 例如,請考慮具有 User 必要 Username 屬性的類型:

public class User
{
    public int Id { get; set; }

    [Required]
    public string Username { get; set; }
}

嘗試以 Null Username 儲存實體會導致下列例外狀況:

Microsoft.EntityFrameworkCore.DbUpdateException:實體類型 'User' 實例缺少必要的屬性 '{'Username'}',且索引鍵值為 '{Id: 1}'。

如有必要,可以停用此驗證。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .LogTo(Console.WriteLine, new[] { InMemoryEventId.ChangesSaved })
        .UseInMemoryDatabase("UserContextWithNullCheckingDisabled", b => b.EnableNullChecks(false));
}

診斷和攔截器的命令來源資訊

GitHub 問題: #23719 。 這項功能是由 @Giorgi 所貢獻。 非常感謝!

CommandEventData提供給診斷來源和攔截器的 現在包含列舉值,指出 EF 負責建立命令的哪個部分。 這可作為診斷或攔截器中的篩選。 例如,我們可能想要只套用至來自 SaveChanges 的命令攔截器:

public class CommandSourceInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        if (eventData.CommandSource == CommandSource.SaveChanges)
        {
            Console.WriteLine($"Saving changes for {eventData.Context!.GetType().Name}:");
            Console.WriteLine();
            Console.WriteLine(command.CommandText);
        }

        return result;
    }
}

這只會在產生移轉和查詢的應用程式中使用時,將攔截器篩選為事件 SaveChanges 。 例如:

Saving changes for CustomersContext:

SET NOCOUNT ON;
INSERT INTO [Customers] ([Name])
VALUES (@p0);
SELECT [Id]
FROM [Customers]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

更好的暫存值處理

GitHub 問題: #24245

EF Core 不會在實體類型實例上公開暫存值。 例如,請考慮 Blog 具有存放區產生金鑰的實體類型:

public class Blog
{
    public int Id { get; set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

索引 Id 鍵屬性會在內容追蹤時 Blog 立即取得暫存值。 例如,呼叫 DbContext.Add 時:

var blog = new Blog();
context.Add(blog);

暫時值可以從內容變更追蹤器取得,但未設定為實體實例。 例如,此程式碼:

Console.WriteLine($"Blog.Id value on entity instance = {blog.Id}");
Console.WriteLine($"Blog.Id value tracked by EF = {context.Entry(blog).Property(e => e.Id).CurrentValue}");

產生下列輸出:

Blog.Id value on entity instance = 0
Blog.Id value tracked by EF = -2147482647

這很好,因為它會防止暫時值洩漏到應用程式程式碼中,而應用程式程式碼可能會意外被視為非暫時值。 不過,有時候直接處理暫存值會很有用。 例如,應用程式在追蹤實體圖形之前,可能會想要為實體的圖形產生自己的暫存值,以便使用外鍵來形成關聯性。 這可以藉由明確地將值標示為暫時來完成。 例如:

var blog = new Blog { Id = -1 };
var post1 = new Post { Id = -1, BlogId = -1 };
var post2 = new Post { Id = -2, BlogId = -1 };

context.Add(blog).Property(e => e.Id).IsTemporary = true;
context.Add(post1).Property(e => e.Id).IsTemporary = true;
context.Add(post2).Property(e => e.Id).IsTemporary = true;

Console.WriteLine($"Blog has explicit temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has explicit temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has explicit temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

在 EF Core 6.0 中,即使它現在標示為暫時,值仍會保留在實體實例上。 例如,上述程式碼會產生下列輸出:

Blog has explicit temporary ID = -1
Post 1 has explicit temporary ID = -1 and FK to Blog = -1
Post 2 has explicit temporary ID = -2 and FK to Blog = -1

同樣地,EF Core 所產生的暫存值可以明確設定為實體實例,並標示為暫存值。 這可用來使用其暫存索引鍵值,明確設定新實體之間的關聯性。 例如:

var post1 = new Post();
var post2 = new Post();

var blogIdEntry = context.Entry(blog).Property(e => e.Id);
blog.Id = blogIdEntry.CurrentValue;
blogIdEntry.IsTemporary = true;

var post1IdEntry = context.Add(post1).Property(e => e.Id);
post1.Id = post1IdEntry.CurrentValue;
post1IdEntry.IsTemporary = true;
post1.BlogId = blog.Id;

var post2IdEntry = context.Add(post2).Property(e => e.Id);
post2.Id = post2IdEntry.CurrentValue;
post2IdEntry.IsTemporary = true;
post2.BlogId = blog.Id;

Console.WriteLine($"Blog has generated temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has generated temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has generated temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

產生的結果:

Blog has generated temporary ID = -2147482647
Post 1 has generated temporary ID = -2147482647 and FK to Blog = -2147482647
Post 2 has generated temporary ID = -2147482646 and FK to Blog = -2147482647

為 C# 可為 Null 的參考型別加上批註的 EF Core

GitHub 問題: #19007

EF Core 程式碼基底現在會在整個期間使用 C# 可為 Null 的參考型別 (NRT)。 這表示當您從自己的程式碼使用 EF Core 6.0 時,將會取得 Null 使用量的正確編譯器指示。

Microsoft.Data.Sqlite 6.0

提示

您可以從 GitHub 下載範例程式碼,以執行並偵錯下列 所有範例。

連接共用

GitHub 問題: #13837

將資料庫連線保持開啟的時間盡可能少是常見的作法。 這有助於防止連線資源上的爭用。 這就是為什麼 EF Core 等程式庫在執行資料庫作業之前立即開啟連接,並在之後立即關閉它。 例如,請考慮此 EF Core 程式碼:

Console.WriteLine("Starting query...");
Console.WriteLine();

var users = context.Users.ToList();

Console.WriteLine();
Console.WriteLine("Query finished.");
Console.WriteLine();

foreach (var user in users)
{
    if (user.Username.Contains("microsoft"))
    {
        user.Username = "msft:" + user.Username;

        Console.WriteLine("Starting SaveChanges...");
        Console.WriteLine();

        context.SaveChanges();

        Console.WriteLine();
        Console.WriteLine("SaveChanges finished.");
    }
}

此程式碼的輸出已開啟連線記錄,為:

Starting query...

dbug: 8/27/2021 09:26:57.810 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

Query finished.

Starting SaveChanges...

dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.814 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

SaveChanges finished.

請注意,每個作業都會快速開啟和關閉連線。

不過,對於大部分的資料庫系統而言,開啟資料庫的實體連線是一項昂貴的作業。 因此,大部分 ADO.NET 提供者都會建立實體連線集區,並視需要將其出租給 DbConnection 實例。

SQLite 有點不同,因為資料庫存取通常只是存取檔案。 這表示開啟 SQLite 資料庫的連線通常非常快速。 不過,情況不一定如此。 例如,開啟加密資料庫的連線可能會非常慢。 因此,使用 Microsoft.Data.Sqlite 6.0 時,SQLite 連線現在會集區。

支援 DateOnly 和 TimeOnly

GitHub 問題: #24506

Microsoft.Data.Sqlite 6.0 支援 .NET 6 的新 DateOnlyTimeOnly 類型。 這些也可用於 EF Core 6.0 與 SQLite 提供者。 與 SQLite 一樣,其原生類型系統表示這些類型的值必須儲存為四個支援類型的其中一個。 Microsoft.Data.Sqlite 會將它們儲存為 TEXT 。 例如,使用下列類型的實體:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    
    public DateOnly Birthday { get; set; }
    public TimeOnly TokensRenewed { get; set; }
}

地圖 SQLite 資料庫中的下表:

CREATE TABLE "Users" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT,
    "Username" TEXT NULL,
    "Birthday" TEXT NOT NULL,
    "TokensRenewed" TEXT NOT NULL);

然後,就可以以正常方式儲存、查詢和更新值。 例如,此 EF Core LINQ 查詢:

var users = context.Users.Where(u => u.Birthday < new DateOnly(1900, 1, 1)).ToList();

已轉譯成 SQLite 上的下列內容:

SELECT "u"."Id", "u"."Birthday", "u"."TokensRenewed", "u"."Username"
FROM "Users" AS "u"
WHERE "u"."Birthday" < '1900-01-01'

而傳回只會在 1900 CE 之前搭配生日使用:

Found 'ajcvickers'
Found 'wendy'

Savepoints API

GitHub 問題: #20228

我們已針對 ADO.NET 提供者 中的儲存點,針對一般 API 進行標準化 。 Microsoft.Data.Sqlite 現在支援此 API,包括:

使用儲存點可讓交易的一部分回復,而不需要回復整個交易。 例如,下列程式碼:

  • 建立交易
  • 將更新傳送至資料庫
  • 建立儲存點
  • 將另一個更新傳送至資料庫
  • 回復至先前建立的儲存點
  • 認可交易
using var connection = new SqliteConnection("Command Timeout=60;DataSource=test.db");
connection.Open();

using var transaction = connection.BeginTransaction();

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'ajcvickers' WHERE Id = 1";
    command.ExecuteNonQuery();
}

transaction.Save("MySavepoint");

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'wfvickers' WHERE Id = 2";
    command.ExecuteNonQuery();
}

transaction.Rollback("MySavepoint");

transaction.Commit();

這會導致第一次更新認可至資料庫,而第二次更新則不會認可,因為儲存點在認可交易之前已回復。

連接字串中的命令逾時

GitHub 問題: #22505 。 這項功能是由 @nmichels 所貢獻。 非常感謝!

ADO.NET 提供者支援兩個不同的逾時:

  • 連接逾時,決定連線到資料庫時等待的時間上限。
  • 命令逾時,決定等候命令完成執行的時間上限。

您可以使用 從程式碼 DbCommand.CommandTimeout 設定命令逾時。 許多提供者現在也會在連接字串中公開此命令逾時。 Microsoft.Data.Sqlite 正以 Command Timeout 連接字串 關鍵字遵循此趨勢。 例如, "Command Timeout=60;DataSource=test.db" 將使用 60 秒做為連接所建立命令的預設逾時。

提示

Sqlite 會將 視為 Default TimeoutCommand Timeout 同義字,因此若偏好,則可以改用 。