Share via


EF Core 9 の新機能

EF Core 9 (EF9) は、EF Core 8 に続く次のリリースであり、2024 年 11 月にリリースされる予定です。 詳細については、「Entity Framework Core 9 に対する計画」を参照してください。

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

ヒント

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

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

ヒント

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

NoSQL 用 Azure Cosmos DB

Azure Cosmos DB for NoSQL の EF Core データベース プロバイダーに対する EF9 の重要な更新に取り組んでいます。

ロールベースのアクセス

Azure Cosmos DB for NoSQL には、組み込みのロールベースのアクセス制御 (RBAC) システムが含まれています。 これが、コンテナーの管理と使用の両方について EF9 でサポートされるようになりました。 アプリケーション コードを変更する必要はありません。 詳細については、issue #32197 を参照してください。

同期アクセスが既定でブロックされる

ヒント

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

Azure Cosmos DB for NoSQL は、アプリケーション コードからの同期 (ブロッキング) アクセスをサポートしません。 以前は、EF は非同期呼び出しでブロッキングすることで、これを既定でマスクしました。 しかし、これはどちらも同期の使用を奨励します。これは不適切な方法であり、デッドロックを引き起こす可能性があります。 そのため、EF9 以降では、同期アクセスが試行されると例外がスローされます。 次に例を示します。

