次の方法で共有


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 では、datetime2PeriodEnd という 2 つの非表示のPeriodStart列が作成されます。 これらの "期間列" は、行内のデータが存在していた時間範囲を表します。 これらの列は EF Core モデルの シャドウ プロパティ にマップされ、後で示すようにクエリで使用できます。

Von Bedeutung

これらの列の時刻は、常に 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
    });

await context.SaveChangesAsync();

このデータは、通常の方法で照会、更新、および削除できます。 例えば次が挙げられます。

var employee = await context.Employees.SingleAsync(e => e.Name == "Rainbow Dash");
context.Remove(employee);
await context.SaveChangesAsync();

また、通常の 追跡クエリの後に、現在のデータの期間列の値に 追跡対象エンティティからアクセスできます。 例えば次が挙げられます。

var employees = await context.Employees.ToListAsync();
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 時刻にアクティブ (現在) であった行を返します。 これは、特定の主キーの現在のテーブルまたは履歴テーブルの 1 行です。
  • TemporalAll: 履歴データ内のすべての行を返します。 通常、これは、特定の主キーの履歴テーブルまたは現在のテーブルの多数の行です。
  • TemporalFromTo: 指定された 2 つの UTC 時刻の間にアクティブであったすべての行を返します。 与えられた主キーに対して、履歴テーブルおよび/または現在のテーブルから多くの行が含まれる場合があります。
  • TemporalBetween: TemporalFromToと同じですが、上限でアクティブになった行が含まれる点が異なります。
  • TemporalContainedIn: アクティブになり始め、終了した 2 つの UTC 時刻の間にアクティブになっているすべての行を返します。 与えられた主キーに対して、履歴テーブルおよび/または現在のテーブルから多くの行が含まれる場合があります。

これらの各演算子に含まれる行の詳細については、 SQL Server テンポラル テーブルのドキュメント を参照してください。

たとえば、データの更新と削除を行った後、 TemporalAll を使用してクエリを実行して履歴データを表示できます。

var history = await 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")
        })
    .ToListAsync();

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

EF.Property メソッドが期間列の値にアクセスする方法をご覧ください。 これは、データを並べ替えるために 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 分に、最後に返された行がアクティブでなくなったことに注意してください。 これは、その時点で、レインボー ダッシュの行がメイン テーブルから削除されたためです。 このデータを復元する方法については、後で説明します。

同様のクエリは、 TemporalFromToTemporalBetween、または TemporalContainedInを使用して記述できます。 例えば次が挙げられます。

var history = await 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")
        })
    .ToListAsync();

このクエリは、次の行を返します。

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 = await context
    .Employees
    .TemporalAsOf(timeStamp2)
    .SingleAsync(e => e.Name == "Rainbow Dash");

context.Add(employee);
await context.SaveChangesAsync();

このクエリは、指定された UTC 時刻と同じように、レインボー ダッシュの 1 行を返します。 テンポラル演算子を使用するすべてのクエリは既定では追跡されないため、ここで返されるエンティティは追跡されません。 これは、現在メイン テーブルに存在しないため、理にかなっています。 エンティティをメイン テーブルに再挿入するには、エンティティを 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 オプションを使用して、既存のバンドルを新しいバンドルで上書きできることに注意してください。

この新しいバンドルを実行すると、次の 2 つの新しい移行がデータベースに適用されます。

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>

今回は、3 つの移行がすべて適用されたことに注意してください。これらはいずれも運用データベースにまだ適用されていないためです。

その他のオプションをコマンド ラインに渡すことができます。 一般的な選択肢は次のとおりです。

  • --output をクリックして、作成する実行可能ファイルのパスを指定します。
  • --context プロジェクトに複数のコンテキスト型が含まれている場合に使用する DbContext 型を指定します。
  • --project をクリックして、使用するプロジェクトを指定します。 デフォルトで現在の作業ディレクトリが使われます。
  • --startup-project をクリックして、使用するスタートアップ プロジェクトを指定します。 デフォルトで現在の作業ディレクトリが使われます。
  • --no-build コマンドを実行する前にプロジェクトがビルドされないようにします。 これは、プロジェクトが -date up-toことがわかっている場合にのみ使用してください。
  • --verbose コマンドの動作に関する詳細情報を表示します。 このオプションは、バグ レポートに情報を含む場合に使用します。

