Share via


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 バージョンでは動作しなくなる可能性があります
JSON の列挙型は、既定で文字列ではなく int として格納されます
SQL Server の datetime が .NET の DateOnlyTimeOnlyにスキャフォールディングするようになります
データベース生成された値を持つブール値列は null 許容としてスキャフォールディングされなくなりました
SQLite Math メソッドが SQL に変換されるようになりました
一部の API では ITypeBase が IEntityType の代わりとなります
ValueGenerator 式ではパブリック API を使用する必要があります
ExcludeFromMigrations は TPC 階層内の他のテーブルを除外しなくなりました
非シャドウ整数キーは Cosmos ドキュメントに保持されます
リレーショナル モデルはコンパイル済みモデルで生成されます
スキャフォールディングにより異なるナビゲーション名が生成される場合がある
識別子に最大長が設定されるようになった
SQL Server のキー値は大文字と小文字を区別せずに比較される

影響が大きい変更

LINQ クエリの Contains は、以前のバージョンの SQL Server で動作しなくなる可能性があります

イシュー #13617 の追跡

以前の動作

以前は、パラメーター化された値リストを含む LINQ クエリで Contains 演算子が使用されていた場合、EF は非効率的ではあるものの、すべての SQL Server バージョンで動作する SQL を生成していました。

新しい動作

EF Core 8.0 以降では、EF により効率的な SQL が生成されるようになりましたが、SQL Server 2014 以前ではサポートされていません。

新しい SQL Server バージョンは、以前の互換性レベルで構成されている可能性があり、それにより新しい SQL との互換性もなくなる可能性があることに注意してください。 これは、以前のオンプレミスの SQL Server インスタンスから移行され、以前の互換性レベルを引き継ぐ Azure SQL データベースでも発生する可能性があります。

理由

Contains 用に EF Core によって生成された以前の SQL は、パラメーター化された値が定数として SQL に挿入されました。 たとえば、次の LINQ クエリは、

var names = new[] { "Blog1", "Blog2" };

var blogs = await context.Blogs
    .Where(b => names.Contains(b.Name))
    .ToArrayAsync();

次の SQL に変換されます。

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (N'Blog1', N'Blog2')

このような定数値を 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 Core を以前の効率の低い SQL に戻すように構成します。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));

JSON の列挙型は、既定で文字列ではなく int として格納されます

イシュー #13617 の追跡

以前の動作

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 の datetime が .NET の DateOnlyTimeOnly にスキャフォールディングするようになります

イシュー #24507 の追跡

以前の動作

これまで、date または time 列がある SQL Server データベースをスキャフォールディングする場合、EF により、型が DateTimeTimeSpan のエンティティ プロパティが生成されました。

新しい動作

EF Core 8.0 以降、datetime は、DateOnlyTimeOnly としてスキャフォールディングされます。

理由

DateOnlyTimeOnly は .NET 6.0 で導入され、データベースの日付と時刻の型のマッピングに最適です。 DateTime には特に未使用の時間要素が含まれ、date にマッピングするときに混乱を引き起こす可能性があり、TimeSpan はイベントが発生する時刻ではなく、時間間隔 (場合によっては日数を含む) を表します。 新しい型の使用により、バグと混乱が防止され、意図が明確になります。

軽減策

この変更は、データベースを EF コード モデル ("データベース優先" フロー) に定期的に再スキャフォールディングするユーザーにのみ影響します。

この変更に対応するには、新たにスキャフォールディングされる DateOnlyTimeOnly の型を使用するようにコードを変更することをお勧めします。 しかしながら、それが不可能な場合は、スキャフォールディング テンプレートを編集して、以前のマッピングに戻すことができます。 これを行うには、こちらのページの説明に従ってテンプレートを設定します。 次に、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 許容としてスキャフォールディングされなくなりました

イシュー #15070 の追跡

以前の動作

以前は、データベースの既定の制約を持つ null 非許容 bool 列は、null 許容 bool? プロパティとしてスキャフォールディングされていました。

新しい動作

EF Core 8.0 以降では、null 非許容 bool 列は常に null 非許容プロパティとしてスキャフォールディングされます。

理由

bool プロパティは、その値が CLR の既定値である false である場合、値をデータベースに送信しません。 データベースのその列に対する既定値が true となっている場合、たとえプロパティの値が false であっても、データベース内の値は最終的には true になります。 ただし、EF8 では、プロパティに値があるかどうかを判断するために使用される番兵を変更できます。 これは、データベース生成された値 true を持つ bool プロパティに対して自動的に行われます。つまり、プロパティを 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 に変換されるようになりました

イシュー #18843 の追跡

以前の動作

以前は、Math の Abs、Max、Min、Round のメソッドのみが SQL に変換されていました。 他のすべてのメンバーは、クエリの最後の Select 式に表示された場合、クライアントで評価されます。