System.InvalidOperationException: An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.SyncNotSupported':
 Azure Cosmos DB does not support synchronous I/O. Make sure to use and correctly await only async methods when using
 Entity Framework Core to access Azure Cosmos DB. See https://aka.ms/ef-cosmos-nosync for more information.
 This exception can be suppressed or logged by passing event ID 'CosmosEventId.SyncNotSupported' to the 'ConfigureWarnings'
 method in 'DbContext.OnConfiguring' or 'AddDbContext'.
   at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, Exception exception)
   at Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal.CosmosLoggerExtensions.SyncNotSupported(IDiagnosticsLogger`1 diagnostics)
   at Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal.CosmosClientWrapper.DeleteDatabase()
   at Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal.CosmosDatabaseCreator.EnsureDeleted()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureDeleted()

例外が示すように、現時点では、警告レベルを適切に構成することで、同期アクセスを引き続き使用できます。 たとえば、DbContextOnConfiguring で次のように入力します。

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

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

拡張プリミティブ コレクション

ヒント

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

Cosmos DB プロバイダーは、EF Core 6 以降、限られた形式でプリミティブ コレクションをサポートしてきました。 このサポートは EF9 で強化されます。まずドキュメント データベース内のプリミティブ コレクションのメタデータと API サーフェスを統合し、リレーショナル データベースのプリミティブ コレクションに合わせて調整します。 つまり、プリミティブ コレクションをモデルのビルド API を使用して明示的にマップできるようになり、要素型のファセットを構成できるようになります。 たとえば、必須の (null 以外の) 文字列の一覧をマップするには、次のようにします。

modelBuilder.Entity<Book>()
    .PrimitiveCollection(e => e.Quotes)
    .ElementType(b => b.IsRequired());

モデルのビルド API の詳細については、EF8 の新機能: プリミティブ コレクションに関する記事を参照してください。

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

導入部で説明したように、EF Core を Just-In-Time (JIT) コンパイルなしで実行できるようにするために、バックグラウンドで多くの作業が行われています。 代わりに、EF は、アプリケーションでクエリを実行するために必要なすべてのものを事前コンパイル (AOT) します。 この AOT コンパイルと関連処理は、アプリケーションのビルドと発行の一環として行われます。 EF9 リリースのこの時点では、アプリ開発者が使用できる入手可能な情報は多くありません。 ただし、関心のある方のために、AOT とプリコンパイル済みクエリをサポートする EF9 の全 issue を次に示します。

経験談が集まっているため、事前コンパイル済みクエリを使用する方法の例については、こちらに戻ってご覧ください。

LINQ と SQL の翻訳

チームは、JSON マッピングとドキュメント データベースに対する継続的な改善の一環として、EF Core 9 のクエリ パイプラインに対するいくつかの重要なアーキテクチャ変更に取り組んでいます。 これは、皆さんのような方々にこれらの新しい内部構造でコードを実行してもらう必要があることを意味します。 (リリースのこの時点で "新機能" ドキュメントを読んでいるということは、皆さんはコミュニティに熱心に参加しているということです。ありがとうございます)。120,000 件を超えるテストを行っていますが、まだ十分ではありません。 問題を見つけて信頼できるリリースを提供するには、この製品上で実際のコードを実行していただける皆様の力が必要です。

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]

OPENJSON の WITH 句に渡される列を取り除く

ヒント

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

EF9 は、OPENJSON WITH を呼び出すときに不要な列を削除します。 たとえば、述語を使って JSON コレクションからカウントを取得するクエリを考えてみましょう。

var postsUpdatedOn = await context.Posts
    .Where(p => p.Metadata!.Updates.Count(e => e.UpdatedOn >= date) == 1)
    .ToListAsync();

Azure SQL データベース プロバイダーを使っている場合、EF8 では、このクエリによって次の SQL が生成されます。

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 (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
        [PostedFrom] nvarchar(45) '$.PostedFrom',
        [UpdatedBy] nvarchar(max) '$.UpdatedBy',
        [UpdatedOn] date '$.UpdatedOn',
        [Commits] nvarchar(max) '$.Commits' AS JSON
    ) AS [u]
    WHERE [u].[UpdatedOn] >= @__date_0) = 1

このクエリでは UpdatedByCommits が必要ないことに注目してください。 EF9 以降、これらの列は取り除かれるようになりました。

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 (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
        [PostedFrom] nvarchar(45) '$.PostedFrom',
        [UpdatedOn] date '$.UpdatedOn'
    ) AS [u]
    WHERE [u].[UpdatedOn] >= @__date_0) = 1

一部のシナリオでは、WITH 句が完全に削除されます。 次に例を示します。

var tagsWithCount = await context.Tags.Where(p => p.Text.Length == 1).ToListAsync();

EF8 では、このクエリは次の SQL に変換されます。

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) WITH ([value] nvarchar(max) '$') AS [t0]) = 1

EF9 では、これは次のように改善されました。

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) AS [t0]) = 1

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 とパラメーターに変換されます。

info: 2/5/2024 15:43:13.789 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      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 の値はパラメーター化されます。 この場合、定数を作成すると、パラメーター値のみが異なる多数のクエリでクエリ キャッシュが汚染されます。 これはデータベース全体のパフォーマンスに非常に悪影響を及ぼします。

一般に、これらの既定値は変更しないでください。 ただし、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 値の定数が含まれるようになりました。

info: 2/5/2024 15:43:13.812 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      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

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" 文字列のパラメーターが含まれるようになりました。

info: 2/5/2024 15:43:13.803 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      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

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

ヒント

ここで示すコードは、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 では、dotnetPostsIQueryable がインライン化され、ラウンド トリップが 1 回行われます。

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

新しい ToHashSetAsync<T> メソッド

ヒント

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

Enumerable.ToHashSet メソッドは、.NET Core 2.0 以降に存在します。 EF9 では、同等の非同期メソッドが追加されています。 次に例を示します。

var set1 = await context.Posts
    .Where(p => p.Tags.Count > 3)
    .ToHashSetAsync();

var set2 = await context.Posts
    .Where(p => p.Tags.Count > 3)
    .ToHashSetAsync(ReferenceEqualityComparer.Instance);

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

ExecuteUpdate と ExecuteDelete

複合型インスタンスを ExecuteUpdate に渡すことができるようになりました

ヒント

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

追跡や SaveChanges を実行せずにデータベースへの即時の直接更新を実行する ExecuteUpdate API が EF7 で導入されました。 次に例を示します。

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

このコードを実行すると、次のクエリが実行され、Regionが "Deutschland" に更新されます。

UPDATE [s]
SET [s].[Region] = N'Deutschland'
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

EF8 では、ExecuteUpdate を使って複合型プロパティの値を更新することもできます。 ただし、複合型の各メンバーを明示的に指定する必要があります。 次に例を示します。

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

await context.Stores
    .Where(e => e.Region == "Deutschland")
    .ExecuteUpdateAsync(
        s => s.SetProperty(b => b.StoreAddress.Line1, newAddress.Line1)
            .SetProperty(b => b.StoreAddress.Line2, newAddress.Line2)
            .SetProperty(b => b.StoreAddress.City, newAddress.City)
            .SetProperty(b => b.StoreAddress.Country, newAddress.Country)
            .SetProperty(b => b.StoreAddress.PostCode, newAddress.PostCode));

このコードを実行すると、次のクエリが実行されます。

UPDATE [s]
SET [s].[StoreAddress_PostCode] = @__newAddress_PostCode_4,
    [s].[StoreAddress_Country] = @__newAddress_Country_3,
    [s].[StoreAddress_City] = @__newAddress_City_2,
    [s].[StoreAddress_Line2] = NULL,
    [s].[StoreAddress_Line1] = @__newAddress_Line1_0
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Deutschland'

EF9 では、複合型インスタンス自体を渡すことで同じ更新を実行できます。 つまり、各メンバーを明示的に指定する必要はありません。 次に例を示します。

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

このコードを実行すると、前の例と同じクエリが実行されます。

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 の 1 回の呼び出しにまとめることができます。 次に例を示します。

await context.Customers
    .Where(e => e.Name == name)
    .ExecuteUpdateAsync(
        s => s.SetProperty(
                b => b.CustomerInfo.WorkAddress, new Address("Gressenhall Workhouse", null, "Beetley", "Norfolk", "NR20 4DR"))
            .SetProperty(b => b.CustomerInfo.HomeAddress, new Address("Gressenhall Farm", null, "Beetley", "Norfolk", "NR20 4DR"))
            .SetProperty(b => b.CustomerInfo.Tag, "Tog"));

このコードを実行すると、前の例と同じクエリが実行されます。

UPDATE [c]
SET [c].[CustomerInfo_Tag] = N'Tog',
    [c].[CustomerInfo_HomeAddress_City] = N'Beetley',
    [c].[CustomerInfo_HomeAddress_Country] = N'Norfolk',
    [c].[CustomerInfo_HomeAddress_Line1] = N'Gressenhall Farm',
    [c].[CustomerInfo_HomeAddress_Line2] = NULL,
    [c].[CustomerInfo_HomeAddress_PostCode] = N'NR20 4DR',
    [c].[CustomerInfo_WorkAddress_City] = N'Beetley',
    [c].[CustomerInfo_WorkAddress_Country] = N'Norfolk',
    [c].[CustomerInfo_WorkAddress_Line1] = N'Gressenhall Workhouse',
    [c].[CustomerInfo_WorkAddress_Line2] = NULL,
    [c].[CustomerInfo_WorkAddress_PostCode] = N'NR20 4DR'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0

移行

テンポラル テーブルの移行の改善

EF9 では、既存のテーブルをテンポラル テーブルに変更するときに作成される移行のサイズが小さくなりました。 たとえば、EF8 で 1 つの既存のテーブルをテンポラル テーブルにすると、次の移行が行われます。

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "SiteUri",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "Name",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<int>(
        name: "Id",
        table: "Blogs",
        type: "int",
        nullable: false,
        oldClrType: typeof(int),
        oldType: "int")
        .Annotation("SqlServer:Identity", "1, 1")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart")
        .OldAnnotation("SqlServer:Identity", "1, 1");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
}

EF9 では、同じ操作による移行の規模が大幅に小さくなりました。

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodEndColumn", true);

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodStartColumn", true);
}

モデル構築

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

ヒント

ここで示すコードは 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-preview.4.24205.3

ヒント

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

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

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
</PropertyGroup>

モデルのビルド方法を制御するための追加の省略可能な MSBuild プロパティがあります。これは、コマンド ラインで dotnet ef dbcontext optimize に渡されるオプションと同じです。 これには以下が含まれます。

MSBuild のプロパティ 説明
EFOptimizeContext 自動コンパイル済みモデルを有効にするには、true に設定します。
DbContextName 使用する DbContext クラス。 クラス名のみ、または名前空間で完全修飾された名前。 このオプションを省略した場合、EF Core によりコンテキスト クラスが検出されます。 複数のコンテキスト クラスがある場合、このオプションが必要です。
EFStartupProject スタートアップ プロジェクトへの相対パス。 既定値は現在のフォルダーです。
EFTargetNamespace 生成されるすべてのクラスに使用する名前空間。 既定値は、ルート名前空間、および出力ディレクトリと CompiledModels から生成されます。

この例では、スタートアップ プロジェクトを指定する必要があります。

<PropertyGroup>
  <EFOptimizeContext>true</EFOptimizeContext>
  <EFStartupProject>..\App\App.csproj</EFStartupProject>
</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 --startup-assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.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.

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

[注意!] EF8 および EF9 でコンパイル済みモデルに加えられた変更に関するパフォーマンスの問題の解決に取り組んでいます。 詳細については、issue 33483# を参照してください。

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

ヒント

ここで示すコードは 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"

シーケンスのキャッシュを指定する

ヒント

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

EF9 では、データベース シーケンスの キャッシュ オプションを、これをサポートする任意のリレーショナル データベース プロバイダーに対して設定できます。 たとえば、UseCache を使用すると、キャッシュを明示的にオンにして、キャッシュ サイズを設定できます。

modelBuilder.HasSequence<int>("MyCachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseCache(3);

これにより、SQL Server を使用すると、次のシーケンス定義が作成されます。

CREATE SEQUENCE [MyCachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE CACHE 3;

同様に、UseNoCache はキャッシュを明示的にオフにします。

modelBuilder.HasSequence<int>("MyUncachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseNoCache();
CREATE SEQUENCE [MyUncachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE NO CACHE;

UseCacheUseNoCache も呼び出されない場合、キャッシュは指定されず、データベースは既定値を使用します。 これはデータベースごとに異なる既定値になる場合があります。

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

キーとインデックスの 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 の動作を調整すると意図しない結果が生じる傾向があることも認識しているため、皆さんのような方々に、これを試していただき、ネガティブなエクスペリエンスがあれば報告していただくようお願いしています。