使用可能なすべてのオプションを表示するには、 dotnet ef migrations bundle --help を使用します。

既定では、各移行は独自のトランザクションに適用されることに注意してください。 この領域の将来の拡張機能の詳細については、 GitHub の問題 #22616 を参照してください。

事前コンベンションモデルの設定

GitHub の問題: #12229

以前のバージョンの EF Core では、そのマッピングが既定値と異なる場合に、特定の型のすべてのプロパティのマッピングを明示的に構成する必要があります。 これには、文字列の最大長や 10 進精度などの "ファセット" と、プロパティ型の値変換が含まれます。

これには、次のいずれかが必要です。

  • 各プロパティのモデル ビルダーの構成
  • 各プロパティのマッピング属性
  • すべてのエンティティ型のすべてのプロパティに対する明示的な反復と、モデルの構築時の低レベルメタデータ API の使用。

このイテレーションが行われる時点では、エンティティ型とマップされたプロパティの一覧が最終的ではない可能性があるため、明示的な反復はエラーが発生しやすく、堅牢に行うのが難しいことに注意してください。

EF Core 6.0 では、このマッピング構成を特定の型に対して 1 回指定できます。 その後、モデル内のその型のすべてのプロパティに適用されます。 これは、モデル構築規則で使用されるモデルの側面を構成するため、"事前規則モデル構成" と呼ばれます。 このような構成は、ConfigureConventionsDbContextをオーバーライドすることによって適用されます。

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

すべての文字列プロパティは、(Unicode ではなく) ANSI に構成でき、最大長は 1024 です。

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

DateTimes から longs への既定の変換を使用して、すべての DateTime プロパティをデータベース内の 64 ビット整数に変換できます。

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 との間でシリアル化されます。

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

この値コンバーターは、Money のすべての用途に対して 1 回構成できます。

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

また、シリアル化された JSON が格納される文字列列に対して追加のファセットを指定できることにも注意してください。 この場合、列の最大長は 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);

これはほとんど必要ありませんが、型がモデルのマップされたプロパティと相関しない方法でクエリで使用される場合に役立ちます。

コンベンションの事前設定モデル構成の詳細と例についてさらに議論するためには、`.NET Blog` の「Entity Framework Core 6.0 Preview 6: Configure Conventions」をご覧ください。

コンパイル済みモデル

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

制限事項

コンパイル済みモデルにはいくつかの制限があります。

これらの制限のため、コンパイル済みモデルは、EF Core のスタートアップ時間が遅すぎる場合にのみ使用する必要があります。 通常、小さいモデルのコンパイルに使用しても効果はありません。

これらの機能のサポートが成功に不可欠な場合は、上記でリンクされている該当するイシューに投票してください。

ベンチマーク

ヒント

GitHub からサンプル コードをダウンロードして、大規模なモデルのコンパイルとベンチマークの実行を試すことができます。

上記で参照されている GitHub リポジトリのモデルには、449 個のエンティティ型、6390 のプロパティ、720 のリレーションシップが含まれています。 これは中程度に大きなモデルです。 BenchmarkDotNet を使用して測定すると、最初のクエリの平均時間は、合理的に強力なラップトップで 1.02 秒になります。 コンパイル済みモデルを使用すると、これは同じハードウェアで 117 ミリ秒にダウンします。 このような 8 倍から 10 倍の改善は、モデルのサイズが大きくなると比較的一定のままです。

コンパイル済みモデルのパフォーマンスの向上

EF Core のスタートアップ パフォーマンスとコンパイル済みモデルの詳細については、.NET ブログの 「Entity Framework Core 6.0 Preview 5: コンパイル 済みモデルの発表」を参照してください。

TechEmpower Fortunes のパフォーマンスの向上

GitHub の問題: #23611

