EF Core 8 (EF8) での破壊的変更
このページでは、EF Core 7 から EF Core 8 に更新された既存のアプリケーションを中断させる可能性がある API と動作変更について説明します。 以前のバージョンの EF Core から更新する場合は、以前の破壊的変更について確認してください。
ターゲット フレームワーク
EF Core 8 は .NET 8 をターゲットとします。 以前の .NET、.NET Core、および .NET Framework バージョンをターゲットとするアプリケーションは、.NET 8 をターゲットとするように更新する必要があります。
まとめ
影響が大きい変更
LINQ クエリの Contains
は、以前のバージョンの SQL Server で動作しなくなる可能性があります
以前の動作
EF では、パラメーター化された値リストに対して Contains
演算子を使用した LINQ クエリの特殊なサポートがありました。
var names = new[] { "Blog1", "Blog2" };
var blogs = await context.Blogs
.Where(b => names.Contains(b.Name))
.ToArrayAsync();
EF Core 8.0 より前では、EF はパラメーター化された値を定数として SQL に挿入しました。
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (N'Blog1', N'Blog2')
新しい動作
EF Core 8.0 以降では、EF は多くの場合、より効率的な SQL を生成するようになりましたが、SQL Server 2014 以降ではサポートされていません。
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (
SELECT [n].[value]
FROM OPENJSON(@__names_0) WITH ([value] nvarchar(max) '$') AS [n]
)
新しい SQL Server バージョンは、以前の互換性レベルで構成されている可能性があり、それにより新しい SQL との互換性もなくなる可能性があることに注意してください。 これは、以前のオンプレミスの SQL Server インスタンスから移行され、以前の互換性レベルを引き継ぐ Azure SQL データベースでも発生する可能性があります。
理由
SQL に定数値を挿入すると、多くのパフォーマンスの問題が発生し、クエリ プランのキャッシュが無効になり、他のクエリの不要な削除が発生します。 新しい EF Core 8.0 変換では、SQL Server の OPENJSON
関数を使用して、代わりに値を JSON 配列として転送します。 これにより、以前の手法に固有のパフォーマンスの問題が解決されます。ただし、OPENJSON
関数は SQL Server 2014 以前では使用できません。
この変更の詳細については、こちらのブログ記事を参照してください。
軽減策
データベースが SQL Server 2016 (13.x) 以降の場合、または Azure SQL を使用している場合は、次のコマンドを使用して、データベースの構成済みの互換性レベルを確認します。
SELECT name, compatibility_level FROM sys.databases;
互換性レベルが 130 (SQL Server 2016) 未満の場合は、それをより新しい値に変更することを検討してください (ドキュメント)。
それ以外の場合、データベースのバージョンが実際に SQL Server 2016 より古い場合、または何らかの理由で変更できない古い互換性レベルに設定されている場合は、EF を以前の 8.0 より前の SQL に戻すよう構成できます。 EF 9 を使用している場合は、新しく導入された TranslateParameterizedCollectionsToConstantsを使用できます。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer("<CONNECTION STRING>", o => o.TranslateParameterizedCollectionsToConstants())
EF 8 を使用している場合は、EF の SQL 互換性レベルを構成することで、SQL Server を使用する場合と同じ効果を実現できます。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));
LINQ クエリでの Contains
に関するクエリ パフォーマンスの回帰の可能性
以前の動作
EF では、パラメーター化された値リストに対して Contains
演算子を使用した LINQ クエリの特殊なサポートがありました。
var names = new[] { "Blog1", "Blog2" };
var blogs = await context.Blogs
.Where(b => names.Contains(b.Name))
.ToArrayAsync();
EF Core 8.0 より前では、EF はパラメーター化された値を定数として SQL に挿入しました。
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (N'Blog1', N'Blog2')
新しい動作
EF Core 8.0 以降では、EF によって次のものが生成されるようになりました。
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (
SELECT [n].[value]
FROM OPENJSON(@__names_0) WITH ([value] nvarchar(max) '$') AS [n]
)
ただし、EF 8 のリリース後、ほとんどの場合、新しい SQL の方が効率的ですが、少数のケースでは効率が大幅に低下する可能性があり、場合によってはクエリ タイムアウトが発生する可能性があります
軽減策
EF 9 を使用している場合は、新しく導入された TranslateParameterizedCollectionsToConstants を使用して、すべてのクエリの Contains
変換を 8.0 より前の動作に戻すことができます。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer("<CONNECTION STRING>", o => o.TranslateParameterizedCollectionsToConstants())
EF 8 を使用している場合は、EF の SQL 互換性レベルを構成することで、SQL Server を使用する場合と同じ効果を実現できます。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));
最後に、次のように EF.Constant を使用して、クエリごとに翻訳を制御できます。
var blogs = await context.Blogs
.Where(b => EF.Constant(names).Contains(b.Name))
.ToArrayAsync();
JSON の列挙型は、既定で文字列ではなく int として格納されます
以前の動作
EF7 では、JSON にマップされる列挙型は、JSON ドキュメントに既定で文字列値として格納されます。
新しい動作
EF Core 8.0 以降、EF は既定で列挙型を JSON ドキュメント内の整数値にマップするようになりました。
理由
EF はこれまで常に、既定で列挙型をリレーショナル データベースの数値列にマップしてきました。 EF では、JSON の値が列とパラメーターの値とやり取りするクエリがサポートされているため、JSON の値が JSON 以外の列の値と一致することが重要です。
軽減策
文字列を引き続き使用するには、列挙型プロパティを変換で構成します。 次に例を示します。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().Property(e => e.Status).HasConversion<string>();
}
または、列挙型のすべてのプロパティについて::
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<StatusEnum>().HaveConversion<string>();
}
影響が中程度の変更
SQL Server の date
と time
が .NET の DateOnly
と TimeOnly
にスキャフォールディングするようになります
以前の動作
これまで、date
または time
列がある SQL Server データベースをスキャフォールディングする場合、EF により、型が DateTime と TimeSpan のエンティティ プロパティが生成されました。
新しい動作
EF Core 8.0 以降、date
と time
は、DateOnly と TimeOnly としてスキャフォールディングされます。
理由
DateOnly と TimeOnly は .NET 6.0 で導入され、データベースの日付と時刻の型のマッピングに最適です。 DateTime には特に未使用の時間要素が含まれ、date
にマッピングするときに混乱を引き起こす可能性があり、TimeSpan はイベントが発生する時刻ではなく、時間間隔 (場合によっては日数を含む) を表します。 新しい型の使用により、バグと混乱が防止され、意図が明確になります。
軽減策
この変更は、データベースを EF コード モデル ("データベース優先" フロー) に定期的に再スキャフォールディングするユーザーにのみ影響します。
この変更に対応するには、新たにスキャフォールディングされる DateOnly と TimeOnly の型を使用するようにコードを変更することをお勧めします。 しかしながら、それが不可能な場合は、スキャフォールディング テンプレートを編集して、以前のマッピングに戻すことができます。 これを行うには、こちらのページの説明に従ってテンプレートを設定します。 次に、EntityType.t4
ファイルを編集し、エンティティ プロパティが生成される場所 (property.ClrType
を検索) を見つけ、コードを次のように変更します。
var clrType = property.GetColumnType() switch
{
"date" when property.ClrType == typeof(DateOnly) => typeof(DateTime),
"date" when property.ClrType == typeof(DateOnly?) => typeof(DateTime?),
"time" when property.ClrType == typeof(TimeOnly) => typeof(TimeSpan),
"time" when property.ClrType == typeof(TimeOnly?) => typeof(TimeSpan?),
_ => property.ClrType
};
usings.AddRange(code.GetRequiredUsings(clrType));
var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !clrType.IsValueType;
var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !clrType.IsValueType;
#>
public <#= code.Reference(clrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
データベース生成された値を持つブール値列は null 許容としてスキャフォールディングされなくなりました
以前の動作
以前は、データベースの既定の制約を持つ null 非許容 bool
列は、null 許容 bool?
プロパティとしてスキャフォールディングされていました。
新しい動作
EF Core 8.0 以降では、null 非許容 bool
列は常に null 非許容プロパティとしてスキャフォールディングされます。
理由
bool
プロパティは、その値が CLR の既定値である false
である場合、値をデータベースに送信しません。 データベースのその列に対する既定値が true
となっている場合、たとえプロパティの値が false
であっても、データベース内の値は最終的には true
になります。 ただし、EF8 では、プロパティに値があるかどうかを判断するために使用される番兵を変更できます。 これは、データベース生成された値 bool
を持つ true
プロパティに対して自動的に行われます。つまり、プロパティを null 許容としてスキャフォールディングする必要がなくなりました。
軽減策
この変更は、データベースを EF コード モデル ("データベース優先" フロー) に定期的に再スキャフォールディングするユーザーにのみ影響します。
null 非許容の bool プロパティを使用するようにコードを変更することでこの変更に対応することをお勧めします。 しかしながら、それが不可能な場合は、スキャフォールディング テンプレートを編集して、以前のマッピングに戻すことができます。 これを行うには、こちらのページの説明に従ってテンプレートを設定します。 次に、EntityType.t4
ファイルを編集し、エンティティ プロパティが生成される場所 (property.ClrType
を検索) を見つけ、コードを次のように変更します。
#>
var propertyClrType = property.ClrType != typeof(bool)
|| (property.GetDefaultValueSql() == null && property.GetDefaultValue() != null)
? property.ClrType
: typeof(bool?);
#>
public <#= code.Reference(propertyClrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
<#
影響が小さい変更
SQLite Math
メソッドが SQL に変換されるようになりました
以前の動作
以前は、Math
の Abs、Max、Min、Round のメソッドのみが SQL に変換されていました。 他のすべてのメンバーは、クエリの最後の Select 式に表示された場合、クライアントで評価されます。
新しい動作
EF Core 8.0 では、対応する Math
を持つすべての メソッドが SQL に変換されます。
これらの数学関数は、(SQLitePCLRaw.bundle_e_sqlite3 NuGet パッケージへの依存関係を通じて) 既定で提供されるネイティブ SQLite ライブラリで有効になっています。 これらは、SQLitePCLRaw.bundle_e_sqlcipher によって提供されるライブラリでも有効になっています。 これらのライブラリのいずれかを使用している場合、アプリケーションがこの変更の影響を受けないようにする必要があります。
ただし、他の方法でネイティブ SQLite ライブラリを含むアプリケーションでは、数学関数が有効にならない可能性があります。 このような場合、Math
メソッドは SQL に変換され、実行時に "そのような関数" エラーは発生しません。
理由
SQLite のバージョン 3.35.0 では、組み込みの数学関数が追加されました。 これらは既定では無効になっていますが、広く使用されるようになったため、EF Core SQLite プロバイダーで既定の変換を提供することにしました。
また、SQLitePCLRaw プロジェクトの Eric Sink と協力して、そのプロジェクトの一部として提供されるすべてのネイティブ SQLite ライブラリで数学関数を有効にしました。
軽減策
中断を修正する最も簡単な方法は、可能であれば、SQLITE_ENABLE_MATH_FUNCTIONS コンパイル時オプションを指定してネイティブ SQLite ライブラリを有効にすることです。
ネイティブ ライブラリのコンパイルを制御しない場合は、Microsoft.Data.Sqlite API を使用して実行時に関数を自分で作成することで、中断を修正することもできます。
sqliteConnection
.CreateFunction<double, double, double>(
"pow",
Math.Pow,
isDeterministic: true);
または、Select 式を AsEnumerable
で区切られた 2 つの部分に分割して、クライアント評価を強制することもできます。
// Before
var query = dbContext.Cylinders
.Select(
c => new
{
Id = c.Id
// May throw "no such function: pow"
Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
});
// After
var query = dbContext.Cylinders
// Select the properties you'll need from the database
.Select(
c => new
{
c.Id,
c.Radius,
c.Height
})
// Switch to client-eval
.AsEnumerable()
// Select the final results
.Select(
c => new
{
Id = c.Id,
Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
});
一部の API では ITypeBase が IEntityType の代わりとなります
以前の動作
以前は、マップされたすべての構造型がエンティティ型でした。
新しい動作
EF8 での複合型の導入により、一部の API は、エンティティ型または複合型のいずれかで API が使用できるように、以前は IEntityType
を使用していたのが ITypeBase
を使用するようになりました。 これには、次のものが含まれます。
IProperty.DeclaringEntityType
は廃止されたので代わりにIProperty.DeclaringType
を使用する必要があります。IEntityTypeIgnoredConvention
は廃止されたので代わりにITypeIgnoredConvention
を使用する必要があります。IValueGeneratorSelector.Select
はITypeBase
を受け取るようになり、これはIEntityType
であるかもしれませんが、必ずそうとは限りません。
理由
EF8 での複合型の導入により、これらの API は、IEntityType
または IComplexType
のいずれかで使用できます。
軽減策
古い API は廃止されていますが、EF10 まで削除されません。 コードは新しい API を使用するようにできるだけ早く更新する必要があります。
ValueConverter 式と ValueComparer 式では、コンパイル済みモデルに対してパブリック API を使用する必要があります
以前の動作
以前は、ValueConverter
および ValueComparer
の定義は、コンパイル済みモデルには含まれていなかったため、任意のコードを含むことができました。
新しい動作
EF は、ValueConverter
および ValueComparer
オブジェクトから式を抽出し、これらの C# をコンパイル済みモデルに含めるようになりました。 つまり、これらの式ではパブリック API のみを使用する必要があります。
理由
EF チームは、今後、AOT での EF Core の使用をサポートするために、さらに多くのコンストラクトをコンパイル済みモデルに段階的に移行している最中です。
軽減策
比較子によって使用される API をパブリックにします。 たとえば、次の単純なコンバーターについて考えてみます。
public class MyValueConverter : ValueConverter<string, byte[]>
{
public MyValueConverter()
: base(v => ConvertToBytes(v), v => ConvertToString(v))
{
}
private static string ConvertToString(byte[] bytes)
=> ""; // ... TODO: Conversion code
private static byte[] ConvertToBytes(string chars)
=> Array.Empty<byte>(); // ... TODO: Conversion code
}
EF8 を使用してコンパイル済みモデルでこのコンバーターを使用するには、ConvertToString
および ConvertToBytes
メソッドをパブリックにする必要があります。 次に例を示します。
public class MyValueConverter : ValueConverter<string, byte[]>
{
public MyValueConverter()
: base(v => ConvertToBytes(v), v => ConvertToString(v))
{
}
public static string ConvertToString(byte[] bytes)
=> ""; // ... TODO: Conversion code
public static byte[] ConvertToBytes(string chars)
=> Array.Empty<byte>(); // ... TODO: Conversion code
}
ExcludeFromMigrations は TPC 階層内の他のテーブルを除外しなくなりました
以前の動作
以前は、TPC 階層内のテーブルで ExcludeFromMigrations
を使用すると、階層内の他のテーブルも除外されていました。
新しい動作
EF Core 8.0 以降では、ExcludeFromMigrations
は他のテーブルには影響しません。
理由
以前の動作はバグであり、プロジェクトをまたいで階層を管理するために移行を使用することを妨げていました。
軽減策
除外する必要がある他のすべてのテーブルで ExcludeFromMigrations
を明示的に使用します。
非シャドウ整数キーは Cosmos ドキュメントに保持されます
以前の動作
以前は、合成されたキー プロパティとなる条件に一致する非シャドウ整数プロパティは JSON ドキュメントに保持されず、代わりに途中で再合成されていました。
新しい動作
EF Core 8.0 以降では、これらのプロパティが保持されるようになりました。
理由
以前の動作はバグであり、合成されたキー条件に一致するプロパティが Cosmos に保持されるのを防いでいました。
軽減策
プロパティの値が保持されるべきではない場合は、モデルからプロパティを除外します。
さらに、Microsoft.EntityFrameworkCore.Issue31664
AppContext スイッチを true
に設定してこの動作を完全に無効にすることができます。詳細については、「ライブラリ コンシューマーの AppContext」を参照してください。
AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31664", isEnabled: true);
リレーショナル モデルはコンパイル済みモデルで生成されます
以前の動作
以前は、コンパイル済みモデルを使用する場合でも、リレーショナル モデルは実行時に計算されていました。
新しい動作
EF Core 8.0 以降、リレーショナル モデルは生成されたコンパイル済みモデルの一部です。 ただし、特に大規模なモデルでは、生成されたファイルのコンパイルに失敗する場合があります。
理由
これは、起動時間をさらに改善するために行われました。
軽減策
生成された *ModelBuilder.cs
ファイルを編集し、メソッド AddRuntimeAnnotation("Relational:RelationalModel", CreateRelationalModel());
と同様に行 CreateRelationalModel()
を削除してください。
スキャフォールディングにより異なるナビゲーション名が生成される場合がある
以前の動作
以前は、既存のデータベースから DbContext
とエンティティ型をスキャフォールディングするときに、リレーションシップのナビゲーション名が複数の外部キー列名の共通プレフィックスから派生することがありました。
新しい動作
EF Core 8.0 以降では、複合外部キーからの列名の共通プレフィックスは、ナビゲーション名の生成に使用されなくなりました。
理由
これはあいまいな名前付けルールであり、S
、Student_
、あるいは単に _
などの非常に貧弱な名前が生成されることがあります。 このルールをなくすと、奇妙な名前が生成されなくなり、ナビゲーションの名前付け規則も簡素化されるため、生成される名前を理解して予測しやすくなります。
軽減策
EF Core Power Tools には、以前の方法でナビゲーションを生成し続けるオプションがあります。 または、生成されたコードを T4 テンプレートを使用して完全にカスタマイズできます。 これを使用すると、スキャフォールディング リレーションシップの外部キー プロパティの例を示し、実際のコードに適したルールを使用して、必要なナビゲーション名を生成できます。
識別子に最大長が設定されるようになった
以前の動作
以前は、TPH 継承マッピング用に作成された識別子列は、SQL Server/Azure SQL では nvarchar(max)
として、または他のデータベースでは同等の無制限の文字列型として構成されていました。
新しい動作
EF Core 8.0 以降では、識別子列は、すべての既知の識別子値をカバーする最大長で作成されます。 EF によってこの変更を行うための移行が生成されます。 ただし、識別子列が何らかの方法で (たとえば、インデックスの一部として) 制約されている場合、移行によって作成された AlterColumn
が失敗する可能性があります。
理由
nvarchar(max)
列は非効率的であり、使用可能なすべての値の長さがわかっている場合は不要です。
軽減策
この列のサイズは、明示的に無制限にすることができます。
modelBuilder.Entity<Foo>()
.Property<string>("Discriminator")
.HasMaxLength(-1);
SQL Server のキー値は大文字と小文字を区別せずに比較される
以前の動作
以前は、SQL Server/Azure SQL データベース プロバイダーで文字列キーを使用してエンティティを追跡する場合、キー値は、既定の .NET の大文字と小文字を区別する序数比較子を使用して比較されていました。
新しい動作
EF Core 8.0 以降では、SQL Server/Azure SQL 文字列キー値は、既定の .NET の大文字と小文字を区別しない序数比較子を使用して比較されます。
理由
既定では、SQL Server では、プリンシパル キー値と一致する外部キー値を比較するときに、大文字と小文字を区別しない比較が使用されます。 これは、EF で大文字と小文字が区別される比較を使用する場合、必要に応じて外部キーをプリンシパル キーに接続できない可能性があることを意味します。
軽減策
大文字と小文字を区別する比較は、カスタム ValueComparer
を設定すると使用できます。 次に例を示します。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var comparer = new ValueComparer<string>(
(l, r) => string.Equals(l, r, StringComparison.Ordinal),
v => v.GetHashCode(),
v => v);
modelBuilder.Entity<Blog>()
.Property(e => e.Id)
.Metadata.SetValueComparer(comparer);
modelBuilder.Entity<Post>(
b =>
{
b.Property(e => e.Id).Metadata.SetValueComparer(comparer);
b.Property(e => e.BlogId).Metadata.SetValueComparer(comparer);
});
}
複数の AddDbContext 呼び出しが異なる順序で適用される
以前の動作
以前は、同じコンテキスト タイプで競合する構成で AddDbContext
、 AddDbContextPool
、 AddDbContextFactory
または AddPooledDbContextFactor
への複数の呼び出しが行われた場合、最初の呼び出しが優先されていました。
新しい動作
EF Core 8.0 以降では、最後の呼び出しからの構成が優先されます。
理由
これは、メソッド ConfigureDbContext
の前または後に構成を追加するために使用できる新しいメソッド Add*
と一致するように変更されました。
軽減策
Add*
呼び出しの順序を逆にします。
EntityTypeAttributeConventionBase が TypeAttributeConventionBase に置き換えられました
新しい動作
EF Core 8.0 では EntityTypeAttributeConventionBase
は TypeAttributeConventionBase
に名前が変更されました。
理由
TypeAttributeConventionBase
は、複合型とエンティティ型に使用できるようになったため、より優れた機能を表します。
軽減策
EntityTypeAttributeConventionBase
の使用を TypeAttributeConventionBase
に置き換えます。
.NET