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

重要

これらの列の時刻は、常に、SQL Server によって生成される UTC 時刻です。 次に示すクエリにあるように、テンポラル テーブルに関連するすべての操作には、UTC 時刻が使用されます。

また、関連して EmployeeHistory という履歴テーブルが自動的に作成されることにも注意してください。 期間列と履歴テーブルの名前は、モデル ビルダーに構成を追加することで変更できます。 次に例を示します。

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

これは SQL Server によって作成されるテーブルに反映されます。

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

テンポラル テーブルの使用

ほとんどの場合、テンポラル テーブルは他のテーブルと同じように使用されます。 つまり、期間列と履歴データは SQL Server によって透過的に処理され、アプリケーションではそれらを無視できます。 たとえば、新しいエンティティは、通常の方法でデータベースに保存できます。

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

context.SaveChanges();

その後、通常の方法でこのデータのクエリ、更新、削除を行うことができます。 次に例を示します。

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

また、通常の追跡クエリの後で、現在のデータの期間列の値に、追跡対象のエンティティからアクセスすることができます。 次に例を示します。

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

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

出力は次のとおりです。

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

ValidTo 列 (既定では PeriodEnd という名前) には、datetime2 の最大値が含まれていることに注意してください。 テーブル内の現在の行に対しては、常にこのようになります。 ValidFrom 列 (既定では PeriodStart という名前) には、行が挿入された UTC 時刻が含まれます。

履歴データのクエリ

EF Coreでは、いくつかの新しいクエリ演算子を使用して履歴データを含むクエリがサポートされています。

  • TemporalAsOf: 指定された UTC 時刻にアクティブ (現在) であった行を返します。 これは、現在のテーブルまたは履歴テーブル内の指定された主キーの 1 行です。
  • TemporalAll: 履歴データ内のすべての行を返します。 通常、これは履歴テーブルおよび/または現在のテーブル内の指定された主キーの多くの行です。
  • TemporalFromTo: 指定された 2 つの UTC 時刻の間にアクティブであったすべての行を返します。 これは履歴テーブルおよび/または現在のテーブル内の指定された主キーの多くの行である場合があります。
  • TemporalBetween: TemporalFromTo と同じですが、上限の時点でアクティブになった行が含まれる点が異なります。
  • TemporalContainedIn: 指定された 2 つの UTC 時刻の間にアクティブ状態を開始して終了したすべての行を返します。 これは履歴テーブルおよび/または現在のテーブル内の指定された主キーの多くの行である場合があります。

注意

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

たとえば、データに対していくつかの更新と削除を行った後、TemporalAll を使用してクエリを実行し、履歴データを見ることができます。

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

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

EF.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

最後に返された行が、8/26/2021 4:44:59 PM にアクティブ状態を停止したことに注意してください。 これは、Rainbow Dash の行がその時点でメイン テーブルから削除されたためです。 このデータを復元する方法については、後で説明します。

TemporalFromToTemporalBetween、または TemporalContainedIn を使用して、同様のクエリを記述できます。 次に例を示します。

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

このクエリからは、次の行が返されます。

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

履歴データの復元

前に説明したように、Rainbow Dash は Employees テーブルから削除されました。 これは明らかに間違いだったので、特定の時点に戻り、その時点から不足している行を復元してみましょう。

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

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

このクエリからは、指定した UTC 時刻に存在していた Rainbow Dash の 1 行が返されます。 既定では、テンポラル演算子を使用するすべてのクエリは追跡されないので、ここで返されるエンティティは追跡されません。 現在それはメイン テーブルに存在していないので、これは当然のことです。 そのエンティティをメイン テーブルに再挿入するには、それを Added としてマークしてから、SaveChanges を呼び出すだけです。

Rainbow Dash の行を再挿入した後、履歴データのクエリを実行すると、特定の 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 モデルへの変更に基づいてデータベース スキーマ更新を生成するには、EF Core の移行が使用されます。 多くの場合、継続的インテグレーション/継続的配置 (C.I./C.D.) システムの一部として、これらのスキーマ更新をアプリケーションの展開時に適用する必要があります。