EF Core 6.0 のクエリ パフォーマンスが大幅に改善されました。 具体的には:

  • EF Core 6.0 のパフォーマンスは、業界標準の TechEmpower Fortunes ベンチマークで 5.0 と比較して 70% 高速になりました。
    • これは、ベンチマーク コード、.NET ランタイムなどの機能強化を含む、フル スタックパフォーマンスの向上です。
  • EF Core 6.0 自体は 31%、追跡されていないクエリの実行速度が向上します。
  • クエリの実行時にヒープの割り当てが 43% 削減されました。

これらの改善の後、TechEmpower Fortunes ベンチマークにおいて一般的な「micro-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 からサンプル コードをダウンロードすることで、Cosmos 固有のすべてのサンプルを実行してデバッグできます。

既定では暗黙的な所有権

GitHub の問題: #24803

Azure Cosmos DB プロバイダーのモデルを構築する場合、EF Core 6.0 では、子エンティティ型が既定で親エンティティによって所有されているとマークされます。 これにより、Azure Cosmos DB モデルでの OwnsMany 呼び出しと OwnsOne 呼び出しの多くが不要になります。 これにより、親型のドキュメントに子型を簡単に埋め込むことができます。これは通常、ドキュメント データベース内の親と子をモデル化する適切な方法です。

たとえば、次のエンティティ型を考えてみましょう。

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

EF Core 6.0 は、Azure Cosmos DB データベース プロバイダーを使用する場合に、プリミティブ型のコレクションをネイティブにマップします。 たとえば、次のエンティティ型を考えてみましょう。

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);
await context.SaveChangesAsync();

これにより、次の 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";

await context.SaveChangesAsync();

制限事項:

  • 文字列キーを持つディクショナリのみがサポートされています
  • プリミティブ コレクションの内容へのクエリは現在サポートされていません。 これらの機能が重要な場合は、 #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 STRINGEQUALS 大文字と小文字を区別しない呼び出しのみ

LOWERLTRIMRTRIMTRIMUPPERSUBSTRINGの翻訳は、@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.Abs または MathF.Abs ABS
Math.Acos または MathF.Acos ACOS
Math.Asin または MathF.Asin ASIN
Math.Atan または MathF.Atan ATAN
Math.Atan2 または MathF.Atan2 ATN2
Math.Ceiling または MathF.Ceiling CEILING
Math.Cos または MathF.Cos COS
Math.Exp または MathF.Exp EXP
Math.Floor または MathF.Floor FLOOR
Math.Log または MathF.Log LOG
Math.Log10 または MathF.Log10 LOG10
Math.Pow または MathF.Pow POWER
Math.Round または MathF.Round ROUND
Math.Sign または MathF.Sign SIGN
Math.Sin または MathF.Sin SIN
Math.Sqrt または MathF.Sqrt SQRT
Math.Tan または MathF.Tan TAN
Math.Truncate または MathF.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

LINQ を使用する代わりに、生の SQL クエリを実行することが必要な場合があります。 これは、 FromSql メソッドを使用して Azure Cosmos DB プロバイダーでサポートされるようになりました。 これは、リレーショナル プロバイダーと常に同じように機能します。 例えば次が挙げられます。

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) は、必要に応じてこれらのイベントに含まれます。

ここで示すログには、ID 値が表示されるように EnableSensitiveDataLogging() が使用されています。

Azure Cosmos DB データベースに項目を挿入すると、 CosmosEventId.ExecutedCreateItem イベントが生成されます。 たとえば、次のコードを使用します。

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

次の診断イベントをログに記録します。

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 イベントが生成され、読み取られた項目の 1 つ以上の CosmosEventId.ExecutedReadNext イベントが生成されます。 たとえば、次のコードを使用します。

