次の方法で共有


EF Core 9 の新機能

EF Core 9 (EF9) は、EF Core 8 に続く次のリリースであり、2024 年 11 月にリリースされる予定です。

EF9 は、EF9 の最新機能と API の調整がすべて含まれるデイリー ビルドとして利用できます。 ここでのサンプルでは、これらのデイリー ビルドを使用します。

ヒント

サンプルは、GitHub からサンプル コードをダウンロードすることにより、実行してデバッグできます。 以下の各セクションは、そのセクションに固有のソース コードにリンクしています。

EF9 は .NET 8 をターゲットとしているため、.NET 8 (LTS) または .NET 9 で使用できます。

ヒント

"新機能" ドキュメントは、プレビューごとに更新されます。 すべてのサンプルは EF9 のデイリー ビルドを使用するように設定されており、通常、最新のプレビューと比較して作業が完了するまでにさらに数週間かかります。 新しい機能をテストするときは、古いビットに対してテストを行わないように、デイリー ビルドを使用することを強くお勧めします。

NoSQL 用 Azure Cosmos DB

EF 9.0 では、Azure Cosmos DB の EF Core プロバイダーが大幅に改善されています。プロバイダーの重要な部分が、新しい機能を提供して、新しい形式のクエリを可能にし、プロバイダーを Azure Cosmos DB のベスト プラクティスに合わせるために書き換えられています。 主な改善点の概要は以下のとおりです。詳しい一覧については、このエピックの問題をご覧ください

警告

プロバイダーに加えられる改善の一環として、影響の大きい破壊的変更を多数行う必要がありました。既存のアプリケーションをアップグレードする場合は、破壊的変更に関するセクションを注意深くお読みください。

パーティション キーとドキュメント ID を使用したクエリの機能強化

Azure Cosmos DB に保存されている各ドキュメントには、一意のリソース ID があります。 さらに、各ドキュメントには "パーティション キー" を含めることができます。これは、データベースを効果的にスケーリングできるようにデータの論理的なパーティション分割を決定します。 パーティション キーの選択の詳細については、「Azure Cosmos DB でのパーティション分割と水平スケーリング」を参照してください。

EF 9.0 では、Azure Cosmos DB プロバイダーは、LINQ クエリ内のパーティション キーの比較を識別し、それらを抽出して、クエリが関連するパーティションにのみ送信されるようにする機能が大幅に向上しました。これにより、クエリのパフォーマンスが大幅に向上し、RU 料金が削減されます。 次に例を示します。

var sessions = await context.Sessions
    .Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
    .ToListAsync();

このクエリでは、プロバイダーが PartitionKey の比較を自動的に認識します。ログを調べると、以下のようになります。

Executed ReadNext (189.8434 ms, 2.8 RU) ActivityId='8cd669ed-2ca5-4f2b-8923-338899071361', Container='test', Partition='["someValue"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE STARTSWITH(c["Username"], "x")

WHERE 句には PartitionKey が含まれていない点に注意してください。この比較は "リフト" されており、関連するパーティションに対してのみクエリを実行するために使用されます。 以前のバージョンでは、多くの状況で比較が WHERE 句に残されていたため、クエリがすべてのパーティションに対して実行され、コストが増加してパフォーマンスが低下していました。

さらに、クエリがドキュメントの ID プロパティの値も提供し、他のクエリ操作を含んでいない場合、プロバイダーは追加の最適化を適用できます。

var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
    .Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
    .SingleAsync();

ログには、このクエリの以下の内容が示されます。

Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'

ここでは、SQL クエリはまったく送信されません。 代わりに、プロバイダーは非常に効率的なポイント読み取り (ReadItem API) を実行し、パーティション キーと ID を指定してドキュメントを直接取得します。 これは、Azure Cosmos DB で実行できる最も効率的かつコスト効率の高い種類の読み取りです。ポイント読み取りについて詳しくは、 Azure Cosmos DB のドキュメント をご覧ください。

パーティション キーとポイント読み取りを使用したクエリについて詳しくは、クエリのドキュメント ページをご覧ください

階層パーティション キー

ヒント

ここで示すコードは、HierarchicalPartitionKeysSample.cs のものです。

Azure Cosmos DB は当初、単一のパーティション キーをサポートしていましたが、その後、 パーティション キーに最大 3 つのレベルの階層を指定したサブパーティション分割もサポートされるよう、パーティション分割機能が拡張されました。 EF Core 9 では、階層パーティション キーが完全にサポートされ、この機能に関連する優れたパフォーマンスとコスト削減を活用できるようになります。

パーティション キーは、モデル構築 API を使用して、通常は DbContext.OnModelCreating で指定します。 パーティション キーのレベルごとに、エンティティ型にマップされたプロパティが必要です。 たとえば、UserSession エンティティ型について考えてみます。

public class UserSession
{
    // Item ID
    public Guid Id { get; set; }

    // Partition Key
    public string TenantId { get; set; } = null!;
    public Guid UserId { get; set; }
    public int SessionId { get; set; }

    // Other members
    public string Username { get; set; } = null!;
}

次のコードでは、TenantIdUserIdSessionId の各プロパティを使用して、3 レベルのパーティション キーを指定します。

modelBuilder
    .Entity<UserSession>()
    .HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });

ヒント

このパーティション キーの定義は、Azure Cosmos DB のドキュメントの「階層パーティション キーを選択する」に示されている例に従っています。

EF Core 9 以降では、マップされた型のプロパティをパーティション キーでどのように使用できるかに注意してください。 bool および数値型 (int SessionId プロパティなど) の場合、値は直接パーティション キーで使用されます。 その他の型 (Guid UserId プロパティなど) は、自動的に文字列に変換されます。

クエリを実行すると、EF がクエリからパーティション キー値を自動的に抽出し、それをAzure Cosmos DB クエリ API に適用して、可能な限り少ないパーティション数にクエリが適切に制限されるようにします。 たとえば、階層内の 3 つのパーティション キー値すべてを提供する次の LINQ クエリを考えてみましょう。

var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");

var sessions = await context.Sessions
    .Where(
        e => e.TenantId == tenantId
             && e.UserId == userId
             && e.SessionId == sessionId
             && e.Username.Contains("a"))
    .ToListAsync();

このクエリを実行すると、EF Core では tenantIduserIdsessionId パラメーターの値を抽出し、Azure Cosmos DB クエリ API にパーティション キー値として渡します。 たとえば、上記のクエリを実行して得られるログを参照してください。

info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))

パーティション キーの比較が WHERE 句から削除され、代わりに、効率的な実行のためにパーティション キーとして ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0] が使用されている点に注意してください。