EF Core に、これらのスキーマ更新を適用する新しい方法として、移行バンドルが含まれるようになりました。 移行バンドルは、移行と、これらの移行をデータベースに適用するために必要なコードが含まれる、小さな実行可能ファイルです。

Note

移行、バンドル、展開に関する詳細な説明については、.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 を指定すると、コマンドを実行する前にプロジェクトがビルドされなくなります。 これは、プロジェクトが最新であることがわかっている場合にのみ使用する必要があります。
  • --verbose を指定すると、コマンドで行われていることに関する詳細情報が表示されます。 バグ レポートに情報を含める場合は、このオプションを使用します。

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

既定では、各移行は独自のトランザクションで適用されることに注意してください。 この領域において将来可能性のある機能強化については、GitHub イシュー #22616 を参照してください。

規則の前のモデル構成

GitHub イシュー: #12229

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

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

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

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

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

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

DateTime から long への既定の変換を使用して、すべての 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);

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

Note

規則の前のモデル構成の詳細と例については、「Entity Framework Core 6.0 プレビュー 6 のお知らせ: 規則を構成する」を参照してください。

コンパイル済みモデル

GitHub イシュー: #1906

コンパイル済みモデルを使用すると、大きなモデルが含まれるアプリケーションで、EF Core のスタートアップ時間を短縮できます。 大きなモデルとは、通常、エンティティ型とリレーションシップの数が 100 から 1,000 のものを意味します。

スタートアップ時間とは、アプリケーションで 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 個のエンティティ型、6,390 個のプロパティ、720 個のリレーションシップが含まれています。 これは中程度の大きなモデルです。 BenchmarkDotNet を使用して測定すると、かなり強力なノート PC で、最初のクエリまでの平均時間は 1.02 秒です。 コンパイル済みモデルを使用すると、同じハードウェアでこれが最大 117 ミリ秒まで短縮されます。 モデルのサイズが大きくなっても、このような改善は 8 倍から 10 倍で比較的一定しています。

Compiled model performance improvement

Note

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

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

GitHub イシュー: #23611

EF Core 6.0 では、クエリのパフォーマンスが大幅に改善されています。 具体的には次のとおりです。

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

これらの向上の後、TechEmpower Fortunes ベンチマークでの一般的な "micro-ORM" Dapper と EF Core の差は、55% から約 5% 弱に縮まりました。

Note

EF Core 6.0 でのクエリ パフォーマンス向上の詳細については、.NET ブログの「Entity Framework Core 6.0 プレビュー 4 のお知らせ: パフォーマンス エディション」を参照してください。

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 ドキュメントの family ドキュメントには、家族の親、子、ペット、住所が埋め込まれます。 次に例を示します。

{
  "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
}

Note

重要なこととして、これらの所有型をさらに構成する必要がある場合は、OwnsOne/OwnsMany 構成を使用する必要があることに注意してください。

プリミティブ型のコレクション

GitHub イシュー: #14762

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

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

リストとディクショナリの両方を設定し、通常の方法でデータベースに挿入できます。

using var context = new BooksContext();

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

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

その結果、次の JSON ドキュメントが生成されます。

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

その後、これらのコレクションを、ここでも通常の方法で更新できます。

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

context.SaveChanges();

制限事項:

  • 文字列キーを持つディクショナリだけがサポートされます
  • プリミティブ コレクションの内容に対するクエリは、現在サポートされていません。 これらの機能が重要な場合は、#16926#25700#25701 に投票してください。

組み込み関数への変換

GitHub イシュー: #16143

Azure Cosmos DB プロバイダーは、より多くの基本クラス ライブラリ (BCL) メソッドを Azure Cosmos DB 組み込み関数に変換するようになりました。 次の表は、EF Core 6.0 での新しい変換を示したものです。

文字列の変換