var equilateral = await context.Triangles.SingleAsync(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 データベースから 1 つの項目を取得すると、 CosmosEventId.ExecutingReadItem イベントと CosmosEventId.ExecutedReadItem イベントが生成されます。 たとえば、次のコードを使用します。

var isosceles = await context.Triangles.FindAsync("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;
await context.SaveChangesAsync();

次の診断イベントをログに記録します。

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);
await context.SaveChangesAsync();

次の診断イベントをログに記録します。

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によって提供されました。 どうもありがとう!

Azure Cosmos DB プロバイダーによって使用される HttpClientFactory を明示的に設定できるようになりました。 これは、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 プロバイダーの取得」を参照してください。

既存のデータベースからのスキャフォールディングの機能強化

EF Core 6.0 には、既存のデータベースから EF モデルをリバース エンジニアリングする際にいくつかの機能強化が含まれています。

多対多リレーションシップの構築

GitHub の問題: #22475

EF Core 6.0 では、単純な結合テーブルが検出され、それらの多対多マッピングが自動的に生成されます。 たとえば、 PostsTagsのテーブルと、それらを接続 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");
            });

C# の null 許容参照型をスキャフォールディングする

GitHub の問題: #15520

EF Core 6.0 では、C# の null 許容参照型 (NRT) を採用する EF モデルやエンティティ型がスキャフォールディングされるようになりました。 コードがスキャフォールディングされている C# プロジェクトで NRT サポートが有効になっている場合、NRT の使用は自動的にスキャフォールディングされます。

たとえば、次の Tags テーブルには、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 非許容 (必須) のリレーションシップが組み立てられます。

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 = await context.People
    .Include(e => e.Shoes)
    .GroupBy(e => e.FirstName)
    .Select(
        g => g.OrderBy(e => e.FirstName)
            .ThenBy(e => e.LastName)
            .FirstOrDefault())
    .ToListAsync();
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 = await context.People
    .Select(
        p => new
        {
            p.FirstName,
            FullName = p.FirstName + " " + p.MiddleInitial + " " + p.LastName
        })
    .GroupBy(p => p.FirstName)
    .Select(g => g.First())
    .FirstAsync();
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 = await context.People
    .Where(e => e.MiddleInitial == "Q" && e.Age == 20)
    .GroupBy(e => e.LastName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e.Length)
    .ToListAsync();
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 = await (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()
               })
    .ToListAsync();
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 = await context.People
    .GroupBy(e => e.FirstName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e)
    .ToListAsync();
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 = await context.People
    .Where(e => e.Age == 20)
    .GroupBy(e => e.Id)
    .Select(g => g.First().MiddleInitial)
    .OrderBy(e => e)
    .ToListAsync();
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
    = await 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),
            })
        .ToListAsync();
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 = await 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)
        })
    .CountAsync();
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 = await context.People
    .GroupBy(n => n.FirstName)
    .Select(g => new
    {
        Feet = g.Key,
        Total = g.Sum(n => n.Feet.Size)
    })
    .ToListAsync();
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 = await 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)
    .ToListAsync();
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 = await 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()})
    .ToListAsync();
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 = await context.People
    .GroupBy(m => new {m.FirstName, m.MiddleInitial })
    .Select(am => new
    {
        Key = am.Key,
        Items = am.ToList()
    })
    .ToListAsync();
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 = await context.Shards
    .Where(e => string.Concat(e.Token1, e.Token2, e.Token3) != e.TokensProcessed).ToListAsync();

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

System.Linq.Async とのよりスムーズな統合

GitHub の問題: #24041

System.Linq.Async パッケージは、クライアント側の非同期 LINQ 処理を追加します。 非同期 LINQ メソッドの名前空間の競合により、このパッケージを以前のバージョンの EF Core で使用するのは面倒でした。 EF Core 6.0 では、公開されている EF Core IAsyncEnumerable<T>がインターフェイスを直接実装する必要がないように、DbSet<TEntity>の C# パターン マッチングを利用しています。

EF Core クエリは通常、サーバー上で完全に変換されるため、ほとんどのアプリケーションで System.Linq.Async を使用する必要はありません。

GitHub の問題: #23921

EF Core 6.0 では、 FreeText(DbFunctions, String, String)Containsのパラメーター要件が緩和されました。 これにより、これらの関数をバイナリ列または値コンバーターを使用してマップされた列で使用できます。 たとえば、値オブジェクトとして定義された Name プロパティを持つエンティティ型について考えてみます。

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