詳しくは、パーティション キーを使用したクエリに関するドキュメントをご覧ください。

LINQ クエリ機能の大幅な改善

EF 9.0 では、Azure Cosmos DB プロバイダーの LINQ 変換機能が大幅に拡張され、プロバイダーがより多くの種類のクエリを実行できるようになりました。 クエリの改善点の全一覧は長すぎるため記載できませんが、主なハイライトは次のとおりです。

  • EF のプリミティブ コレクションを完全にサポートし、int や文字列などのコレクションに対して LINQ クエリを実行できます。 詳しくは、「EF8 の新機能: プリミティブ コレクション」をご覧ください。
  • 非プリミティブ コレクションに対する任意のクエリのサポート。
  • コレクションへのインデックス作成、Length/CountElementAtContains など、多数の追加の LINQ 演算子がサポートされるようになりました。
  • CountSumなどの集約演算子のサポートが追加されました。
  • 追加の関数翻訳(サポートされている翻訳の完全なリストについては、 関数マッピングのドキュメント を参照してください):
    • DateTime および DateTimeOffset コンポーネント メンバー (DateTime.YearDateTimeOffset.Month...) の翻訳。
    • EF.Functions.IsDefined および EF.Functions.CoalesceUndefinedundefined 値を処理できるようになりました。
    • string.ContainsStartsWithEndsWithStringComparison.OrdinalIgnoreCase がサポートされるようになりました。

クエリの機能強化の詳しい一覧については、こちらの問題をご覧ください。

Azure Cosmos DB および JSON 標準に合わせたモデリングの改善

EF 9.0 は、JSON ベースのドキュメント データベースに対してより自然な方法で Azure Cosmos DB ドキュメントにマップし、ドキュメントにアクセスする他のシステムとの相互運用性を高めます。 これには破壊的変更が伴いますが、すべてのケースで 9.0 より前の動作に戻すことができる API が存在します。

ディスクリミネーターのない簡略化された id プロパティ

まず、EF の以前のバージョンでは、ディスクリミネーターの値が JSON id プロパティに挿入され、次のようなドキュメントが生成されていました。

{
    "id": "Blog|1099",
    ...
}

これは、異なる種類のドキュメント (ブログや投稿など) と同じキー値 (1099) が、同じコンテナー パーティション内に存在可能にするために行われました。 EF 9.0 以降では、id プロパティにはキー値のみ含まれています。

{
    "id": 1099,
    ...
}

これは JSON にマップするより自然な方法であり、外部ツールやシステムが EF によって生成された JSON ドキュメントと対話しやすくなります。このような外部システムは通常、既定で .NET 型から派生する EF ディスクリミネーター値を認識しません。

EF は既存のドキュメントに対して古い id 形式でクエリを実行できなくなるため、これは破壊的変更である点に注意してください。 以前の動作に戻すための API が導入されました。詳しくは、破壊的変更に関するメモドキュメントをご覧ください。

ディスクリミネーター プロパティの名前が $type に変更されました

既定のディスクリミネーター プロパティは、以前は Discriminator という名前でした。 EF 9.0 では、既定値が $type に変更されます。

{
    "id": 1099,
    "$type": "Blog",
    ...
}

これは JSON ポリモーフィズムの新しい標準に準拠しており、他のツールとの相互運用性が向上します。 たとえば、.NET の System.Text.Json もポリモーフィズムをサポートしており、$type を既定の識別子プロパティ名として使用しています (ドキュメント)。

これは破壊的変更である点に注意してください。EF は、古い識別子プロパティ名を使用して既存のドキュメントをクエリできなくなります。 以前の名前に戻す方法について詳しくは、破壊的変更に関するメモをご覧ください。

ベクトル類似性検索 (プレビュー)

Azure Cosmos DB では、ベクトル類似性検索のプレビュー サポートが提供されるようになりました。 ベクトル検索は、AI、セマンティック検索など、一部のアプリケーションの種類の基本的な部分です。 Azure Cosmos DB を使用すると、残りのデータと共にベクターをドキュメントに直接格納できます。つまり、1 つのデータベースに対してすべてのクエリを実行できます。 これにより、アーキテクチャが大幅に簡素化され、スタック内の追加の専用ベクター データベース ソリューションが不要になります。 Azure Cosmos DB ベクトル検索について詳しくは、 ドキュメントをご覧ください

Azure Cosmos DB コンテナーが適切にセットアップされたら、ベクトル プロパティを追加して構成すれば、EF 経由でベクトル検索を使用可能になります。

public class Blog
{
    ...

    public float[] Vector { get; set; }
}

public class BloggingContext
{
    ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .Property(b => b.Embeddings)
            .IsVector(DistanceFunction.Cosine, dimensions: 1536);
    }
}

完了したら、LINQ クエリで EF.Functions.VectorDistance() 関数を使用してベクトル類似性検索を実行します。

var blogs = await context.Blogs
    .OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
    .Take(5)
    .ToListAsync();

詳しくは、ベクトル検索に関するドキュメントをご覧ください。

ページ付けの対応

Azure Cosmos DB プロバイダーでは、 後続トークンを使用してクエリ結果をページ分割できるようになりました。これは、従来のように Skip および Take を使用するよりもはるかに効率的でコスト効率に優れています。

var firstPage = await context.Posts
    .OrderBy(p => p.Id)
    .ToPageAsync(pageSize: 10, continuationToken: null);

var continuationToken = firstPage.ContinuationToken;
foreach (var post in page.Values)
{
    // Display/send the posts to the user
}

新しい ToPageAsync 演算子は CosmosPage を返します。これは、後でクエリを効率的に再開して次の 10 項目を取得するために使用できる後続トークンを公開します。

var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);

詳しくは、ページネーションに関するドキュメントのセクションをご覧ください。

より安全な SQL クエリのための FromSql

Azure Cosmos DB プロバイダーでは、 FromSqlRaw経由で SQL クエリが許可されています。 ただし、ユーザー指定のデータが SQL に補間または連結されると、その API は SQL インジェクション攻撃の影響を受ける可能性があります。 EF 9.0 では、パラメーター化されたデータを常に SQL 外部のパラメーターとして統合する新しい FromSql メソッドを使用できるようになりました。

var maxAngle = 8;
_ = await context.Blogs
    .FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
    .ToListAsync();

詳しくは、ページネーションに関するドキュメントのセクションをご覧ください。

ロールベースのアクセス

Azure Cosmos DB for NoSQL には、組み込みのロールベースのアクセス制御 (RBAC) システムが含まれています。 これは、すべてのデータ プレーン操作で EF9 でサポートされるようになりました。 しかし、Azure Cosmos DB SDK では、Azure Cosmos DB での管理プレーン操作に対する RBAC はサポートされていません。 RBAC で EnsureCreatedAsync の代わりに Azure Management API を使用してください。