BCL メソッド 組み込み関数 メモ
String.Length LENGTH
String.ToLower LOWER
String.TrimStart LTRIM
String.TrimEnd RTRIM
String.Trim TRIM
String.ToUpper UPPER
String.Substring SUBSTRING
+ 演算子 CONCAT
String.IndexOf INDEX_OF
String.Replace REPLACE
String.Equals STRINGEQUAL 大文字と小文字の区別がない呼び出しのみ

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) がこれらのイベントに含まれます。

Note

ここで示すログでは、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);
context.SaveChanges();

次の診断イベントがログに記録されます。

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

クエリを使用して Azure Cosmos DB データベースから項目を取得すると、CosmosEventId.ExecutingSqlQuery イベントが生成され、次に、読み取られる項目に対して 1 つ以上の CosmosEventId.ExecutedReadNext イベントが生成されます。 たとえば、次のコードを実行します。

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

次の診断イベントがログに記録されます。

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

パーティション キーを指定して Find を使用し、Azure Cosmos DB データベースから 1 つの項目を取得すると、CosmosEventId.ExecutingReadItemCosmosEventId.ExecutedReadItem イベントが生成されます。 たとえば、次のコードを実行します。

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

次の診断イベントがログに記録されます。

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

更新された項目を Azure Cosmos DB データベースに保存すると、CosmosEventId.ExecutedReplaceItem イベントが生成されます。 たとえば、次のコードを実行します。

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

次の診断イベントがログに記録されます。

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

項目を Azure Cosmos DB データベースから削除すると、CosmosEventId.ExecutedDeleteItem イベントが生成されます。 たとえば、次のコードを実行します。

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

次の診断イベントがログに記録されます。

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

スループットを構成する

GitHub イシュー: #17301

Azure Cosmos DB モデルに、手動または自動スケーリングのスループットを構成できるようになりました。 これらの値により、データベースのスループットがプロビジョニングされます。 次に例を示します。

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

さらに、個々のエンティティ型を構成して、対応するコンテナーのスループットをプロビジョニングできます。 次に例を示します。

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

Time to Live を構成する

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

Note

既存のアプリケーションに Azure Cosmos DB プロバイダーの機能強化を適用する詳細な例については、.NET ブログの「Taking the EF Core Azure Cosmos DB Provider for a Test Drive」を参照してください。

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

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 モデルとエンティティ型がスキャフォールディングされるようになりました。 NRT の使用法は、コードがスキャフォールディングされる C# プロジェクトで NRT のサポートが有効になっていると、自動的にスキャフォールディングされます。

たとえば、次の Tags テーブルには、null 許容と null 非許容の両方の文字列型の列が含まれています。

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

この結果、生成されたクラスには、対応する null 許容と null 非許容の文字列プロパティがあります。

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

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

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

同様に、次の Posts テーブルには、Blogs テーブルとの必要なリレーションシップが含まれています。

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

これにより、ブログ間の null 非許容 (必須) リレーションシップがスキャフォールディングされます。

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

そして 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; }
}

最後に、生成された 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 は次のようになりました。

  • グループに対して FirstOrDefault (または同様のもの) が後に続く GroupBy を変換します
  • グループからの上位 N 件の結果の選択をサポートします
  • GroupBy 演算子が適用された後にナビゲーションを展開します

次に示すのは、顧客レポートのクエリと、SQL Server での変換の例です。

例 1:

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

例 2:

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

例 3:

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

例 4:

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

例 5:

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

例 6:

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

例 7:

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

例 8:

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

例 9:

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

例 10:

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

例 11:

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

例 12:

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

例 13:

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

Model

これらの例で使用されているエンティティ型は次のとおりです。

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

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

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

複数の引数が指定された String.Concat を変換

GitHub イシュー: #23859。 この機能には @wmeints の貢献がありました。 どうもありがとう!

EF Core 6.0 より、複数の引数が指定された String.Concat への呼び出しが SQL に変換されるようになりました。 たとえば、以下のクエリ:

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

は、SQL Server を使用すると、次の SQL に変換されます。

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

System.Linq.Async との円滑な統合

GitHub イシュー: #24041