プロパティの型がContainsされていない場合でも、FreeTextまたはNameを使用してクエリstring実行できるようになりました。 例えば次が挙げられます。

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

これにより、SQL Server を使用すると、次の SQL が生成されます。

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

SQLite で ToString を翻訳する

GitHub の問題: #17223。 この機能は 、@ralmsdeveloperによって提供されました。 どうもありがとう!

SQLite データベース プロバイダーを使用すると、 ToString() の呼び出しが 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 = await context.Users.Where(u => EF.Functions.Like(u.PhoneNumber.ToString(), "%555%")).ToListAsync();

これは、SQLite データベースを使用する場合、次の SQL に変換されます。

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

SQL Server の ToString() の変換は EF Core 5.0 で既にサポートされており、他のデータベース プロバイダーでもサポートされる場合があることに注意してください。

エフ。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 = await context.Users.Where(u => u.Popularity == (int)(EF.Functions.Random() * 4.0) + 1).ToListAsync();

これは、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 = await context.Users.Where(
    e => string.IsNullOrWhiteSpace(e.FirstName)
         || string.IsNullOrWhiteSpace(e.LastName)).ToListAsync();

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では、 CustomerDensitiesに返されるデータを定義する LINQ クエリを記述できます。

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 = await context.CustomerDensities.ToListAsync();

1 つのパラメーターを使用して部分文字列を変換する

GitHub の問題: #20173。 この機能は 、@stevendarbyによって提供されました。 どうもありがとう!

EF Core 6.0 では、 string.Substring の使用が 1 つの引数で変換されるようになりました。 例えば次が挙げられます。

var result = await context.Customers
    .Select(a => new { Name = a.Name.Substring(3) })
    .FirstOrDefaultAsync(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 つのクエリまたは複数のクエリに変換する方法を示しています。

例 1:

LINQ クエリ:

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

単一の 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 クエリ:

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

単一の 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 クエリ:

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

単一の 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 この句は削除されます。

たとえば、次のクエリを考えてみましょう。

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

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によって提供されました。 どうもありがとう!

クエリ タグを使用すると、生成された SQL に含まれるようなテキスト タグを LINQ クエリに追加できます。 EF Core 6.0 では、これを使用して、LINQ コードのファイル名と行番号でクエリにタグを付けることができます。 例えば次が挙げられます。

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

これにより、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

テーブルをそのプリンシパル エンティティと共有するときに、オプションの依存エンティティが存在するかどうかを知るのは難しくなります。 これは、依存先が存在するかどうかに関係なく、プリンシパルが必要とするため、テーブルに関連する行があるからです。 これを明確に処理する方法は、依存するプロパティが少なくとも 1 つ必要であることを確認することです。 必須プロパティを 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としてマークすることで保証されます。

ここで、顧客がクエリを実行するとき、郵便番号列が null の場合は、顧客に住所がないことを意味し、 Customer.Address ナビゲーション プロパティは null のままです。 たとえば、顧客を反復処理し、アドレスが null であるかどうかを確認します。

await foreach (var customer in context.Customers1.AsAsyncEnumerable())
{
    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()
    });

ただし、データベースでは、次の 2 つのケースは区別できません。データベース列に直接クエリを実行するとわかります。

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 である場合に、オプションの依存ファイルを保存するときに警告が表示されるようになりました。 例えば次が挙げられます。

警告: 2021年9月27日 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) 型 'Address' のエンティティ(主キー値: {CustomerId: -2147482646})は、テーブルの共有を利用したオプションの依存エンティティです。 エンティティには、エンティティが存在するかどうかを識別するための既定値以外のプロパティはありません。 クエリが実行されるとき、すべてのプロパティが既定値に設定されたインスタンスの代わりに、オブジェクト インスタンスがまったく作成されないことを意味します。 入れ子になった依存も失われます。 既定値のみを使用してインスタンスを保存したり、受信ナビゲーションをモデルで必要に応じてマークしたりしないでください。

