Breaking changes in EF Core 8 (EF8)

This page documents API and behavior changes that have the potential to break existing applications updating from EF Core 7 to EF Core 8. Make sure to review earlier breaking changes if updating from an earlier version of EF Core:

Target Framework

EF Core 8 targets .NET 8. Applications targeting older .NET, .NET Core, and .NET Framework versions will need to update to target .NET 8.

Summary

Breaking change Impact
Contains in LINQ queries may stop working on older SQL Server versions High
Enums in JSON are stored as ints instead of strings by default High
SQL Server date and time now scaffold to .NET DateOnly and TimeOnly Medium
Boolean columns with a database generated value are no longer scaffolded as nullable Medium
SQLite Math methods now translate to SQL Low
ITypeBase replaces IEntityType in some APIs Low
ValueGenerator expressions must use public APIs Low
ExcludeFromMigrations no longer excludes other tables in a TPC hierarchy Low
Non-shadow integer keys are persisted to Cosmos documents Low

High-impact changes

Contains in LINQ queries may stop working on older SQL Server versions

Tracking Issue #13617

Old behavior

Previously, when the Contains operator was used in LINQ queries with a parameterized value list, EF generated SQL that was inefficient but worked on all SQL Server versions.

New behavior

Starting with EF Core 8.0, EF now generates SQL that is more efficient, but is unsupported on SQL Server 2014 and below.

Note that newer SQL Server versions may be configured with an older compatibility level, also making them incompatible with the new SQL. This can also occur with an Azure SQL database which was migrated from a previous on-premises SQL Server instance, carrying over the old compatibility level.

Why

The previous SQL generated by EF Core for Contains inserted the parameterized values as constants in the SQL. For example, the following LINQ query:

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

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

... would be translated to the following SQL:

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

Such insertion of constant values into the SQL creates many performance problems, defeating query plan caching and causing unneeded evictions of other queries. The new EF Core 8.0 translation uses the SQL Server OPENJSON function to instead transfer the values as a JSON array. This solves the performance issues inherent in the previous technique; however, the OPENJSON function is unavailable in SQL Server 2014 and below.

For more information about this change, see this blog post.

Mitigations

If your database is SQL Server 2016 (13.x) or newer, or if you're using Azure SQL, check the configured compatibility level of your database via the following command:

SELECT name, compatibility_level FROM sys.databases;

If the compatibility level is below 130 (SQL Server 2016), consider modifying it to a newer value (documentation.

Otherwise, if your database version really is older than SQL Server 2016, or is set to an old compatibility level which you cannot change for some reason, configure EF Core to revert to the older, less efficient SQL as follows:

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

Enums in JSON are stored as ints instead of strings by default

Tracking Issue #13617

Old behavior

In EF7, enums mapped to JSON are, by default, stored as string values in the JSON document.

New behavior

Starting with EF Core 8.0, EF now, by default, maps enums to integer values in the JSON document.

Why

EF has always, by default, mapped enums to a numeric column in relational databases. Since EF supports queries where values from JSON interact with values from columns and parameters, it is important that the values in JSON match the values in the non-JSON column.

Mitigations

To continue using strings, configure the enum property with a conversion. For example:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>().Property(e => e.Status).HasConversion<string>();
}

Or, for all properties of the enum type::

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<StatusEnum>().HaveConversion<string>();
}

Medium-impact changes

SQL Server date and time now scaffold to .NET DateOnly and TimeOnly

Tracking Issue #24507

Old behavior

Previously, when scaffolding a SQL Server database with date or time columns, EF would generate entity properties with types DateTime and TimeSpan.

New behavior

Starting with EF Core 8.0, date and time are scaffolded as DateOnly and TimeOnly.

Why

DateOnly and TimeOnly were introduced in .NET 6.0, and are a perfect match for mapping the database date and time types. DateTime notably contains a time component that goes unused and can cause confusion when mapping it to date, and TimeSpan represents a time interval - possibly including days - rather than a time of day at which an event occurs. Using the new types prevents bugs and confusion, and provides clarity of intent.

Mitigations

This change only affects users which regularly re-scaffold their database into an EF code model ("database-first" flow).

It is recommended to react to this change by modifying your code to use the newly scaffolded DateOnly and TimeOnly types. However, if that isn't possible, you can edit the scaffolding templates to revert to the previous mapping. To do this, set up the templates as described on this page. Then, edit the EntityType.t4 file, find where the entity properties get generated (search for property.ClrType), and change the code to the following:

        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!;" : "" #>
<#

Boolean columns with a database generated value are no longer scaffolded as nullable

Tracking Issue #15070

Old behavior

Previously, non-nullable bool columns with a database default constraint were scaffolded as nullable bool? properties.

New behavior

Starting with EF Core 8.0, non-nullable bool columns are always scaffolded as non-nullable properties.

Why