System.Linq.Async パッケージでは、クライアント側の非同期 LINQ 処理を追加します。 非同期 LINQ メソッドの名前空間の競合が原因で、このパッケージは以前のバージョンの EF Core では使いにくい状態でした。 EF Core 6.0 では、公開された EF Core DbSet<TEntity> でインターフェイスを直接実装する必要がないように、IAsyncEnumerable<T> に 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));

プロパティの型が string ではなく Name であっても、Contains または FreeText を使用してクエリを実行できるようになりました。 次に例を示します。

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

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

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

SQLite で ToString を変換する

GitHub イシュー: #17223。 この機能には @ralmsdeveloper の貢献がありました。 どうもありがとう!

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 = context.Users.Where(u => EF.Functions.Like(u.PhoneNumber.ToString(), "%555%")).ToList();

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

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

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

EF.Functions.Random

GitHub イシュー: #16141。 この機能には @RaymondHuy の貢献がありました。 どうもありがとう!

EF.Functions.Random がデータベース関数にマップされ、0 と 1 の間 (それぞれを除く) の擬似乱数が返されます。 変換は、SQL Server、SQLite、Azure Cosmos DB 用の EF Core リポジトリに実装されています。 たとえば、Popularity プロパティを持つ User エンティティ型について考えてみます。

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

Popularity は、1 から 5 までの値 (それぞれを含む) になります。 EF.Functions.Random を使用すると、ランダムに選択された評判を持つすべてのユーザーを返すクエリを作成できます。

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

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

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

IsNullOrWhitespace の SQL Server 変換の向上

GitHub イシュー: #22916。 この機能には @Marusyk の貢献がありました。 どうもありがとう!

次のクエリがあるとします。

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

EF Core 6.0 より前は、これは SQL Server で次のように変換されていました。

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

この変換は、EF Core 6.0 で次のように改善されています。

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

メモリ内プロバイダー用のクエリの定義

GitHub イシュー: #24600

新しいメソッドである ToInMemoryQuery を使用すると、メモリ内データベースに対して特定のエンティティ型の定義クエリを作成できます。 これは、メモリ内データベースにビューと同等のものを作成する場合に特に便利であり、それらのビューでキーなしエンティティ型が返される場合に特に役立ちます。 たとえば、英国に拠点を置く顧客の顧客データベースについて考えてみます。 各顧客の住所は次のとおりです。

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

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

ここで、このデータに対して、それぞれの郵便番号区域に存在する顧客の数を示すビューが必要だとします。 これを表すキーなしエンティティ型を作成できます。

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

さらに、その DbSet プロパティを他の最上位レベルのエンティティ型のセットとともに DbContext で定義します。

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

1 つのパラメーターが含まれる Substring を変換する

GitHub イシュー: #20173。 この機能には @stevendarby の貢献がありました。 どうもありがとう!

string.Substring の使用は、EF Core 6.0 で 1 つの引数を使用して変換されるようになりました。 次に例を示します。

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

これは、SQL Server を使用する場合、次の SQL に変換されます。

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

非ナビゲーション コレクションの分割クエリ

GitHub イシュー: #21234

単一の LINQ クエリを複数の SQL クエリに分割することが EF Core でサポートされています。 このサポートは、EF Core 6.0 で、非ナビゲーション コレクションがクエリ プロジェクションに含まれるケースを含むように拡張されています。

次に示すのは、1 つのクエリまたは複数のクエリへの SQL Server の変換を示すクエリの例です。

例 1:

LINQ クエリ:

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

単一の SQL クエリ:

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

複数の SQL クエリ:

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

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

例 2:

LINQ クエリ:

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

単一の SQL クエリ:

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

複数の SQL クエリ:

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

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

例 3:

LINQ クエリ:

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

単一の SQL クエリ:

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

複数の SQL クエリ:

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

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

コレクションに結合するときに最後の ORDER BY 句を削除する

GitHub イシュー: #19828