これは、オプションの依存自体が、追加の省略可能な依存の主要な役割を果たしており、さらに同じテーブルにマップされる場合は、さらに複雑になります。 EF Core 6.0 では、単なる警告ではなく、入れ子になったオプションの依存のケースのみを禁止します。 たとえば、 ContactInfoCustomer によって所有され、 AddressContactInfoによって所有されている次のモデルを考えてみましょう。

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 許容プロパティ値を含み、プリンシパルと同じテーブルを共有する場合を回避することです。 これを回避するには、次の 3 つの簡単な方法があります。

  1. 依存関係を必須にする。 つまり、すべてのプロパティが null の場合でも、依存エンティティはクエリ後に常に値を持つことになります。
  2. 前述のように、依存するプロパティに少なくとも 1 つの必須プロパティが含まれていることを確認します。
  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 には、データベースへのマップ方法を変更するためにコードに適用できる新しい属性がいくつか含まれています。

ユニコード属性

GitHub の問題: #19794。 この機能は 、@RaymondHuyによって提供されました。 どうもありがとう!

EF Core 6.0 以降では、 データベースの種類を直接指定せずにマッピング属性を使用して、文字列プロパティを Unicode 以外の列にマップできるようになりました。 たとえば、"ISBN 978-3-16-148410-0" という形式のBook のプロパティを持つ エンティティ型について考えてみましょう。

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によって提供されました。 どうもありがとう!

データベース列の有効桁数と小数点以下桁数は、データベースタイプを直接指定することなくマッピング属性を使用して設定できるようになりました。 たとえば、10 進数のProduct プロパティを持つ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]));

エンティティタイプ構成属性

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 以降では、EF Core が適切な構成を検索して使用できるように、エンティティ型に EntityTypeConfigurationAttribute を配置できます。 例えば次が挙げられます。

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

この属性は、IEntityTypeConfiguration エンティティ型がモデルに含まれるたびに、EF Core が指定されたBook実装を使用することを意味します。 エンティティ型は、通常のメカニズムのいずれかを使用してモデルに含まれます。 たとえば、エンティティ型の 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 値を格納するように最適化された通常の列です。 これは、使用頻度の低いサブタイプのプロパティがテーブル内のほとんどの行で null 列値になる TPH 継承マッピング を使用する場合に便利です。 たとえば、ForumModeratorから拡張されるForumUser クラスについて考えてみます。

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

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

数百万人のユーザーが存在する可能性があり、モデレーターはほんの一部です。 つまり、 ForumName をスパースとしてマッピングすると、ここで意味があります。 これは、IsSparseOnModelCreatingを使用して構成できるようになりました。 例えば次が挙げられます。

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"、および "Euro" として HasConversion<string>使用して保存するように構成できます。 例えば次が挙げられます。

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

EF Core 6.0 以降では、ジェネリック型で 値コンバーター型を指定できます。 これは、組み込みの値コンバーターの 1 つです。 たとえば、列挙値を 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

2 つのエンティティ型間のあいまいな多対多リレーションシップは、規則によって検出されます。 必要に応じて、または必要に応じて、ナビゲーションを明示的に指定できます。 例えば次が挙げられます。

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

どちらの場合も、EF Core は、2 つの型間の結合エンティティとして機能する 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

Von Bedeutung

以下で説明する問題により、null の変換を許可する ValueConverter のコンストラクターは、EF Core 6.0 リリースの [EntityFrameworkInternal] でマークされています。 これらのコンストラクターを使用すると、ビルド警告が生成されるようになりました。

値コンバーターでは、通常、null から他の値への変換は許可されません。 これは、NULL 許容型と null 非許容型の両方に同じ値コンバーターを使用できるためです。これは、FK が null 許容であることが多く、PK が無効である PK/FK の組み合わせに非常に役立ちます。

EF Core 6.0 以降では、null を変換する値コンバーターを作成できます。 しかし、この機能の検証により、多くの落とし穴で実際には非常に問題であることが明らかになっています。 例えば次が挙げられます。