同期 I/O が既定でブロックされるようになりました

Azure Cosmos DB for NoSQL では、アプリケーション コードからの同期 (ブロッキング) API はサポートされていません。 以前は、EF は非同期呼び出しをブロックすることによりこれをマスクしていました。 ただし、これにより同期 I/O の使用が促されるため、適切ではないプラクティスであり、デッドロックを引き起こす可能性があります。 したがって、EF 9 以降では、同期アクセスが試行されると例外がスローされます。 次に例を示します。

同期 I/O は、警告レベルを適切に構成することにより、今のところ引き続き使用できます。 たとえば、DbContextOnConfiguring で次のように入力します。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));

ただし、EF 11 では同期サポートが完全に削除される予定のため、できるだけ早く ToListAsyncSaveChangesAsync などの非同期メソッドを使用するよう更新を開始してください。

AOT とプリコンパイル済みクエリ

警告

NativeAOT とクエリプリコンパイルは試験的な機能であり、運用環境での使用にはまだ適していません。 以下で説明するサポートは、最終的な機能に向けたインフラストラクチャと見なす必要があります。これは、EF 10 でリリースされる可能性があります。 現在のサポートを試し、エクスペリエンスを報告することをお勧めしますが、運用環境で EF NativeAOT アプリケーションをデプロイすることはお勧めします。

EF 9.0 では、.NET ネイティブAOT の初期の試験的なサポートが提供され、EF を利用してデータベースにアクセスする事前コンパイル済みアプリケーションを公開できます。 NativeAOT モードで LINQ クエリをサポートするために、EF は query プリコンパイルに依存します: このメカニズムは EF LINQ クエリを静的に識別し、C# interceptors を生成します。これには、各特定のクエリを実行するコードが含まれています。 これにより、アプリケーションの起動時間が大幅に短縮される可能性があります。LINQ クエリを SQL にコンパイルする処理が大量に発生し、アプリケーションが起動するたびに発生しなくなったのでです。 代わりに、各クエリのインターセプターには、そのクエリの最終版 SQL と、データベースの結果を .NET オブジェクトとして具体化するための最適化されたコードが含まれています。

たとえば、次の EF クエリを含むプログラムを指定します。

var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();

EF によってプロジェクトに C# インターセプターが生成され、クエリの実行が引き継がれるようになります。 プログラムが起動するたびにクエリを処理して SQL に変換する代わりに、インターセプターには SQL が直接埋め込まれています (この場合は SQL Server の場合)。これにより、プログラムの起動速度が大幅に向上します。

var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));

さらに、同じインターセプターには、データベースの結果から .NET オブジェクトを具体化するコードが含まれています。

var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);

これは、別の新しい .NET 機能 ( 安全なアクセサーを使用して、データベースからオブジェクトのプライベート フィールドにデータを挿入します。

NativeAOT に興味があり、最先端の機能を試す場合は、ぜひお試しください。 この機能は不安定と見なす必要があり、現在は多くの制限があることに注意してください。EF 10 では、安定させ、運用環境での使用に適した状態にすることが期待されています。

詳細については、 NativeAOT のドキュメント ページ を参照してください。

LINQ と SQL の翻訳

すべてのリリースと同様、EF9 には LINQ クエリ機能の大幅な機能強化が含まれています。 新しいクエリを変換できるようになり、サポートされているシナリオの多くの SQL 変換が改善されて、パフォーマンスと読みやすさが向上しました。

改善点の数が多すぎるため、ここですべてを列挙することはできません。 以下に、より重要な改善点のいくつかをハイライトします。9.0 で行われた作業のより詳細な一覧については、こちらの問題をご覧ください。

EF Core によって生成される SQL の最適化に対して、レベルの高い数多くの貢献をしてくださった Andrea Canciani (@ranma42) に感謝の意を表したいと思います。

複合型: GroupBy および ExecuteUpdate のサポート

GroupBy

ヒント

ここで示すコードは ComplexTypesSample.cs のものです。

EF9 は、複合型インスタンスによるグループ化をサポートします。 次に例を示します。

var groupedAddresses = await context.Stores
    .GroupBy(b => b.StoreAddress)
    .Select(g => new { g.Key, Count = g.Count() })
    .ToListAsync();

EF はこれを複合型の各メンバーによるグループ化に変換します。これは、値オブジェクトとして複合型のセマンティクスに適合します。 たとえば、Azure SQL で次のようにします。

SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode], COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]

ExecuteUpdate

ヒント

ここで示すコードは ExecuteUpdateSample.cs のものです。

同様に、EF9 では、ExecuteUpdate が複合型プロパティを受け入れるようにも改善されています。 ただし、複合型の各メンバーを明示的に指定する必要があります。 次に例を示します。

var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");

await context.Stores
    .Where(e => e.Region == "Germany")
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, newAddress));

これにより、複合型にマップされた各列を更新する SQL が生成されます。

UPDATE [s]
SET [s].[StoreAddress_City] = @__complex_type_newAddress_0_City,
    [s].[StoreAddress_Country] = @__complex_type_newAddress_0_Country,
    [s].[StoreAddress_Line1] = @__complex_type_newAddress_0_Line1,
    [s].[StoreAddress_Line2] = NULL,
    [s].[StoreAddress_PostCode] = @__complex_type_newAddress_0_PostCode
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

以前は、ExecuteUpdate 呼び出しにおいて複合型のさまざまなプロパティを手動で一覧表示する必要がありました。

不要な要素を SQL から取り除く

以前は、EF は実際には必要のない要素を含む SQL を生成することがありましたが、ほとんどの場合、これらは SQL 処理の前の段階で必要だった可能性があったからであり、そのまま残されていました。 EF9 では、このような要素のほとんどが削除されるため、SQL がよりコンパクトになり、場合によってはより効率が向上します。

テーブルの排除

最初の例として、EF によって生成された SQL には、クエリで実際には必要のないテーブルへの JOIN が含まれている場合があります。 table-per-type (TPT) 継承マッピングを使用する以下のモデルについて考えます。

public class Order
{
    public int Id { get; set; }
    ...

    public Customer Customer { get; set; }
}

public class DiscountedOrder : Order
{
    public double Discount { get; set; }
}

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

    public List<Order> Orders { get; set; }
}

public class BlogContext : DbContext
{
    ...

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

次に、少なくとも 1 つの注文を持つすべての顧客を取得するため、次のクエリを実行します。

var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync();

EF8 により、次の SQL が生成されました。

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id]
    WHERE [c].[Id] = [o].[CustomerId])