関連する 1 対多のエンティティを読み込むとき、特定のエンティティのすべての関連エンティティがグループ化されるように、EF Core によって ORDER BY 句が追加されています。 ただし、最後の ORDER BY 句は、EF で必要なグループ化を生成するために必要ではなく、しかもパフォーマンスに影響を与える可能性があります。 そのため、EF Core 6.0 でこの句は削除されます。

たとえば、次のクエリについて考えてみます。

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

SQL Server で EF Core 5.0 を使用すると、このクエリは次のように変換されます。

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

EF Core 6.0 を使用すると、そうではなく次のように変換されます。

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

ファイル名と行番号でクエリにタグを付ける

GitHub イシュー: #14176。 この機能には @michalczerwinski の貢献がありました。 どうもありがとう!

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

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

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

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

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

所有されるオプションの依存処理に対する変更

GitHub イシュー: #24558

オプションの依存エンティティが存在するかどうかを、それがテーブルをプリンシパル エンティティと共有している場合に知ることは難しくなります。 理由は、依存が存在するかどうかに関係なく、プリンシパルで必要とされるためにテーブルに依存の行があることです。 これを明確に処理する方法は、依存に少なくとも 1 つの必須プロパティがあることを確認することです。 必須のプロパティは null にできないため、そのプロパティの列の値が null である場合は、依存エンティティが存在しないことを意味します。

たとえば、所有されている Address を各顧客が持つ Customer クラスを考えてみます。

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

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

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

住所はオプションです。つまり、住所のない顧客を保存することは有効です。

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

ただし、顧客に住所がある場合は、その住所に少なくとも null 以外の郵便番号が含まれている必要があります。

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

Postcode プロパティを Required とマークすることで、これが確実になります。

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

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

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

次の結果が生成されます。

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

代わりに、住所のプロパティが不要な場合を考えてみます。

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

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

これで、住所のない顧客と、住所のすべてのプロパティが null である住所を持つ顧客の両方を保存できるようになりました。

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

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

ただし、データベースの列に直接クエリを実行することで確認できるため、これら 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 であるオプションの依存を保存するときに、警告が出されるようになりました。 次に例を示します。

warn: 9/27/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) The entity of type 'Address' with primary key values {CustomerId: -2147482646} is an optional dependent using table sharing.(プライマリ キー値 {CustomerId: -2147482646} を持つ 'Address' 型のエンティティは、テーブル共有を使用しているオプションの依存です。) The entity does not have any property with a non-default value to identify whether the entity exists.(このエンティティには、エンティティが存在するかどうかを識別するための、既定値以外のプロパティがありません。) This means that when it is queried no object instance will be created instead of an instance with all properties set to default values.(つまり、クエリが実行されたときに、すべてのプロパティが既定値に設定されたインスタンスではないオブジェクトのインスタンスが作成されることはありません。) Any nested dependents will also be lost.(入れ子になった依存も失われます。) Either don't save any instance with only default values or mark the incoming navigation as required in the model.(既定値のみを使用してインスタンスを保存しないようにするか、モデルで受信ナビゲーションを必須とマークします。)

これは、オプションの依存自体が、同じテーブルにマップされた別のオプションの依存のプリンシパルとして機能する場合、さらに難しくなります。 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 値の場合、アドレス自体にデータがあっても、リレーションシップがオプションであれば、Address のインスタンスは EF Core によって作成されません。 この種類のモデルの場合、EF Core 6.0 では次の例外がスローされます。