これらは簡単な問題ではなく、クエリの問題では検出が容易ではありません。 そのため、この機能は EF Core 6.0 の内部としてマークされています。 引き続き使用できますが、コンパイラの警告が表示されます。 警告は、 #pragma warning disable EF1001を使用して無効にすることができます。

null を変換すると便利な例の 1 つは、データベースに 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 });

await context.SaveChangesAsync();

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 ファクトリの機能強化

AddDbContextFactory では、DbContext も直接登録されます

GitHub の問題: #25164

場合によっては、DbContext 型 、その型のコンテキストのファクトリの両方をアプリケーション依存関係挿入 (D.I.) コンテナーに登録すると便利な場合があります。 これにより、たとえば、DbContext のスコープ付きインスタンスを要求スコープから解決できますが、ファクトリを使用して必要に応じて複数の独立したインスタンスを作成できます。

これをサポートするために、 AddDbContextFactory では、DbContext 型もスコープ付きサービスとして登録されるようになりました。 たとえば、アプリケーションの D.I. コンテナーでのこの登録を考えてみましょう。

var container = services
    .AddDbContextFactory<SomeDbContext>(
        builder => builder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample;ConnectRetryCount=0"))
    .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
}

この場合、コンテナー スコープが破棄されるときにコンテキスト インスタンスが破棄されます。コンテキストを明示的に破棄しないでください。

より高いレベルでは、ファクトリの DbContext を他の D.I. 型に挿入できることを意味します。 例えば次が挙げられます。

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

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

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

        var results1 = await context1.Blogs.ToListAsync();
        var results2 = await context2.Blogs.ToListAsync();

        // Contexts obtained from the factory must be explicitly disposed
    }
}

または:

private class MyController1
{
    private readonly SomeDbContext _context;

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