クエリには、列が参照されていないにもかかわらず、DiscountedOrders テーブルへの結合が含まれている点に注意してください。 EF9 により、結合なしで排除された SQL が生成されます。

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[CustomerId])

プロジェクションの排除

同様に、以下のクエリを調べてみましょう。

var orders = await context.Orders
    .Where(o => o.Amount > 10)
    .Take(5)
    .CountAsync();

EF8 では、このクエリにより次の SQL が生成されました。

SELECT COUNT(*)
FROM (
    SELECT TOP(@__p_0) [o].[Id]
    FROM [Orders] AS [o]
    WHERE [o].[Amount] > 10
) AS [t]

外側の SELECT 式は単に行をカウントするだけなので、サブクエリでは [o].[Id] プロジェクションは必要ない点に注意してください。 EF9 により、代わりに以下が生成されます。

SELECT COUNT(*)
FROM (
    SELECT TOP(@__p_0) 1 AS empty
    FROM [Orders] AS [o]
    WHERE [o].[Amount] > 10
) AS [s]

... プロジェクションが空です。 これは大したことではないように思えるかもしれませんが、場合によっては SQL を大幅に簡素化することができます。テストにおける SQL の変更の一部をスクロールして、効果を確認してください。

GREATEST/LEAST に関連する変換

ヒント

ここで示すコードは、LeastGreatestSample.cs のものです。

GREATESTLEAST の SQL 関数を使う新しい変換がいくつか導入されました。

重要

GREATESTLEAST の関数が 2022 バージョンの SQL Server/Azure SQL データベースに導入されました。 Visual Studio 2022 では、既定で SQL Server 2019 がインストールされます。 EF9 でこれらの新しい変換を試すには、SQL Server Developer Edition 2022 をインストールすることをお勧めします。

たとえば、Math.Max または Math.Min を使うクエリは、それぞれ GREATESTLEAST を使って Azure SQL 用に変換されるようになりました。 次に例を示します。

var walksUsingMin = await context.Walks
    .Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
    .ToListAsync();

EF9 を使い、SQL Server 2022 に対してこのクエリを実行すると、次の SQL に変換されます。

SELECT [w].[Id], [w].[ClosestPubId], [w].[DaysVisited], [w].[Name], [w].[Terrain]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
WHERE LEAST((
    SELECT COUNT(*)
    FROM OPENJSON([w].[DaysVisited]) AS [d]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Beers]) AS [b])) >

Math.MinMath.Max は、プリミティブ コレクションの値にも使用できます。 次に例を示します。

var pubsInlineMax = await context.Pubs
    .SelectMany(e => e.Counts)
    .Where(e => Math.Max(e, threshold) > top)
    .ToListAsync();

EF9 を使い、SQL Server 2022 に対してこのクエリを実行すると、次の SQL に変換されます。

SELECT [c].[value]
FROM [Pubs] AS [p]
CROSS APPLY OPENJSON([p].[Counts]) WITH ([value] int '$') AS [c]
WHERE GREATEST([c].[value], @__threshold_0) > @__top_1

最後に、RelationalDbFunctionsExtensions.LeastRelationalDbFunctionsExtensions.Greatest を使って、SQL で Least または Greatest 関数を直接呼び出すことができます。 次に例を示します。

var leastCount = await context.Pubs
    .Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
    .ToListAsync();

EF9 を使い、SQL Server 2022 に対してこのクエリを実行すると、次の SQL に変換されます。

SELECT LEAST((
    SELECT COUNT(*)
    FROM OPENJSON([p].[Counts]) AS [c]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[DaysVisited]) AS [d]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Beers]) AS [b]))
FROM [Pubs] AS [p]

クエリのパラメーター化を強制または禁止する

ヒント

ここで示すコードは、QuerySample.cs のものです。

一部の特殊なケースを除き、EF Core は LINQ クエリで使われる変数をパラメーター化しますが、生成された SQL には定数が含まれます。 たとえば、次のようなクエリ メソッドがあるとします。

async Task<List<Post>> GetPosts(int id)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && e.Id == id)
        .ToListAsync();

Azure SQL を使う場合、これは次の SQL とパラメーターに変換されます。