System.InvalidOperationException: Entity type 'ContactInfo' is an optional dependent using table sharing and containing other dependents without any required non shared property to identify whether the entity exists. (エンティティ型 "ContactInfo" は、テーブル共有を使用し、エンティティが存在するかどうかを識別するために必要な非共有プロパティを備えない他の依存を含む、オプションの依存です。) If all nullable properties contain a null value in database then an object instance won't be created in the query causing nested dependent's values to be lost. (データベースですべての Null 許容プロパティに null 値が含まれている場合、クエリでオブジェクト インスタンスが作成されないため、入れ子になった依存の値は失われてしまいます。) Add a required property to create instances with null values for other properties or mark the incoming navigation as required to always create an instance. (他のプロパティ用に 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 には、データベースへのマップ方法を変更するためにコードに適用できる新しい属性がいくつか含まれています。

UnicodeAttribute

GitHub イシュー: #19794。 この機能には @RaymondHuy の貢献がありました。 どうもありがとう!

EF Core 6.0 以降では、"データベースの型を直接指定せずに"、マッピング属性を使用して文字列プロパティを Unicode 以外の列にマップできるようになりました。 たとえば、国際標準図書番号 (ISBN) を表す "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]));

Note

既定では、文字列プロパティが EF Core によって Unicode 列にマップされます。 データベース システムが Unicode 型のみをサポートしている場合、UnicodeAttribute は無視されます。

PrecisionAttribute

GitHub イシュー: #17914。 この機能には @RaymondHuy の貢献がありました。 どうもありがとう!

"データベースの型を直接指定せずに"、マッピング属性を使用してデータベース列の有効桁数と小数点以下桁数を構成できるようになりました。 たとえば、10 進数の Price プロパティを持つ Product エンティティ型について考えてみます。

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

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

このプロパティは、EF Core で有効桁数 10、小数点以下桁数 2 のデータベース列にマップされます。 たとえば、SQL Server の場合は次のようになります。

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

EntityTypeConfigurationAttribute

GitHub イシュー: #23163。 この機能には @KaloyanIT の貢献がありました。 どうもありがとう!

IEntityTypeConfiguration<TEntity> インスタンスで、各エンティティ型の ModelBuilder 構成を独自の構成クラスに含めることができます。 次に例を示します。

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

通常、この構成クラスは、インスタンスを作成したうえで DbContext.OnModelCreating から呼び出す必要があります。 次に例を示します。

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

EF Core 6.0 以降では、エンティティ型に対して EntityTypeConfigurationAttribute を配置することで、EF Core で適切な構成を見つけて使用できます。 次に例を示します。

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

この属性は、Book エンティティ型がモデルに含まれている場合、指定した IEntityTypeConfiguration の実装が EF Core で使用されることを意味します。 エンティティ型は、通常のいずれかのメカニズムを使用してモデルに含められます。 たとえば、エンティティ型に対して DbSet<TEntity> を使用します。

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

    //...

または、OnModelCreating において登録します。

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

注意

EntityTypeConfigurationAttribute 型は、アセンブリ内で自動的に検出されることはありません。 エンティティ型の属性が検出される前に、エンティティ型をモデルに追加する必要があります。

モデル構築の機能強化

新しいマッピング属性に加えて、EF Core 6.0 には、モデルの構築プロセスに対する他のいくつかの機能強化が含まれています。

SQL Server スパース列のサポート

GitHub イシュー: #8023

SQL Server のスパース列は、null 値を格納するように最適化された通常の列です。 これは、TPH 継承マッピングを使用する場合に便利です。その場合、使用頻度の低いサブタイプのプロパティがテーブル内のほとんどの行で null 列値になります。 たとえば、ForumUser から拡張される ForumModerator クラスについて考えてみます。

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

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

数百万人のユーザーがいても、モデレーターはそのうちほんの一握りでしょう。 つまり、この場合は ForumName をスパースとしてマッピングすることに意味があります。 これを OnModelCreatingIsSparse を使用して構成できるようになりました。 次に例を示します。

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

Note

スパース列には制限があります。 SQL Server スパース列に関するドキュメントを必ずお読みになり、スパース列がご自身のシナリオに確実に適した選択肢であるようにしてください。

HasConversion API に対する機能強化

GitHub イシュー: #25468

EF Core 6.0 より前は、HasConversion メソッドのジェネリック オーバーロードでジェネリック パラメーターを使用して、"変換先の型" を指定していました。 たとえば、Currency 列挙型について考えます。

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

HasConversion<string> を使用してこの列挙型の値を文字列 "UsDollars"、"PoundsStirling"、"Euros" として保存するように EF Core を構成できます。 次に例を示します。

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

重要

以下で説明する問題により、EF Core 6.0 リリースでは、null 値の変換を許可する ValueConverter のコンストラクターは [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
    }

"Unknown" という種類の猫は、データベース内で Breed 列が null に設定されます。 次に例を示します。

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

context.SaveChanges();

これにより、SQL Server で次の insert ステートメントが生成されます。

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

DbContext ファクトリの機能強化

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"))
    .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 void DoSomething()
    {
        using var context1 = _contextFactory.CreateDbContext();
        using var context2 = _contextFactory.CreateDbContext();

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

または:

private class MyController1
{
    private readonly SomeDbContext _context;

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

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

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

DbContextFactory でパラメーターなしの DbContext コンストラクターが無視される

GitHub イシュー: #24124

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

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

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

DbContext プールを依存関係の挿入なしで使用できる

GitHub イシュー: #24137

PooledDbContextFactory 型は、DbContext インスタンス用のスタンドアロン プールとして使用できるようにパブリックに設定されています。アプリケーションに依存関係の挿入コンテナーを含める必要はありません。 このプールは、コンテキスト インスタンスの作成に使用される DbContextOptions のインスタンスを使用して作成されます。

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

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

その後、ファクトリをインスタンスの作成とプールに使用できます。 次に例を示します。

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

ColumnAttributeOrder プロパティを使用して、移行でテーブルを作成するときに列の順序を指定できるようになりました。 たとえば、次のモデルについて考えます。

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 を使用して列名と順序の両方を指定する方法も示しています。

列の順序付けは、OnModelCreatingModelBuilder 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 を使用することによって、異なるプロパティの属性で同じ順序番号が指定されている場合の競合の解決を含め、属性で行われる順序をオーバーライドすることができます。

重要

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

EF Core の最小限の API

GitHub イシュー: #25192

.NET Core 6.0 には、簡素化された "最小限の API" を特徴とする更新されたテンプレートが含まれています。これにより、.NET アプリケーションで従来必要とされた定型コードの大部分が不要になります。

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"));
var builder = WebApplication.CreateBuilder(args);

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

Note

EF Core の最小限の API でサポートされているのは、DbContext とプロバイダーのごく基本的な登録と構成のみです。 EF Core で使用可能なすべての種類の登録と構成にアクセスするには、AddDbContextAddDbContextPoolAddDbContextFactory などを使用します。

最小限の API の詳細については、次のリソースを参照してください。

SaveChangesAsync で同期コンテキストを保持

GitHub イシュー: #23971

5.0 リリースで EF Core コードを変更し、非同期コードの await を実行するすべての場所で、Task.ConfigureAwaitfalse に設定されるようにしました。 これは、通常、EF Core の使用に適した選択肢です。 ただし、EF Core では、非同期データベース操作が完了した後に、生成された値を追跡対象のエンティティに設定するため、SaveChangesAsync は特別なケースです。 これらの変更によって、たとえば、U.I. スレッドで実行する必要がある通知がトリガーされる可能性があります。 このため、SaveChangesAsync メソッドについてのみ、EF Core 6.0 でこの変更を元に戻します。

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

GitHub イシュー: #10613。 この機能には @fagnercarvalho の貢献がありました。 どうもありがとう!

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

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

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

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

Microsoft.EntityFrameworkCore.DbUpdateException: Required properties '{'Username'}' are missing for the instance of entity type 'User' with the key value '{Id: 1}'. (キー値 '{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>();
}

Blog がコンテキストによって追跡されるとすぐに、Id キー プロパティに一時的な値が取得されます。 たとえば、DbContext.Add を呼び出すとき、次のようになります。

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

一時的な値はコンテキスト変更トラッカーから取得できますが、エンティティ インスタンスには設定されません。 たとえば、次のコードを実行します。

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

次の出力が生成されます。

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

一時的な値が誤って一時的でないものとして扱われる可能性があるアプリケーション コードへの一時的な値の漏洩を防いでいるため、これは適切です。 ただし、一時的な値を直接処理すると便利な場合もあります。 たとえば、エンティティが追跡される前に、アプリケーションでそれらのグラフ用に独自の一時的な値を生成して、外部キーを用いたリレーションシップの形成に使用できるようにしたい場合があります。 これは、値を明示的に一時的とマークすることによって行うことができます。 次に例を示します。

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

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

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

EF Core 6.0 では、値は一時的とマークされるようになりましたが、エンティティ インスタンスに残ります。 たとえば、上記のコードで次の出力が生成されます。

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

同様に、EF Core によって生成される一時的な値をエンティティ インスタンスに対して明示的に設定し、一時的な値としてマークすることができます。 これを使用すると、一時的なキー値を使用して、新しいエンティティ間のリレーションシップを明示的に設定できます。 次に例を示します。

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

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

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

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

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

結果:

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

C# の null 許容参照型の注釈を付けられた EF Core

GitHub イシュー: #19007

EF Core コードベースで、C# の null 許容参照型 (NRT) が全体に使用されるようになりました。 これは、ご自身のコードから EF Core 6.0 を使用する場合に、null の使用法に合った適切なコンパイラが取得されることを意味します。

Microsoft.Data.Sqlite 6.0

ヒント

以下に示すすべてのサンプルは、GitHub からサンプル コードをダウンロードすることにより、実行してデバッグできます。

接続プール

GitHub イシュー: #13837

データベース接続を開いている時間は、可能な限り短くするのが一般的です。 これは、接続リソースの競合を防ぐのに役立ちます。 このような理由から、EF Core のようなライブラリでは、データベース操作を実行する直前に接続が開かれて、直後に閉じられます。 たとえば、次のような EF Core コードについて考えます。

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

var users = context.Users.ToList();

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

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

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

        context.SaveChanges();

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

接続のログを有効にすると、このコードからの出力は次のようになります。

Starting query...

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

Query finished.

Starting SaveChanges...

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

SaveChanges finished.

操作ごとに接続が開かれて、すぐに閉じられることに注意してください。

ただし、ほとんどのデータベース システムでは、データベースへの物理接続を開くのは負荷が高い操作です。 そのため、ほとんどの ADO.NET プロバイダーでは、物理接続のプールが作成され、必要に応じて DbConnection のインスタンスに貸し出されます。

SQLite の場合は、データベース アクセスは通常はファイルにアクセスするだけなので、少し異なります。 つまり、普通、SQLite データベースへの接続は極めて速く開かれます。 ただし、常にそうとは限りません。 たとえば、暗号化されたデータベースへの接続を開くときは、非常に遅くなる可能性があります。 そのため、Microsoft.Data.Sqlite 6.0 を使用すると、SQLite の接続がプールされるようになっています。

DateOnly と TimeOnly のサポート

GitHub イシュー: #24506

Microsoft.Data.Sqlite 6.0 で .NET 6 から新しい DateOnly および TimeOnly 型がサポートされます。 これらは、EF Core 6.0 で SQLite プロバイダーと共に使用することもできます。 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 = context.Users.Where(u => u.Birthday < new DateOnly(1900, 1, 1)).ToList();

SQLite で次のように変換されます。

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

誕生日が西暦 1900 年より前のユーザーのみが返されます。

Found 'ajcvickers'
Found 'wendy'

セーブポイント API

GitHub イシュー: #20228

これまで ADO.NET プロバイダーのセーブポイント用の共通 API を標準化してきました。 Microsoft.Data.Sqlite でこの API がサポートされるようになりました。これには以下が含まれます。

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

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

using var transaction = connection.BeginTransaction();

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

transaction.Save("MySavepoint");

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

transaction.Rollback("MySavepoint");

transaction.Commit();

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

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

GitHub イシュー: #22505。 この機能には @nmichels の貢献がありました。 どうもありがとう!

2 つの異なるタイムアウトが ADO.NET プロバイダーによってサポートされています。

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

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

ヒント

Sqlite で Default TimeoutCommand Timeout の同意語として扱われるため、必要に応じて代わりに使用できます。