新しい動作

EF Core 8.0 では、対応する SQLite 数学関数を持つすべての 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 の代わりとなります

イシュー #13947 の追跡

以前の動作

以前は、マップされたすべての構造型がエンティティ型でした。

新しい動作

EF8 での複合型の導入により、一部の API は、エンティティ型または複合型のいずれかで API が使用できるように、以前は IEntityType を使用していたのが ITypeBase を使用するようになりました。 これには、次のものが含まれます。

  • IProperty.DeclaringEntityType は廃止されたので代わりに IProperty.DeclaringType を使用する必要があります。
  • IEntityTypeIgnoredConvention は廃止されたので代わりに ITypeIgnoredConvention を使用する必要があります。
  • IValueGeneratorSelector.SelectITypeBase を受け取るようになり、これは IEntityType であるかもしれませんが、必ずそうとは限りません。

理由

EF8 での複合型の導入により、これらの API は、IEntityType または IComplexType のいずれかで使用できます。

軽減策

古い API は廃止されていますが、EF10 まで削除されません。 コードは新しい API を使用するようにできるだけ早く更新する必要があります。

ValueConverter 式と ValueComparer 式では、コンパイル済みモデルに対してパブリック API を使用する必要があります

イシュー #24896 の追跡

以前の動作

以前は、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 階層内の他のテーブルを除外しなくなりました

イシュー #30079 の追跡

以前の動作

以前は、TPC 階層内のテーブルで ExcludeFromMigrations を使用すると、階層内の他のテーブルも除外されていました。

新しい動作

EF Core 8.0 以降では、ExcludeFromMigrations は他のテーブルには影響しません。

理由

以前の動作はバグであり、プロジェクトをまたいで階層を管理するために移行を使用することを妨げていました。

軽減策

除外する必要がある他のすべてのテーブルで ExcludeFromMigrations を明示的に使用します。

非シャドウ整数キーは Cosmos ドキュメントに保持されます

イシュー #31664 の追跡

以前の動作

以前は、合成されたキー プロパティとなる条件に一致する非シャドウ整数プロパティは JSON ドキュメントに保持されず、代わりに途中で再合成されていました。

新しい動作

EF Core 8.0 以降では、これらのプロパティが保持されるようになりました。

理由

以前の動作はバグであり、合成されたキー条件に一致するプロパティが Cosmos に保持されるのを防いでいました。

軽減策

プロパティの値が保持されるべきではない場合は、モデルからプロパティを除外します。 さらに、Microsoft.EntityFrameworkCore.Issue31664 AppContext スイッチを true に設定してこの動作を完全に無効にすることができます。詳細については、「ライブラリ コンシューマーの AppContext」を参照してください。

AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31664", isEnabled: true);

リレーショナル モデルはコンパイル済みモデルで生成されます

イシュー #24896 の追跡

以前の動作

以前は、コンパイル済みモデルを使用する場合でも、リレーショナル モデルは実行時に計算されていました。

新しい動作

EF Core 8.0 以降、リレーショナル モデルは生成されたコンパイル済みモデルの一部です。 ただし、特に大規模なモデルでは、生成されたファイルのコンパイルに失敗する場合があります。

理由

これは、起動時間をさらに改善するために行われました。

軽減策

生成された *ModelBuilder.cs ファイルを編集し、メソッド CreateRelationalModel() と同様に行 AddRuntimeAnnotation("Relational:RelationalModel", CreateRelationalModel()); を削除してください。

スキャフォールディングにより異なるナビゲーション名が生成される場合がある

イシュー #27832 の追跡

以前の動作

以前は、既存のデータベースから DbContext とエンティティ型をスキャフォールディングするときに、リレーションシップのナビゲーション名が複数の外部キー列名の共通プレフィックスから派生することがありました。

新しい動作

EF Core 8.0 以降では、複合外部キーからの列名の共通プレフィックスは、ナビゲーション名の生成に使用されなくなりました。

理由

これはあいまいな名前付けルールであり、SStudent_、あるいは単に _ などの非常に貧弱な名前が生成されることがあります。 このルールをなくすと、奇妙な名前が生成されなくなり、ナビゲーションの名前付け規則も簡素化されるため、生成される名前を理解して予測しやすくなります。

軽減策

EF Core Power Tools には、以前の方法でナビゲーションを生成し続けるオプションがあります。 または、生成されたコードを T4 テンプレートを使用して完全にカスタマイズできます。 これを使用すると、スキャフォールディング リレーションシップの外部キー プロパティの例を示し、実際のコードに適したルールを使用して、必要なナビゲーション名を生成できます。

識別子に最大長が設定されるようになった

イシュー #10691 の追跡

以前の動作

以前は、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 のキー値は大文字と小文字を区別せずに比較される

イシュー #27526 の追跡

以前の動作

以前は、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);
        });
}