Executed DbCommand (1ms) [Parameters=[@__id_0='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = @__id_0

この値はクエリごとに変わらないため、EF によって SQL に ".NET Blog" の定数が作成されたことに注目してください。 定数を使うと、クエリ プランの作成時にデータベース エンジンによってこの値が検査されるようになるため、クエリの効率が向上する可能性があります。

一方、id のさまざまな値を使って同じクエリが実行される可能性があるため、id の値はパラメーター化されます。 このケースで定数を作成すると、id 値のみが異なる多数のクエリによって、クエリ キャッシュが汚染されることになります。 これはデータベース全体のパフォーマンスに非常に悪影響を及ぼします。

一般に、これらの既定値は変更しないでください。 ただし、EF Core 8.0.2 では、既定でパラメーターが使われる場合でも、EF に定数の使用を強制する EF.Constant メソッドが導入されました。 次に例を示します。

async Task<List<Post>> GetPostsForceConstant(int id)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
        .ToListAsync();

変換には、id 値の定数が含まれるようになりました。

Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = 1

EF.Parameter メソッド

EF9 では、その逆を行う EF.Parameter メソッドが導入されました。 つまり、コード内で値が定数であっても、EF にパラメーターの使用を強制します。 次に例を示します。

async Task<List<Post>> GetPostsForceParameter(int id)
    => await context.Posts
        .Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
        .ToListAsync();

変換には、".NET Blog" 文字列のパラメーターが含まれるようになりました。

Executed DbCommand (1ms) [Parameters=[@__p_0='.NET Blog' (Size = 4000), @__id_1='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1

パラメーター化されたプリミティブ コレクション

EF8 では、 プリミティブ コレクションを使用する一部のクエリの変換方法が変更されました。 LINQ クエリにパラメーター化されたプリミティブ コレクションが含まれている場合、EF はその内容を JSON に変換し、クエリを 1 つのパラメーター値として渡します。

async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
        .ToListAsync();

これにより、SQL Serverで次の翻訳が行われます。

Executed DbCommand (5ms) [Parameters=[@__ids_0='[1,2,3]' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (
    SELECT [i].[value]
    FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)

これにより、異なるパラメーター化されたコレクションに対して同じ SQL クエリを使用できますが (パラメーター値の変更のみ)、場合によっては、データベースがクエリを最適に計画できないため、パフォーマンスの問題につながる可能性があります。 この EF.Constant メソッドを使用して、前の翻訳に戻すことができます。

次のクエリでは、その目的で EF.Constant を使用しています。

async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
    => await context.Posts
        .Where(
            e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
        .ToListAsync();

結果の SQL は次のようになります。

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (1, 2, 3)

さらに、EF9 では、すべてのクエリのプリミティブ コレクションのパラメーター化を防ぐために使用できる TranslateParameterizedCollectionsToConstants コンテキスト オプション が導入されています。 また、プリミティブ コレクションのパラメーター化を明示的に強制する補完的な TranslateParameterizedCollectionsToParameters も追加しました (これがデフォルトの動作です)。

ヒント

EF.Parameter メソッドはコンテキスト オプションをオーバーライドします。 ほとんどのクエリ(すべてではない)に対してプリミティブ コレクションのパラメーター化を防止する場合は、コンテキスト オプション TranslateParameterizedCollectionsToConstants を設定し、パラメーター化するクエリまたは個々の変数に対して EF.Parameter を使用します。

インライン化された非相関サブクエリ

ヒント

ここで示すコードは、QuerySample.cs のものです。

EF8 では、別のクエリで参照される IQueryable が別のデータベース ラウンドトリップとして実行される場合があります。 たとえば、次のような LINQ クエリについて考えてみます。

var dotnetPosts = context
    .Posts
    .Where(p => p.Title.Contains(".NET"));

var results = dotnetPosts
    .Where(p => p.Id > 2)
    .Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
    .Skip(2).Take(10)
    .ToArray();

EF8 では、dotnetPosts のクエリが 1 つのラウンド トリップとして実行され、最終結果が 2 番目のクエリとして実行されます。 たとえば、SQL Server の場合は次のようになります。

SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY

EF9では、dotnetPosts 内の IQueryable がインライン化されるため、単一のデータベースがラウンド トリップします。

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata], (
    SELECT COUNT(*)
    FROM [Posts] AS [p0]
    WHERE [p0].[Title] LIKE N'%.NET%')
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

SQL Server 上のサブクエリと集計に対する集計関数

EF9 では、サブクエリまたはその他の集計関数で構成される集計関数を使用して、一部の複雑なクエリの変換が向上します。 そうしたクエリの例を次に示します。

var latestPostsAverageRatingByLanguage = await context.Blogs
    .Select(x => new
    {
        x.Language,
        LatestPostRating = x.Posts.OrderByDescending(xx => xx.PublishedOn).FirstOrDefault()!.Rating
    })
    .GroupBy(x => x.Language)
    .Select(x => x.Average(xx => xx.LatestPostRating))
    .ToListAsync();

まず、 Select は、SQL に変換するときにサブクエリを必要とする各 Post に対して LatestPostRating を計算します。 クエリの後半で、これらの結果は Average 操作を使用して集計されます。 SQL Server で実行すると、結果の SQL は次のようになります。

SELECT AVG([s].[Rating])
FROM [Blogs] AS [b]
OUTER APPLY (
    SELECT TOP(1) [p].[Rating]
    FROM [Posts] AS [p]
    WHERE [b].[Id] = [p].[BlogId]
    ORDER BY [p].[PublishedOn] DESC
) AS [s]
GROUP BY [b].[Language]

以前のバージョンでは、EF Core では、サブクエリに集計操作を直接適用しようとして、同様のクエリに対して無効な SQL が生成されました。 これは SQL Server では許可されず、例外が発生します。 別の集計に対して集計を使用するクエリにも、同じ原則が適用されます。

var topRatedPostsAverageRatingByLanguage = await context.Blogs.
Select(x => new
{
    x.Language,
    TopRating = x.Posts.Max(x => x.Rating)
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.TopRating))
.ToListAsync();

Note

この変更は、サブクエリ (またはその他の集計) を介した集計をサポートし、 LATERAL JOIN (APPLY) をサポートしない Sqlite には影響しません。 Sqlite で実行されている最初のクエリの SQL を次に示します。

SELECT ef_avg((
    SELECT "p"."Rating"
    FROM "Posts" AS "p"
    WHERE "b"."Id" = "p"."BlogId"
    ORDER BY "p"."PublishedOn" DESC
    LIMIT 1))
FROM "Blogs" AS "b"
GROUP BY "b"."Language"

Count != 0 を使用するクエリが最適化される

ヒント

ここで示すコードは、QuerySample.cs のものです。

EF8 では、次の LINQ クエリが SQL COUNT 関数を使用するように変換されました。

var blogsWithPost = await context.Blogs
    .Where(b => b.Posts.Count > 0)
    .ToListAsync();

EF9 では、EXISTS を使用して、より効率的な変換が生成されるようになりました。

SELECT "b"."Id", "b"."Name", "b"."SiteUri"
FROM "Blogs" AS "b"
WHERE EXISTS (
    SELECT 1
    FROM "Posts" AS "p"
    WHERE "b"."Id" = "p"."BlogId")

null が許容される値の比較演算の C# セマンティクス

EF8 では、一部のシナリオでは、null が許容される要素間の比較が正しく実行されませんでした。 C# では、オペランドの 1 つまたは両方が null の場合、比較演算の結果は false になります。それ以外の場合、オペランドに含まれる値が比較されます。 EF8 では、データベースの null セマンティクスを使用して比較が変換されていました。 これにより、LINQ to Objects を使用した同様のクエリとは異なる結果が生成されます。 さらに、フィルターとプロジェクションで比較を行うと、異なる結果が生成されます。 クエリによっては、Sql Server と Sqlite/Postgres の間で差分結果も生成されます。

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

var negatedNullableComparisonFilter = await context.Entities
    .Where(x => !(x.NullableIntOne > x.NullableIntTwo))
    .Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();

次の SQL が生成されます。

SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])

NullableIntOne または NullableIntTwo が null に設定されているエンティティがフィルターで除外されます。

EF9 では、以下が生成されます。

SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE CASE
    WHEN [e].[NullableIntOne] > [e].[NullableIntTwo] THEN CAST(0 AS bit)
    ELSE CAST(1 AS bit)
END = CAST(1 AS bit)

プロジェクションで実行される同様の比較:

var negatedNullableComparisonProjection = await context.Entities.Select(x => new
{
    x.NullableIntOne,
    x.NullableIntTwo,
    Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();

結果の SQL は次のようになります。

SELECT [e].[NullableIntOne], [e].[NullableIntTwo], CASE
    WHEN NOT ([e].[NullableIntOne] > [e].[NullableIntTwo]) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END AS [Operation]
FROM [Entities] AS [e]

NullableIntOne または NullableIntTwo が null に設定されているエンティティに対して false が返されます (C# で期待される true ではなく)。 生成された Sqlite で同じシナリオを実行します。

SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne" > "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"

NullableIntOne または NullableIntTwo が null の場合、変換によって null 値が生成されるため、Nullable object must have a value 例外が発生します。

EF9 では、これらのシナリオが適切に処理され、LINQ to Objects およびさまざまなプロバイダー間で一貫した結果が生成されます。

この機能強化は @ranma42 によって提供されました。 どうもありがとう!

Order および OrderDescending LINQ 演算子の翻訳

EF9 を使用すると、LINQ の簡略化された順序付け操作 (Order および OrderDescending) を変換できます。 これらは OrderBy/OrderByDescending と同様に機能しますが、引数は必要ありません。 代わりに、既定の順序付けが適用されます。エンティティの場合、これは主キー値に基づく順序付けと、値自体に基づく他の型の順序付けを意味します。

簡略化された順序演算子を利用するクエリの例を次に示します。

var orderOperation = await context.Blogs
    .Order()
    .Select(x => new
    {
        x.Name,
        OrderedPosts = x.Posts.OrderDescending().ToList(),
        OrderedTitles = x.Posts.Select(xx => xx.Title).Order().ToList()
    })
    .ToListAsync();

このクエリは、次の数式と同じです。

var orderByEquivalent = await context.Blogs
    .OrderBy(x => x.Id)
    .Select(x => new
    {
        x.Name,
        OrderedPosts = x.Posts.OrderByDescending(xx => xx.Id).ToList(),
        OrderedTitles = x.Posts.Select(xx => xx.Title).OrderBy(xx => xx).ToList()
    })
    .ToListAsync();

そして、次の SQL を生成します。

SELECT [b].[Name], [b].[Id], [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata], [p0].[Title], [p0].[Id]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Posts] AS [p0] ON [b].[Id] = [p0].[BlogId]
ORDER BY [b].[Id], [p].[Id] DESC, [p0].[Title]

Note

Order および OrderDescending メソッドは、エンティティ、複合型、またはスカラーのコレクションでのみサポートされます。複数のプロパティを含む匿名型のコレクションなど、より複雑なプロジェクションでは機能しません。

この機能強化は、EF チームの卒業生 @bricelamによって提供されました。 どうもありがとう!

論理否定演算子 (!) の変換の改善

EF9 には、SQL CASE/WHENCOALESCE、否定、および他のさまざまな構造に関する多くの最適化が導入されています。これらのほとんどは Andrea Canciani (@ranma42) によって提供されたものです。これらすべてに感謝します。 以下では、論理否定に関する最適化のいくつかについて詳しく説明します。

以下のクエリを調べてみましょう。

var negatedContainsSimplification = await context.Posts
    .Where(p => !p.Content.Contains("Announcing"))
    .Select(p => new { p.Content }).ToListAsync();

EF8 では、次の SQL を生成します。

SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)

EF9 では、NOT 操作を比較に "プッシュ" します。

SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0

SQL Server に適用可能なもう 1 つの例は、否定条件演算です。

var caseSimplification = await context.Blogs
    .Select(b => !(b.Id > 5 ? false : true))
    .ToListAsync();

EF8 では、ネストされた CASE ブロックが生成されていました。

SELECT CASE
    WHEN CASE
        WHEN [b].[Id] > 5 THEN CAST(0 AS bit)
        ELSE CAST(1 AS bit)
    END = CAST(0 AS bit) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]

EF9 では、入れ子を削除しました。

SELECT CASE
    WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]

SQL Server で、否定された bool プロパティをプロジェクションした場合:

var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();

EF8 では、比較が SQL Server クエリのプロジェクションに直接表示できないため、CASE ブロックが生成されます。

SELECT [p].[Title], CASE
   WHEN [p].[Archived] = CAST(0 AS bit) THEN CAST(1 AS bit)
   ELSE CAST(0 AS bit)
END AS [Active]
FROM [Posts] AS [p]

EF9 では、この変換が簡略化され、ビット単位の NOT (~) が使用されるようになりました。

SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]

Azure SQL と Azure Synapse のサポートの強化

EF9 を使用すると、対象となる SQL Server の種類を指定する際の柔軟性が向上します。 UseSqlServer を使用して EF を構成する代わりに、 UseAzureSql または UseAzureSynapse を指定できるようになりました。 これにより、Azure SQL または Azure Synapse を使用する場合、EF はより優れた SQL を生成できます。 EF は、データベース固有の機能 (例: Azure SQL 上の JSON 専用型) を利用したり、その制限 (例: Azure Synapse で LIKE を使用する場合、 ESCAPE 句は使用できません) を回避したりできます。

クエリの他の改善点

  • EF8 に導入されたプリミティブ コレクションのクエリ サポートは、すべての ICollection<T> 型をサポートするように拡張されました。 これはパラメーター コレクションとインライン コレクションにのみ適用される点に注意してください。エンティティの一部であるプリミティブ コレクションは、依然として配列、リスト、EF9 では読み取り専用の配列/リストに制限されます。
  • クエリの結果を HashSet として返す新しい ToHashSetAsync 関数 (#30033@wertzui による提供)。
  • TimeOnly.FromDateTimeFromTimeSpan が SQL Server で変換されるようになりました (#33678)。
  • 列挙型を介した ToString が変換されるようになりました (#33706@Danevandy99 による提供)。
  • string.Join は、SQL Server の非集計コンテキストで CONCAT_WS に変換されるようになりました (#28899)。
  • EF.Functions.PatIndex は、パターンの最初の出現の開始位置を返す SQL Server PATINDEX 関数に変換されるようになりました (#33702@smnsht)。
  • SumAverage が SQLite の小数点に対して機能するようになりました (#33721@ranma42 による提供)。
  • string.StartsWith および EndsWith の修正と最適化 (#31482)。
  • Convert.To* メソッドは、object 型の引数を受け入れることができるようになりました (#33891@imangd による提供)。
  • 排他または (XOR) 操作が SQL Server で変換されるようになりました (#34071、@ranma42 提供)。
  • COLLATE および AT TIME ZONE 演算の null 可能性に関する最適化 (#34263@ranma42による提供)。
  • INEXISTS およびセット演算に対する DISTINCT の最適化 (#34381@ranma42による提供)。

上記は、EF9 でのクエリの重要な改善点の一部にすぎません。より詳細な一覧については、こちらの問題をご覧ください。

移行

同時移行に対する保護

EF9 では、複数の移行実行が同時に実行されないように保護するロック メカニズムが導入されています。これにより、データベースが破損した状態になる可能性があります。 これは、推奨される方法を使用して 移行が運用環境にデプロイされている場合には発生しませんが、この方法を使用して実行時に移行が適用された場合に DbContext.Database.Migrate() 発生する可能性があります。 アプリケーションの起動時ではなく、デプロイ時に移行を適用することをお勧めしますが、その結果、アプリケーション アーキテクチャが複雑になる可能性があります (例: .NET Aspire プロジェクトを使用する場合)。

Note

Sqlite データベースを使用している場合は、 この機能に関連する潜在的な問題を確認してください。

トランザクション内で複数の移行操作を実行できない場合に警告する

移行中に実行される操作の大部分は、トランザクションによって保護されます。 これにより、何らかの理由で移行が失敗した場合、データベースは破損した状態になりません。 ただし、一部の操作はトランザクションにラップされません ( たとえば、SQL Server のメモリ最適化テーブルに対する操作や、データベースの照合順序の変更などのデータベース変更操作)。 移行エラーが発生した場合にデータベースが破損しないように、これらの操作は別の移行を使用して分離して実行することをお勧めします。 EF9 は、移行に複数の操作が含まれ、そのうちの 1 つがトランザクションにラップできないシナリオを検出し、警告を発行するようになりました。

データ シード処理の改善

EF9 では、データ シード処理を実行する便利な方法が導入されました。この方法では、データベースに初期データが設定されています。 DbContextOptionsBuilder には、DbContext が初期化されるときに実行される UseSeeding メソッドと UseAsyncSeeding メソッドが含まれるようになりました (EnsureCreatedAsyncの一部として)。

Note

アプリケーションが以前に実行されていた場合、データベースにサンプル データが既に含まれている可能性があります (これは、コンテキストの最初の初期化時に追加されます)。 そのため、 UseSeeding UseAsyncSeeding はデータベースの設定を試みる前に、データが存在するかどうかを確認する必要があります。 これは、単純な EF クエリを発行することで実現できます。

これらのメソッドの使用方法の例を次に示します。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
        .UseSeeding((context, _) =>
        {
            var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                context.SaveChanges();
            }
        })
        .UseAsyncSeeding(async (context, _, cancellationToken) =>
        {
            var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                await context.SaveChangesAsync(cancellationToken);
            }
        });

詳細についてはここ を参照してください。

その他の移行の機能強化

  • 既存のテーブルを SQL Server テンポラル テーブルに変更すると、移行コードのサイズが大幅に削減されました。

モデル構築

自動コンパイル済みモデル

ヒント

ここで示すコードは NewInEFCore9.CompiledModels サンプルのものです。

コンパイル済みモデルを使用すると、大規模なモデル (数百または数千のエンティティ型の数) を持つアプリケーションの起動時間を短縮できます。 これまでのバージョンの EF Core では、コンパイル済みモデルはコマンド ラインを使用して手動で生成する必要がありました。 次に例を示します。

dotnet ef dbcontext optimize

コマンドの実行後、.UseModel(MyCompiledModels.BlogsContextModel.Instance) のような行を OnConfiguring に追加して、コンパイル済みモデルを使用するように EF Core に指示する必要があります。

EF9 以降では、アプリケーションの DbContext 型がコンパイル済みモデルと同じプロジェクト/アセンブリにあるときは、この .UseModel 行が不要になります。 代わりに、コンパイル済みモデルが自動的に検出されて使用されます。 これは、モデルをビルドするときにいつでも EF ログを持つことで確認できます。 これで、単純なアプリケーションを実行すると、アプリケーションの起動時に EF がモデルをビルドするようすが示されます。

Starting application...
>> EF is building the model...
Model loaded with 2 entity types.

モデル プロジェクトでの dotnet ef dbcontext optimize の実行の出力は次のとおりです。

PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> dotnet ef dbcontext optimize

Build succeeded in 0.3s

Build succeeded in 0.3s
Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered automatically, but you can also call 'options.UseModel(BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> 

ログ出力に、"コマンドの実行時にモデルがビルドされている" ことが示されています。 ここで、リビルド後にコードを変更せずにアプリケーションを再度実行すると、出力は次のようになります。

Starting application...
Model loaded with 2 entity types.

コンパイル済みモデルが自動的に検出されて使用されたため、アプリケーションの起動時にモデルがビルドされませんでした。

MSBuild の統合

上記の方法では、エンティティ型または DbContext 構成が変更されたときには、やはりコンパイル済みモデルを手動で再生成する必要があります。 ただし、EF9 には、モデル プロジェクトのビルド時にコンパイル済みモデルを自動的に更新できる MSBuild タスク パッケージが付属しています。 作業を開始するには、Microsoft.EntityFrameworkCore.Tasks NuGet パッケージをインストールします。 次に例を示します。

dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0

ヒント

上記のコマンドには、使用している EF Core のバージョンと一致するパッケージ バージョンを使用してください。

次に、.csproj ファイルでEFOptimizeContextプロパティとEFScaffoldModelStageプロパティを設定して、統合を有効にします。 次に例を示します。

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
    <EFScaffoldModelStage>build</EFScaffoldModelStage>
</PropertyGroup>

これで、プロジェクトをビルドすると、コンパイル済みモデルがビルド中であることを示すログがビルド時に表示されます。

Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
  --additionalprobingpath G:\packages 
  --additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages" 
  --runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\ 
  --namespace NewInEfCore9 
  --suffix .g 
  --assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll
  --project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model 
  --root-namespace NewInEfCore9 
  --language C# 
  --nullable 
  --working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App 
  --verbose 
  --no-color 
  --prefix-output 

そしてアプリケーションを実行すると、コンパイル済みのモデルが検出されたため、モデルが再びビルドされないことがわかります。

Starting application...
Model loaded with 2 entity types.

これで、モデルが変更されるたびに、プロジェクトがビルドされるとすぐにコンパイル済みモデルが自動的にリビルドされます。

詳細については、 MSBuild の統合を参照してください。

読み取り専用プリミティブ コレクション

ヒント

ここで示すコードは PrimitiveCollectionsSample.cs から引用したものです。

EF8 で、マッピング配列と、プリミティブ型の変更可能なリストのサポートが導入されました。 EF9 ではこれが、読み取り専用コレクション/リストを含むように拡張されました。 具体的には EF9 は、IReadOnlyListIReadOnlyCollection、または ReadOnlyCollection として型指定されたコレクションをサポートします。 たとえば、次のコードでは、DaysVisited が規則によって日付のプリミティブ コレクションとしてマップされます。

public class DogWalk
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}

読み取り専用コレクションは、必要に応じて通常の変更可能なコレクションでサポートできます。 たとえば、次のコードでは、DaysVisited を日付のプリミティブ コレクションにマップできますが、クラス内のコードは引き続き基盤のリストを操作できます。

    public class Pub
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public IReadOnlyCollection<string> Beers { get; set; }

        private List<DateOnly> _daysVisited = new();
        public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
    }

次に、これらのコレクションを、通常の方法でクエリに使用できます。 たとえば、次の LINQ クエリ:

var walksWithADrink = await context.Walks.Select(
    w => new
    {
        WalkName = w.Name,
        PubName = w.ClosestPub.Name,
        Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
        TotalCount = w.DaysVisited.Count
    }).ToListAsync();

これは、SQLite では次の SQL に変換されます。

SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (
    SELECT COUNT(*)
    FROM json_each("w"."DaysVisited") AS "d"
    WHERE "d"."value" IN (
        SELECT "d0"."value"
        FROM json_each("p"."DaysVisited") AS "d0"
    )) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"

キーとインデックスの fill-factor を指定する

ヒント

ここで示すコードは、ModelBuildingSample.cs のものです。

EF9 では、EF Core Migrations を使用してキーとインデックスを作成する場合の SQL Server fill-factor の仕様がサポートされています。 SQL Server のドキュメントによると、"インデックスが作成または再構築されるとき、fill-factor 値によって、データで埋められる各リーフ レベル ページ上の領域の割合が決まり、各ページの残りは将来の拡張に備えて空き領域として予約されます。"

fill-factor は、単一または複合の主キーと代替キーおよびインデックスに設定できます。 次に例を示します。

modelBuilder.Entity<User>()
    .HasKey(e => e.Id)
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasAlternateKey(e => new { e.Region, e.Ssn })
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasIndex(e => new { e.Name })
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasIndex(e => new { e.Region, e.Tag })
    .HasFillFactor(80);

既存のテーブルに適用すると、テーブルが制約の fill-factor に変更されます。

ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];
ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];

ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80);

この機能強化は、@deano-hunter によって提供されました。 どうもありがとう!

既存のモデル構築規則の拡張性が向上しました

ヒント

ここで示すコードは CustomConventionsSample.cs のものです。

アプリケーションのパブリック モデル構築規則は EF7 で導入されました。 EF9 では、既存の規則の一部を簡単に拡張できるようになりました。 たとえば、EF7 の属性によってプロパティをマップするコードを次に示します。

public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
    public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
        : base(dependencies)
    {
    }

    public override void ProcessEntityTypeAdded(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionContext<IConventionEntityTypeBuilder> context)
        => Process(entityTypeBuilder);

    public override void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        if ((newBaseType == null
             || oldBaseType != null)
            && entityTypeBuilder.Metadata.BaseType == newBaseType)
        {
            Process(entityTypeBuilder);
        }
    }

    private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
    {
        foreach (var memberInfo in GetRuntimeMembers())
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                entityTypeBuilder.Property(memberInfo);
            }
            else if (memberInfo is PropertyInfo propertyInfo
                     && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
            {
                entityTypeBuilder.Ignore(propertyInfo.Name);
            }
        }

        IEnumerable<MemberInfo> GetRuntimeMembers()
        {
            var clrType = entityTypeBuilder.Metadata.ClrType;

            foreach (var property in clrType.GetRuntimeProperties()
                         .Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
            {
                yield return property;
            }

            foreach (var property in clrType.GetRuntimeFields())
            {
                yield return property;
            }
        }
    }
}

EF9 では、これは次のように単純化できます。

public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
    : PropertyDiscoveryConvention(dependencies)
{
    protected override bool IsCandidatePrimitiveProperty(
        MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping)
    {
        if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping))
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                return true;
            }

            structuralType.Builder.Ignore(memberInfo.Name);
        }

        mapping = null;
        return false;
    }
}

