Breaking changes in EF Core 9 (EF9)
This page documents API and behavior changes that have the potential to break existing applications updating from EF Core 8 to EF Core 9. Make sure to review earlier breaking changes if updating from an earlier version of EF Core:
Target Framework
EF Core 9 targets .NET 8. This means that existing applications that target .NET 8 can continue to do so. Applications targeting older .NET, .NET Core, and .NET Framework versions will need to target .NET 8 or .NET 9 to use EF Core 9.
Summary
Note
If you are using Azure Cosmos DB, please see the separate section below on Azure Cosmos DB breaking changes.
High-impact changes
Exception is thrown when applying migrations if there are pending model changes
Old behavior
If the model has pending changes compared to the last migration they are not applied with the rest of the migrations when Migrate
is called.
New behavior
Starting with EF Core 9.0, if the model has pending changes compared to the last migration an exception is thrown when dotnet ef database update
, Migrate
or MigrateAsync
is called:
The model for context 'DbContext' has pending changes. Add a new migration before updating the database. This exception can be suppressed or logged by passing event ID 'RelationalEventId.PendingModelChangesWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.
Why
Forgetting to add a new migration after making model changes is a common mistake that can be hard to diagnose in some cases. The new exception ensures that the app's model matches the database after the migrations are applied.
Mitigations
There are several common situations when this exception can be thrown:
- There are no migrations at all. This is common when the database is updated through other means.
- Mitigation: If you don't plan to use migrations for managing the database schema then remove the
Migrate
orMigrateAsync
call, otherwise add a migration.
- Mitigation: If you don't plan to use migrations for managing the database schema then remove the
- There is at least one migration, but the model snapshot is missing. This is common for migrations created manually.
- Mitigation: Add a new migration using EF tooling, this will update the model snapshot.
- The model wasn't modified by the developer, but it's built in a non-deterministic way causing EF to detect it as modified. This is common when
new DateTime()
,DateTime.Now
,DateTime.UtcNow
, orGuid.NewGuid()
are used in objects supplied toHasData()
.- Mitigation: Add a new migration, examine its contents to locate the cause, and replace the dynamic data with a static, hardcoded value in the model. The migration should be recreated after the model is fixed. If dynamic data has to be used for seeding consider using the new seeding pattern instead of
HasData()
.
- Mitigation: Add a new migration, examine its contents to locate the cause, and replace the dynamic data with a static, hardcoded value in the model. The migration should be recreated after the model is fixed. If dynamic data has to be used for seeding consider using the new seeding pattern instead of
- The last migration was created for a different provider than the one used to apply the migrations.
- Mitigation: This is an unsupported scenario. The warning can be suppressed using the code snippet below, but this scenario will likely stop working in a future EF Core release. The recommended solution is to generate a separate set of migrations for each provider.
- The migrations are generated or choosen dynamically by replacing some of the EF services.
Mitigation: The warning is a false positive in this case and should be suppressed:
options.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
If your scenario doesn't fall under any of the above cases and adding a new migration creates the same migration each time or an empty migration and the exception is still thrown then create a small repro project and share it with the EF team in a new issue.
Low-impact changes
EF.Functions.Unhex()
now returns byte[]?
Old behavior
The EF.Functions.Unhex()
function was previously annotated to return byte[]
.
New behavior
Starting with EF Core 9.0, Unhex() is now annotated to return byte[]?
.
Why
Unhex()
is translated to the SQLite unhex
function, which returns NULL for invalid inputs. As a result, Unhex()
returned null
for those cases, in violation of the annotation.
Mitigations
If you are sure that the text content passed to Unhex()
represents a valid, hexadecimal string, you can simply add the null-forgiving operator as an assertion that the invocation will never return null:
var binaryData = await context.Blogs.Select(b => EF.Functions.Unhex(b.HexString)!).ToListAsync();
Otherwise, add runtime checks for null on the return value of Unhex().
SqlFunctionExpression's nullability arguments' arity validated
Old behavior
Previously it was possible to create a SqlFunctionExpression
with a different number of arguments and nullability propagation arguments.
New behavior
Starting with EF Core 9.0, EF now throws if the number of arguments and nullability propagation arguments do not match.
Why
Not having matching number of arguments and nullability propagation arguments can lead to unexpected behavior.
Mitigations
Make sure the argumentsPropagateNullability
has same number of elements as the arguments
. When in doubt use false
for nullability argument.
ToString()
method now returns empty string for null
instances
Old behavior
Previously EF returned inconsistent results for the ToString()
method when the argument value was null
. E.g. ToString()
on bool?
property with null
value returned null
, but for non-property bool?
expressions whose value was null
it returned True
. The behavior was also inconsistent for other data types, e.g. ToString()
on null
value enum returned empty string.
New behavior
Starting with EF Core 9.0, the ToString()
method now consistently returns empty string in all cases when the argument value is null
.
Why
The old behavior was inconsistent across different data types and situations, as well as not aligned with the C# behavior.
Mitigations
To revert to the old behavior, rewrite the query accordingly:
var newBehavior = context.Entity.Select(x => x.NullableBool.ToString());
var oldBehavior = context.Entity.Select(x => x.NullableBool == null ? null : x.NullableBool.ToString());
Shared framework dependencies were updated to 9.0.x
Old behavior
Apps using the Microsoft.NET.Sdk.Web
SDK and targetting net8.0 would resolve packages like System.Text.Json
, Microsoft.Extensions.Caching.Memory
, Microsoft.Extensions.Configuration.Abstractions
, Microsoft.Extensions.Logging
and Microsoft.Extensions.DependencyModel
from the shared framework, so these assemblies wouldn't normally be deployed with the app.
New behavior
While EF Core 9.0 still supports net8.0 it now references the 9.0.x versions of System.Text.Json
, Microsoft.Extensions.Caching.Memory
, Microsoft.Extensions.Configuration.Abstractions
, Microsoft.Extensions.Logging
and Microsoft.Extensions.DependencyModel
. Apps targetting net8.0 will not be able to leverage the shared framework to avoid deploying these assemblies.
Why
The matching dependency versions contain the latest security fixes and using them simplifies the servicing model for EF Core.
Mitigations
Change your app to target net9.0 to get the previous behavior.
Azure Cosmos DB breaking changes
Extensive work has gone into making the Azure Cosmos DB provider better in 9.0. The changes include a number of high-impact breaking changes; if you are upgrading an existing application, please read the following carefully.
High-impact changes
The discriminator property is now named $type
instead of Discriminator
Old behavior
EF automatically adds a discriminator property to JSON documents to identify the entity type that the document represents. In previous versions of EF, this JSON property used to be named Discriminator
by default.
New behavior
Starting with EF Core 9.0, the discriminator property is now called $type
by default. If you have existing documents in Azure Cosmos DB from previous versions of EF, these use the old Discriminator
naming, and after upgrading to EF 9.0, queries against those documents will fail.
Why
An emerging JSON practice uses a $type
property in scenarios where a document's type needs to be identified. For example, .NET's System.Text.Json also supports polymorphism, using $type
as its default discriminator property name (docs). To align with the rest of the ecosystem and make it easier to interoperate with external tools, the default was changed.
Mitigations
The easiest mitigation is to simply configure the name of the discriminator property to be Discriminator
, just as before:
modelBuilder.Entity<Session>().HasDiscriminator<string>("Discriminator");
Doing this for all your top-level entity types will make EF behave just like before.
At this point, if you wish, you can also update all your documents to use the new $type
naming.
The id
property now contains only the EF key property by default
Old behavior
Previously, EF inserted the discriminator value of your entity type into the id
property of the document. For example, if you saved a Blog
entity type with an Id
property containing 8, the JSON id
property would contain Blog|8
.
New behavior
Starting with EF Core 9.0, the JSON id
property no longer contains the discriminator value, and only contains the value of your key property. For the above example, the JSON id
property would simply be 8
. If you have existing documents in Azure Cosmos DB from previous versions of EF, these have the discriminator value in the JSON id
property, and after upgrading to EF 9.0, queries against those documents will fail.
Why
Since the JSON id
property must be unique, the discriminator was previously added to it to allow different entities with the same key value to exist. For example, this allowed having both a Blog
and a Post
with an Id
property containing the value 8 within the same container and partition. This aligned better with relational database data modeling patterns, where each entity type is mapped to its own table, and therefore has its own key-space.
EF 9.0 generally changed the mapping to be more aligned with common Azure Cosmos DB NoSQL practices and expectations, rather than to correspond to the expectations of users coming from relational databases. In addition, having the discriminator value in the id
property made it more difficult for external tools and systems to interact with EF-generated JSON documents; such external systems aren't generally aware of the EF discriminator values, which are by default derived from .NET types.
Mitigations
The easiest mitigation is to simply configure EF to include the discriminator in the JSON id
property, as before. A new configuration option has been introduced for this purpose:
modelBuilder.Entity<Session>().HasDiscriminatorInJsonId();
Doing this for all your top-level entity types will make EF behave just like before.
At this point, if you wish, you can also update all your documents to rewrite their JSON id
property. Note that this is only possible if entities of different types don't share the same id value within the same container.
Medium-impact changes
Sync I/O via the Azure Cosmos DB provider is no longer supported
Old behavior
Previously, calling synchronous methods like ToList
or SaveChanges
would cause EF Core to block synchronously using .GetAwaiter().GetResult()
when executing async calls against the Azure Cosmos DB SDK. This can result in deadlock.
New behavior
Starting with EF Core 9.0, EF now throws by default when attempting to use synchronous I/O. The exception message is "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."
Why
Synchronous blocking on asynchronous methods can result in deadlock, and the Azure Cosmos DB SDK only supports async methods.
Mitigations
In EF Core 9.0, the error can be suppressed with:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.ConfigureWarnings(w => w.Ignore(CosmosEventId.SyncNotSupported));
}
That being said, applications should stop using sync APIs with Azure Cosmos DB since this is not supported by the Azure Cosmos DB SDK. The ability to suppress the exception will be removed in a future release of EF Core, after which the only option will be to use async APIs.
SQL queries must now project JSON values directly
Old behavior
Previously, EF generated queries such as the following:
SELECT c["City"] FROM root c
Such queries cause Azure Cosmos DB to wrap each result in a JSON object, as follows:
[
{
"City": "Berlin"
},
{
"City": "México D.F."
}
]
New behavior
Starting with EF Core 9.0, EF now adds the VALUE
modifier to queries as follows:
SELECT VALUE c["City"] FROM root c
Such queries cause Azure Cosmos DB to return the values directly, without being wrapped:
[
"Berlin",
"México D.F."
]
If your application makes use of SQL queries, such queries are likely broken after upgrading to EF 9.0, as they don't include the VALUE
modifier.
Why
Wrapping each result in an additional JSON object can cause performance degradation in some scenarios, bloats the JSON result payload, and isn't the natural way to work with Azure Cosmos DB.
Mitigations
To mitigate, simply add the VALUE
modifier to the projections of your SQL queries, as shown above.
Undefined results are now automatically filtered from query results
Old behavior
Previously, EF generated queries such as the following:
SELECT c["City"] FROM root c
Such queries cause Azure Cosmos DB to wrap each result in a JSON object, as follows:
[
{
"City": "Berlin"
},
{
"City": "México D.F."
}
]
If any of the results were undefined (e.g. the City
property was absent from the document), an empty document was returned, and EF would return null
for that result.
New behavior
Starting with EF Core 9.0, EF now adds the VALUE
modifier to queries as follows:
SELECT VALUE c["City"] FROM root c
Such queries cause Azure Cosmos DB to return the values directly, without being wrapped:
[
"Berlin",
"México D.F."
]
The Azure Cosmos DB behavior is to automatically filter undefined
values out of results; this means that if one of the City
properties is absent from the document, the query would return just a single result, rather than two results, with one being null
.
Why
Wrapping each result in an additional JSON object can cause performance degradation in some scenarios, bloats the JSON result payload, and isn't the natural way to work with Azure Cosmos DB.
Mitigations
If getting null
values for undefined results is important for your application, coalesce the undefined
values to null
using the new EF.Functions.Coalesce
operator:
var users = await context.Customer
.Select(c => EF.Functions.CoalesceUndefined(c.City, null))
.ToListAsync();
Incorrectly translated queries are no longer translated
Old behavior
Previously, EF translated queries such as the following:
var sessions = await context.Sessions
.Take(5)
.Where(s => s.Name.StartsWith("f"))
.ToListAsync();
However, the SQL translation for this query was incorrect:
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Session") AND STARTSWITH(c["Name"], "f"))
OFFSET 0 LIMIT @__p_0
In SQL, the WHERE
clause is evaluated before the OFFSET
and LIMIT
clauses; but in the LINQ query above, the Take
operator appears before the Where
operator. As a result, such queries could return incorrect results.
New behavior
Starting with EF Core 9.0, such queries are no longer translated, and an exception is thrown.
Why
Incorrect translations can cause silent data corruption, which can introduce hard-to-discover bugs in your application. EF always prefer to fail-fast by throwing up-front rather than to possibly cause data corruption.
Mitigations
If you were happy with the previous behavior and would like to execute the same SQL, simply swap around the order of LINQ operators:
var sessions = await context.Sessions
.Where(s => s.Name.StartsWith("f"))
.Take(5)
.ToListAsync();
Unfortunately, Azure Cosmos DB does not currently support the OFFSET
and LIMIT
clauses in SQL subqueries, which is what the proper translation of the original LINQ query requires.
Low-impact changes
HasIndex
now throws instead of being ignored
Old behavior
Previously, calls to HasIndex were ignored by the EF Cosmos DB provider.
New behavior
The provider now throws if HasIndex is specified.
Why
In Azure Cosmos DB, all properties are indexed by default, and no indexing needs to be specified. While it's possible to define a custom indexing policy, this isn't currently supported by EF, and can be done via the Azure Portal without EF support. Since HasIndex calls weren't doing anything, they are no longer allowed.
Mitigations
Remove any calls to HasIndex.
IncludeRootDiscriminatorInJsonId
was renamed to HasRootDiscriminatorInJsonId
after 9.0.0-rc.2
Old behavior
The IncludeRootDiscriminatorInJsonId
API was introduced in 9.0.0 rc.1.
New behavior
For the final release of EF Core 9.0, the API was renamed to HasRootDiscriminatorInJsonId
Why
Another related API was renamed to start with Has
instead of Include
, and so this one was renamed for consistency as well.
Mitigations
If your code is using the IncludeRootDiscriminatorInJsonId
API, simply change it to reference HasRootDiscriminatorInJsonId
instead.