A bool property will not have its value sent to the database if that value is false, which is the CLR default. If the database has a default value of true for the column, then even though the value of the property is false, the value in the database ends up as true. However, in EF8, the sentinel used to determine whether a property has a value can be changed. This is done automatically for bool properties with a database generated value of true, which means that it is no longer necessary to scaffold the properties as nullable.

Mitigations

This change only affects users which regularly re-scaffold their database into an EF code model ("database-first" flow).

It is recommended to react to this change by modifying your code to use the non-nullable bool property. However, if that isn't possible, you can edit the scaffolding templates to revert to the previous mapping. To do this, set up the templates as described on this page. Then, edit the EntityType.t4 file, find where the entity properties get generated (search for property.ClrType), and change the code to the following:

#>
        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!;" : "" #>
<#
<#

Low-impact changes

SQLite Math methods now translate to SQL

Tracking Issue #18843

Old Behavior

Previously only the Abs, Max, Min, and Round methods on Math were translated to SQL. All other members would be evaluated on the client if they appeared in the final Select expression of a query.

New behavior

In EF Core 8.0, all Math methods with corresponding SQLite math functions are translated to SQL.

These math functions have been enabled in the native SQLite library that we provide by default (through our dependency on the SQLitePCLRaw.bundle_e_sqlite3 NuGet package). They have also been enabled in the library provided by SQLitePCLRaw.bundle_e_sqlcipher. If you're using one of these libraries, your application should not be affected by this change.

There is a chance, however, that applications including the native SQLite library by other means may not enable the math functions. In these cases, the Math methods will be translated to SQL and encounter no such function errors when executed.

Why

SQLite added built-in math functions in version 3.35.0. Even though they're disabled by default, they've become pervasive enough that we decided to provide default translations for them in our EF Core SQLite provider.

We also collaborated with Eric Sink on the SQLitePCLRaw project to enable math functions in all of the native SQLite libraries provided as part of that project.

Mitigations

The simplest way to fix breaks is, when possible, to enable the math function is the native SQLite library by specifying the SQLITE_ENABLE_MATH_FUNCTIONS compile-time option.

If you don't control compilation of the native library, you can also fix breaks by create the functions yourself at runtime using the Microsoft.Data.Sqlite APIs.

sqliteConnection
    .CreateFunction<double, double, double>(
        "pow",
        Math.Pow,
        isDeterministic: true);

Alternatively, you can force client-evaluation by splitting the Select expression into two parts separated by AsEnumerable.

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

ITypeBase replaces IEntityType in some APIs

Tracking Issue #13947

Old behavior

Previously, all mapped structural types were entity types.

New behavior

With the introduction of complex types in EF8, some APIs that were previously use an IEntityType now use ITypeBase so that the APIs can be used with either entity or complex types. This includes:

  • IProperty.DeclaringEntityType is now obsolete and IProperty.DeclaringType should be used instead.
  • IEntityTypeIgnoredConvention is now obsolete and ITypeIgnoredConvention should be used instead.
  • IValueGeneratorSelector.Select now accepts an ITypeBase which may be, but does not have to be an IEntityType.

Why

With the introduction of complex types in EF8, these APIs can be used with either IEntityType or IComplexType.

Mitigations

The old APIs are obsoleted, but will not be removed until EF10. Code should be updated to use the new APIs ASAP.

ValueConverter and ValueComparer expressions must use public APIs for the compiled model

Tracking Issue #24896

Old behavior

Previously, ValueConverter and ValueComparer definitions were not included in the compiled model, and so could contain arbitrary code.

New behavior

EF now extracts the expressions from the ValueConverter and ValueComparer objects and includes these C# in the compiled model. This means that these expressions must only use public API.

Why

The EF team is gradually moving more constructs into the compiled model to support using EF Core with AOT in the future.

Mitigations

Make the APIs used by the comparer public. For example, consider this simple converter:

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
}

To use this converter in a compiled model with EF8, the ConvertToString and ConvertToBytes methods must be made public. For example:

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 no longer excludes other tables in a TPC hierarchy

Tracking Issue #30079

Old behavior

Previously, using ExcludeFromMigrations on a table in a TPC hierarchy would also exclude other tables in the hierarchy.

New behavior

Starting with EF Core 8.0, ExcludeFromMigrations does not impact other tables.

Why

The old behavior was a bug and prevented migrations from being used to manage hierarchies across projects.

Mitigations

Use ExcludeFromMigrations explicitly on any other table that should be excluded.

Non-shadow integer keys are persisted to Cosmos documents

Tracking Issue #31664

Old behavior

Previously, non-shadow integer properties that match the criteria to be a synthesized key property would not be persisted into the JSON document, but were instead re-synthesized on the way out.

New behavior

Starting with EF Core 8.0, these properties are now persisted.

Why

The old behavior was a bug and prevented properties that match the synthesized key criteria from being persisted to Cosmos.

Mitigations

Exclude the property from the model if its value should not be persisted.