非パブリック コンストラクターを呼び出すように ApplyConfigurationsFromAssembly を更新します

以前のバージョンの EF Core で、ApplyConfigurationsFromAssembly メソッドは、パラメーターなしのパブリック コンストラクターを使った構成の種類のインスタンスのみを作成できました。 EF9 では、これが失敗したときに生成されるエラー メッセージを改善しました。また、非パブリック コンストラクターによるインスタンス化も可能になりました。 これは、アプリケーション コードによってインスタンスを作成してはいけない入れ子になったプライベート クラスに、構成を併置する場合に便利です。 次に例を示します。

public class Country
{
    public int Code { get; set; }
    public required string Name { get; set; }

    private class FooConfiguration : IEntityTypeConfiguration<Country>
    {
        private FooConfiguration()
        {
        }

        public void Configure(EntityTypeBuilder<Country> builder)
        {
            builder.HasKey(e => e.Code);
        }
    }
}

余談ですが、エンティティ型が構成に関連付けられるため、このパターンを嫌う方もいます。 一方、構成がエンティティ型と併置されるので、とても便利だと考える方もいます。 ここでは議論しないでおきましょう。 :-)

SQL Server HierarchyId

ヒント

ここで示すコードは、HierarchyIdSample.cs のものです。

HierarchyId パス生成の便利なメソッド

