EF Core 9 (EF9) での破壊的変更
このページでは、EF Core 8 から EF Core 9 に更新する既存のアプリケーションを破壊する可能性がある API と動作の変更について説明します。 以前のバージョンの EF Core から更新する場合は、以前の破壊的変更について確認するようにしてください。
ターゲット フレームワーク
EF Core 9 は .NET 8 をターゲットとします。 つまり、NET 8 をターゲットとする既存のアプリケーションは引き続きこのバージョンをターゲットにできます。 以前の .NET、.NET Core、および .NET Framework のバージョンをターゲットとしているアプリケーションは、EF Core 9 を使用するためには .NET 8 または .NET 9 をターゲットとする必要があります。
まとめ
Note
Azure Cosmos DB を使用している場合、 Azure Cosmos DB の破壊的変更に関する以下の別個のセクションをご覧ください。
重大な変更 | 影響 |
---|---|
EF.Functions.Unhex() により byte[]? が返されるようになりました |
低 |
SqlFunctionExpression の null 値許容引数のアリティを検証 | 低 |
ToString() メソッドが null インスタンスの空の文字列を返すようになりました |
低 |
共有フレームワークの依存関係が 9.0.x に更新されました | 低 |
影響が小さい変更
EF.Functions.Unhex()
により byte[]?
が返されるようになりました
以前の動作
EF.Functions.Unhex()
関数は以前は byte[]
を返すよう注釈が付けられていました。
新しい動作
EF Core 9.0 以降、Unhex() には byte[]?
を返すように注釈が付けられるようになりました。
理由
Unhex()
は SQLite unhex
関数に変換され、無効な入力に対しては NULL が返されます。 その結果、Unhex()
は注釈に違反して、これらのケースに対して null
を返しました。
軽減策
Unhex()
に渡されるテキスト コンテンツが有効な 16 進文字列を表していることが確実な場合、呼び出しで null が返されないというアサーションとして、null を許容する演算子を追加するだけです。
var binaryData = await context.Blogs.Select(b => EF.Functions.Unhex(b.HexString)!).ToListAsync();
それ以外の場合は、Unhex() の戻り値に対して null 値の実行時チェックを追加します。
SqlFunctionExpression の null 値許容引数のアリティを検証
以前の動作
以前は、異なる数の引数と null 値許容伝達引数で SqlFunctionExpression
を作成することが可能でした。
新しい動作
EF Core 9.0 以降、引数の数と null 値許容伝達引数が一致しない場合、EF はスローを行うようになりました。
理由
引数と null 値許容伝達引数の数が一致しないことで、予期しない動作が発生する可能性があります。
軽減策
argumentsPropagateNullability
が arguments
と同じ要素数を持っていることを確認します。 迷った場合は null 値許容引数に対して false
を使用します。
ToString()
メソッドが null
インスタンスの空の文字列を返すようになりました
以前の動作
以前は、引数値が null
の場合、EF は ToString()
メソッドに対して一貫性のない結果を返していました。 たとえば、値が null
である bool?
プロパティの ToString()
は null
を返しましたが、値が null
である非プロパティ bool?
式の場合は True
を返しました。 他のデータ型でも動作に一貫性がありませんでした。たとえば、 null
値列挙の ToString()
は空の文字列を返しました。
新しい動作
EF Core 9.0 以降では、引数値が null
の場合、 ToString()
メソッドは常に空の文字列を返すようになりました。
理由
以前の動作は、さまざまなデータ型や状況間で一貫性がなく、 C# の動作とも一致していませんでした。
軽減策
古い動作に戻すには、それに応じてクエリを書き直します。
var newBehavior = context.Entity.Select(x => x.NullableBool.ToString());
var oldBehavior = context.Entity.Select(x => x.NullableBool == null ? null : x.NullableBool.ToString());
共有フレームワークの依存関係が 9.0.x に更新されました
以前の動作
Microsoft.NET.Sdk.Web
SDK を使用し、net8.0 をターゲットとするアプリは、共有フレームワークから System.Text.Json
、 Microsoft.Extensions.Caching.Memory
、 Microsoft.Extensions.Configuration.Abstractions
、 Microsoft.Extensions.Logging
および Microsoft.Extensions.DependencyModel
などのパッケージを解決するため、通常、これらのアセンブリはアプリと共に展開されません。
新しい動作
EF Core 9.0 は引き続き net8.0 をサポートしていますが、現在は 9.0.x バージョンの System.Text.Json
、 Microsoft.Extensions.Caching.Memory
、 Microsoft.Extensions.Configuration.Abstractions
、 Microsoft.Extensions.Logging
および Microsoft.Extensions.DependencyModel
を参照しています。 net8.0 をターゲットとするアプリでは、これらのアセンブリのデプロイを回避するために共有フレームワークを利用することはできません。
理由
一致する依存関係バージョンには最新のセキュリティ修正プログラムが含まれており、それらを使用すると EF Core のサービス モデルが簡略化されます。
軽減策
前の動作を取得するには、アプリをターゲット net9.0 に変更します。
Azure Cosmos DB の破壊的変更
9.0 では、Azure Cosmos DB プロバイダーの改善に多大な労力が費やされました。 この変更には、影響が大きい破壊的変更が多数含まれています。既存のアプリケーションをアップグレードする場合、以下をよくお読みください。
重大な変更 | 影響 |
---|---|
ディスクリミネーター プロパティの名前が、Discriminator ではなく $type に変更されました |
高 |
既定では、id プロパティにディスクリミネーターが含まれるようになりました |
高 |
Azure Cosmos DB プロバイダー経由の同期 I/O はサポートされなくなりました | Medium |
SQL クエリで JSON 値を直接プロジェクションする必要があります | Medium |
未定義の結果がクエリ結果から自動的にフィルター処理されるようになりました | Medium |
誤って変換されたクエリは変換されなくなりました | Medium |
無視される代わりに HasIndex がスローされるようになりました |
低 |
9.0.0-rc.2 の後、IncludeRootDiscriminatorInJsonId は HasRootDiscriminatorInJsonId に名前が変更されました |
低 |
影響が大きい変更
ディスクリミネーター プロパティの名前が、Discriminator
ではなく $type
に変更されました
以前の動作
EF は、JSON ドキュメントにディスクリミネーター プロパティを自動的に追加し、ドキュメントが表すエンティティ型を識別します。 以前のバージョンの EF では、この JSON プロパティには既定で Discriminator
という名前が付けられていました。
新しい動作
EF Core 9.0 以降では、ディスクリミネーター プロパティは既定で $type
呼び出されるようになりました。 以前のバージョンの EF から得られた既存のドキュメントが Azure Cosmos DB にある場合、古い Discriminator
の命名が使用されており、EF 9.0 にアップグレードした後、それらのドキュメントに対するクエリは失敗します。
理由
新しい JSON プラクティスでは、ドキュメントの種類を識別する必要があるシナリオにおいて、$type
プロパティが使用されます。 たとえば、.NET の System.Text.Json もポリモーフィズムをサポートしており、$type
を既定の識別子プロパティ名として使用しています (ドキュメント)。 エコシステムの残りの部分に合わせて、外部ツールとの相互運用を容易にするため、既定値が変更されました。
軽減策
最も簡単な軽減策は、以前と同じように、ディスクリミネーター プロパティの名前を Discriminator
に構成するだけです。
modelBuilder.Entity<Session>().HasDiscriminator<string>("Discriminator");
これをすべての最上位レベルのエンティティ型に対して行うと、EF は以前と同じように動作します。
この時点で、必要に応じて、新しい $type
の命名を使用するようすべてのドキュメントを更新することもできます。
id
プロパティには、既定で EF キー プロパティのみ含まれるようになりました
以前の動作
以前は、EF はエンティティ型のディスクリミネーター値をドキュメントの id
プロパティに挿入していました。 たとえば、8 を含む Id
プロパティを持つ Blog
エンティティ型を保存した場合、JSON id
プロパティには Blog|8
が含められます。
新しい動作
EF Core 9.0 以降、JSON id
プロパティにはディスクリミネーター値が含められなくなり、キー プロパティの値のみが含められます。 上記の例では、JSON id
プロパティは単に 8
になります。 以前のバージョンの EF から得られた既存のドキュメントが Azure Cosmos DB にある場合、JSON id
プロパティにはディスクリミネーター値が含まれており、EF 9.0 にアップグレードした後、それらのドキュメントに対するクエリは失敗します。
理由
JSON id
プロパティは一意である必要があるため、同じキー値を持つ異なるエンティティが存在できるように、識別子が以前に追加されました。 たとえば、これにより、同じコンテナーとパーティション内に、値 8 を含む Id
プロパティを持つ Blog
と Post
の両方を持つことができるようになりました。 これは、各エンティティ型が独自のテーブルにマップされるため、独自のキースペースを持つリレーショナルデータベースデータモデリングパターンにより適しています。
EF 9.0 では、リレーショナル データベースからのユーザーの期待に応えるのではなく、一般的な Azure Cosmos DB NoSQL のプラクティスと期待に沿うようマッピングが全体的に変更されました。 さらに、id
プロパティにディスクリミネーターの値があると、外部ツールやシステムが EF で生成された JSON ドキュメントと対話することがより困難になります。このような外部システムは通常、既定で .NET 型から派生する EF ディスクリミネーターの値を認識しません。
軽減策
最も簡単な軽減策は、以前と同様、JSON id
プロパティにディスクリミネーターを含むよう EF を構成する方法です。 この目的のため、新しい構成オプションが導入されました。
modelBuilder.Entity<Session>().HasDiscriminatorInJsonId();
これをすべての最上位レベルのエンティティ型に対して行うと、EF は以前と同じように動作します。
この時点で、必要に応じて、すべてのドキュメントを更新して JSON id
プロパティを書き換えることもできます。 これは、異なる型のエンティティが同じコンテナー内で同じ ID 値を共有していない場合のみ可能です。
影響が中程度の変更
Azure Cosmos DB プロバイダー経由の同期 I/O はサポートされなくなりました
以前の動作
以前は、Azure Cosmos DB SDK に対して非同期呼び出しを実行する際に、ToList
や SaveChanges
などの同期メソッドを呼び出すことは、EF Core が .GetAwaiter().GetResult()
を使用して同期的にブロックする原因となっていました。 これにより、デッドロックが発生する可能性があります。
新しい動作
EF Core 9.0 以降では、同期 I/O の使用が試みられた場合に、EF が既定で例外をスローするようになりました。 例外メッセージは次のとおりです。"Azure Cosmos DB は同期 I/O をサポートしていません。 Entity Framework Core を使用して Azure Cosmos DB にアクセスする場合は、非同期メソッドのみを使用してそれを適切に待機するようにしてください。 詳細については、https://aka.ms/ef-cosmos-nosync を参照してください。"
理由
非同期メソッドでの同期ブロックはデッドロックを発生させる可能性があり、Azure Cosmos DB SDK がサポートしているのは非同期メソッドだけです。
軽減策
EF Core 9.0 において、このエラーは次のように抑制できます。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.ConfigureWarnings(w => w.Ignore(CosmosEventId.SyncNotSupported));
}
しかしながら、同期 API は Azure Cosmos DB SDK ではサポートされていないため、アプリケーションは同期 API の使用を停止するべきです。 例外を抑制する機能は、EF Core の将来のリリースで削除され、その後は非同期 API の使用が唯一の選択肢になります。
SQL クエリで JSON 値を直接プロジェクションする必要があります
以前の動作
以前は、EF は次のようなクエリを生成していました。
SELECT c["City"] FROM root c
このようなクエリにより、以下のように Azure Cosmos DB によって各結果が JSON オブジェクトにラップされます。
[
{
"City": "Berlin"
},
{
"City": "México D.F."
}
]
新しい動作
EF Core 9.0 以降、EF は次のように VALUE
修飾子をクエリに追加するようになりました。
SELECT VALUE c["City"] FROM root c
このようなクエリにより、Azure Cosmos DB はラップされずに値を直接返します。
[
"Berlin",
"México D.F."
]
アプリケーションで SQL クエリを使用している場合、そのようなクエリには VALUE
修飾子が含まれていないため、EF 9.0 にアップグレードすると、そのクエリが壊れる可能性があります。
理由
各結果を追加の JSON オブジェクトにラップすると、シナリオによってはパフォーマンスが低下して、JSON 結果のペイロードが肥大化する可能性があるため、Azure Cosmos DB を操作する自然な方法ではありません。
軽減策
軽減するには、上に示すように、VALUE
修飾子を SQL クエリのプロジェクションに追加するだけでかまいません。
未定義の結果がクエリ結果から自動的にフィルター処理されるようになりました
以前の動作
以前は、EF は次のようなクエリを生成していました。
SELECT c["City"] FROM root c
このようなクエリにより、以下のように Azure Cosmos DB によって各結果が JSON オブジェクトにラップされます。
[
{
"City": "Berlin"
},
{
"City": "México D.F."
}
]
いずれかの結果が未定義の場合 (たとえば、City
プロパティがドキュメントに存在しない場合)、空のドキュメントが返され、EF はその結果の null
を返します。
新しい動作
EF Core 9.0 以降、EF は次のように VALUE
修飾子をクエリに追加するようになりました。
SELECT VALUE c["City"] FROM root c
このようなクエリにより、Azure Cosmos DB はラップされずに値を直接返します。
[
"Berlin",
"México D.F."
]
Azure Cosmos DB の動作では、 undefined
値が結果から自動的にフィルター処理されます。つまり、ドキュメントに City
プロパティのいずれかが存在しない場合、クエリは 2 つの結果 (1 つは null
) ではなく、1 つの結果のみを返します。
理由
各結果を追加の JSON オブジェクトにラップすると、シナリオによってはパフォーマンスが低下して、JSON 結果のペイロードが肥大化する可能性があるため、Azure Cosmos DB を操作する自然な方法ではありません。
軽減策
未定義の結果に対して null
値を取得することがアプリケーションにとって重要な場合、新しい EF.Functions.Coalesce
演算子を使用して undefined
値を null
に結合します。
var users = await context.Customer
.Select(c => EF.Functions.CoalesceUndefined(c.City, null))
.ToListAsync();
誤って変換されたクエリは変換されなくなりました
以前の動作
以前は、EF は次のようなクエリを変換していました。
var sessions = await context.Sessions
.Take(5)
.Where(s => s.Name.StartsWith("f"))
.ToListAsync();
ただし、このクエリの SQL 変換は正しくありません。
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Session") AND STARTSWITH(c["Name"], "f"))
OFFSET 0 LIMIT @__p_0
SQL では、WHERE
句は OFFSET
句と LIMIT
句の前に評価されますが、上記の LINQ クエリでは、Take
演算子が Where
演算子の前に表示されます。 その結果、このようなクエリから正しくない結果が返される可能性があります。
新しい動作
EF Core 9.0 以降、このようなクエリは変換されなくなり、例外がスローされます。
理由
変換が正しくないと、サイレント データの破損が発生し、検出が困難な不具合がアプリケーションで発生する可能性があります。 EF は、データ破損を引き起こす可能性よりも、事前にスローすることでフェイルファストすることを常に優先します。
軽減策
前の動作に満足していて、同じ SQL を実行したい場合は、LINQ 演算子の順序を入れ替えるだけでかまいません。
var sessions = await context.Sessions
.Where(s => s.Name.StartsWith("f"))
.Take(5)
.ToListAsync();
残念ながら、Azure Cosmos DB では現在、SQL サブクエリの OFFSET
句と LIMIT
句はサポートされていません。これは、元の LINQ クエリの適切な変換に必要です。
影響が小さい変更
無視される代わりに HasIndex
がスローされるようになりました
以前の動作
以前は、HasIndex の呼び出しは EF Cosmos DB プロバイダーによって無視されていました。
新しい動作
HasIndex が指定されている場合、プロバイダーによりスローされるようになりました。
理由
Azure Cosmos DB では、すべてのプロパティに既定でインデックスが付けられます。インデックスを指定する必要はありません。 カスタム インデックス作成ポリシーを定義することはできますが、これは現在 EF ではサポートされていないため、EF のサポートなしで Azure Portal を使用して行うことができます。 HasIndex 呼び出しは何も行われないため、許可されなくなりました。
軽減策
HasIndex の呼び出しをすべて削除します。
9.0.0-rc.2 の後、IncludeRootDiscriminatorInJsonId
は HasRootDiscriminatorInJsonId
に名前が変更されました
以前の動作
IncludeRootDiscriminatorInJsonId
API は、9.0.0 rc.1 で導入されました。
新しい動作
EF Core 9.0 の最終リリースでは、API の名前が HasRootDiscriminatorInJsonId
に変更されました
理由
別の関連 API の名前が Include
ではなく Has
で始まるように変更されたため、一貫性を保つためにこの API の名前も変更されました。
軽減策
コードで IncludeRootDiscriminatorInJsonId
API を使用している場合、代わりに HasRootDiscriminatorInJsonId
を参照するよう変更するだけでかまいません。
.NET