    public async Task DoSomething()
    {
        var results = await _context.Blogs.ToListAsync();

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

DbContextFactory が DbContext パラメーターなしのコンストラクターを無視する

GitHub の問題: #24124

EF Core 6.0 では、パラメーターなしの DbContext コンストラクターと、 DbContextOptions を受け取るコンストラクターの両方を、 AddDbContextFactoryを介してファクトリを登録するときに同じコンテキスト型で使用できるようになりました。 たとえば、上記の例で使用されているコンテキストには、次の両方のコンストラクターが含まれています。

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;ConnectRetryCount=0")
    .Options;

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

その後、ファクトリを使用してインスタンスを作成およびプールできます。 例えば次が挙げられます。

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

移行を使用してテーブルを作成するときに、OrderColumnAttribute プロパティを使用して列を並べ替えることができるようになりました。 たとえば、次のモデルを考えてみましょう。

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

これにより、基本型で定義されている場合でも、 FistName 列と LastName 列が一番上に移動されます。 列の順序の値にはギャップがあり、複数の派生型で使用されている場合でも、範囲を使用して常に最後に列を配置できることに注意してください。

この例では、列名と順序の両方を指定するために同じ ColumnAttribute を使用する方法も示します。

列の順序は、ModelBuilderOnModelCreating API を使用して構成することもできます。 例えば次が挙げられます。

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 を使用して、異なるプロパティの属性が同じ順序番号を指定した場合の競合の解決など、属性で行われた順序をオーバーライドできます。

Von Bedeutung

一般的なケースでは、ほとんどのデータベースはテーブルの作成時にのみ列の順序付けをサポートします。 つまり、列の順序属性を使用して、既存のテーブルの列を並べ替えることはできません。 これに対する注目すべき例外の 1 つは SQLite です。この場合、移行によってテーブル全体が新しい列の順序で再構築されます。

EF Coreの最小限API(Minimal API)

GitHub の問題: #25192

.NET Core 6.0 には、.NET アプリケーションで従来必要だった定型コードの多くを削除する簡略化された "最小限の API" を備えた更新されたテンプレートが含まれています。

EF Core 6.0 には、DbContext 型を登録し、データベース プロバイダーの構成を 1 行で提供する新しい拡張メソッドが含まれています。 例えば次が挙げられます。

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;ConnectRetryCount=0"));
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 コードを、非同期コードTask.ConfigureAwaitするすべての場所でfalseawaitに設定するように変更しました。 これは通常、EF Core の使用に適した選択肢です。 ただし、 SaveChangesAsync は特殊なケースです。EF Core では、非同期データベース操作の完了後に、生成された値が追跡対象エンティティに設定されるためです。 これらの変更によって通知がトリガーされ、たとえば、U.I. スレッドで実行する必要がある場合があります。 そのため、ef Core 6.0 では、 SaveChangesAsync メソッドの場合にのみ、この変更を元に戻します。

メモリ内データベース: 必要なプロパティが null ではないことを検証する

GitHub の問題: #10613。 この機能は 、@fagnercarvalhoによって提供されました。 どうもありがとう!

EF Core のメモリ内データベースでは、必須としてマークされたプロパティの null 値を保存しようとすると、例外をスローするようになりました。 たとえば、必要なUser プロパティを持つUsername型について考えてみましょう。

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

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

null Username を持つエンティティを保存しようとすると、次の例外が発生します。

Microsoft.EntityFrameworkCore.DbUpdateException: キー値 '{Id: 1}' のエンティティ型 'User' のインスタンスに必要なプロパティ '{'Username'}' がありません。

この検証は、必要に応じて無効にすることができます。 例えば次が挙げられます。

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# のNullable型参照 (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 = await context.Users.ToListAsync();

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

        await context.SaveChangesAsync();

        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 の新しい DateOnly 型と TimeOnly 型がサポートされています。 これらは、SQLite プロバイダーと共に EF Core 6.0 でも使用できます。 SQLite の場合と同様に、ネイティブ型システムは、これらの型の値を、サポートされている 4 つの型のいずれかとして格納する必要があることを意味します。 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 = await context.Users.Where(u => u.Birthday < new DateOnly(1900, 1, 1)).ToListAsync();

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

microsoft では、 ADO.NET プロバイダーのセーブポイント用の一般的な API を標準化してきました。 Microsoft.Data.Sqlite では、次のようなこの API がサポートされるようになりました。

  • Save(String) トランザクションにセーブポイントを作成するには
  • Rollback(String) 前のセーブポイントにロールバックするには
  • Release(String) セーブポイントを解放するには

セーブポイントを使用すると、トランザクション全体をロールバックすることなく、トランザクションの一部をロールバックできます。 たとえば、次のコードを次に示します。

  • トランザクションを作成します。
  • データベースに更新を送信する
  • セーブポイントを作成します。
  • データベースに別の更新プログラムを送信する
  • 前に作成したセーブポイントにロールバックする
  • トランザクションをコミットします
using var connection = new SqliteConnection("Command Timeout=60;DataSource=test.db");
await connection.OpenAsync();

await using var transaction = await connection.BeginTransactionAsync();

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

await transaction.SaveAsync("MySavepoint");

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

await transaction.RollbackAsync("MySavepoint");

await transaction.CommitAsync();

これにより、最初の更新がデータベースにコミットされ、2 番目の更新は、トランザクションをコミットする前にセーブポイントがロールバックされてからコミットされません。

接続文字列のコマンド タイムアウト

GitHub の問題: #22505。 この機能は 、@nmichelsによって提供されました。 どうもありがとう!

ADO.NET プロバイダーでは、次の 2 つの異なるタイムアウトがサポートされます。

  • 接続タイムアウト。データベースへの接続時に待機する最大時間を決定します。
  • コマンドのタイムアウト。コマンドの実行が完了するまでの最大待機時間を決定します。

コマンド タイムアウトは、 DbCommand.CommandTimeoutを使用してコードから設定できます。 多くのプロバイダーも、接続文字列でこのコマンド タイムアウトを公開しています。 Microsoft.Data.Sqlite は、 Command Timeout 接続文字列キーワードを使用してこの傾向に従います。 たとえば、 "Command Timeout=60;DataSource=test.db" は、接続によって作成されたコマンドの既定のタイムアウトとして 60 秒を使用します。

ヒント

Sqlite は Default TimeoutCommand Timeout のシノニムとして扱うので、必要に応じて代わりに使用できます。