SQL Server HierarchyId 型のファースト クラス サポートが EF8 で追加されました。 EF9 では、ツリー構造で新しい子ノードを簡単に作成できるようにする便利なメソッドが追加されています。 たとえば、次のコードは、HierarchyId プロパティを持つ既存のエンティティに対してクエリを実行します。

var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");

この HierarchyId プロパティを使用すると、明示的な文字列操作なしで子ノードを作成できます。 次に例を示します。

var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");

daisyHierarchyId/4/1/3/1/ の場合、child1HierarchyId "/4/1/3/1/1/" を取得し、child2HierarchyId "/4/1/3/1/2/" を取得します。

これら 2 つの子の間にノードを作成するには、追加のサブレベルを使用できます。 次に例を示します。

var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");

これにより、 HierarchyId/4/1/3/1/1.5/であるノードが作成され、 child1child2の間に配置されます。

この機能強化は、@Rezakazemi890 によって提供されました。 どうもありがとう!

ツール

リビルドの削減

dotnet ef コマンド ライン ツールは、既定で、ツールを実行する前にプロジェクトをビルドします。 これは、ツールを実行する前にリビルドしないと、動作しない場合に混乱の原因となることがよくあるためです。 経験豊富な開発者は、--no-build オプションを使用して、遅くなる可能性があるこのビルドを回避できます。 ただし、--no-build オプションを使用した場合でも、次回 EF ツールの外部でビルドされるときにプロジェクトがリビルドされる可能性があります。

これは、@Suchiman によるコミュニティ投稿 によって修正されていると考えています。 ただし、MSBuild の動作を調整すると意図しない結果が生じる傾向があることも認識しているため、皆さんのような方々に、これを試していただき、ネガティブなエクスペリエンスがあれば報告していただくようお願いしています。