Training
Module
Use a database with minimal API, Entity Framework Core, and ASP.NET Core - Training
Learn how to add a database to a minimal API application.
This browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
This page documents API and behavior changes that have the potential to break existing applications updating from EF Core 9 to EF Core 10. Make sure to review earlier breaking changes if updating from an earlier version of EF Core:
Note
If you are using Microsoft.Data.Sqlite, please see the separate section below on Microsoft.Data.Sqlite breaking changes.
Breaking change | Impact |
---|---|
SQL Server json data type used by default on Azure SQL and compatibility level 170 | Low |
ExecuteUpdateAsync now accepts a regular, non-expression lambda | Low |
Previously, when mapping primitive collections or owned types to JSON in the database, the SQL Server provider stored the JSON data in an nvarchar(max)
column:
public class Blog
{
// ...
// Primitive collection, mapped to nvarchar(max) JSON column
public string[] Tags { get; set; }
// Owned entity type mapped to nvarchar(max) JSON column
public List<Post> Posts { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().OwnsMany(b => b.Posts, b => b.ToJson());
}
For the above, EF previously generated the following table:
CREATE TABLE [Blogs] (
...
[Tags] nvarchar(max),
[Posts] nvarchar(max)
);
With EF 10, if you configure EF with UseAzureSql (see documentation), or configure EF with a compatibility level of 170 or above (see documentation), EF will map to the new JSON data type instead:
CREATE TABLE [Blogs] (
...
[Tags] json
[Posts] json
);
Although the new JSON data type is the recommended way to store JSON data in SQL Server going forward, there may be some behavioral differences when transitioning from nvarchar(max)
, and some specific querying forms may not be supported. For example, SQL Server does not support the DISTINCT operator over JSON arrays, and queries attempting to do so will fail.
Note that if you have an existing table and are using UseAzureSql, upgrading to EF 10 will cause a migration to be generated which alters all existing nvarchar(max)
JSON columns to json
. This alter operation is supported and should get applied seamlessly and without any issues, but is a non-trivial change to your database.
Note
For 10.0.0 rc1, support for the new JSON data type has been temporarily disabled for Azure SQL Database, due to lacking support. These issues are expected to be resolved by the time EF 10.0 is released, and the JSON data type will become the default until then.
The new JSON data type introduced by SQL Server is a superior, 1st-class way to store and interact with JSON data in the database; it notably brings significant performance improvements (see documentation). All applications using Azure SQL Database or SQL Server 2025 are encouraged to migrate to the new JSON data type.
If you are targeting Azure SQL Database and do not wish to transition to the new JSON data type right away, you can configure EF with a compatibility level lower than 170:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseAzureSql("<connection string>", o => o.UseCompatibilityLevel(160));
}
If you're targeting on-premises SQL Server, the default compatibility level with UseSqlServer
is currently 150 (SQL Server 2019), so the JSON data type is not used.
As an alternative, you can explicitly set the column type on specific properties to be nvarchar(max)
:
public class Blog
{
public string[] Tags { get; set; }
public List<Post> Posts { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().PrimitiveCollection(b => b.Tags).HasColumnType("nvarchar(max)");
modelBuilder.Entity<Blog>().OwnsMany(b => b.Posts, b => b.ToJson().HasColumnType("nvarchar(max)"));
modelBuilder.Entity<Blog>().ComplexProperty(e => e.Posts, b => b.ToJson());
}
Previously, ExecuteUpdate accepted an expression tree argument (Expression<Func<...>>
) for the column setters.
Starting with EF Core 10.0, ExecuteUpdate now accepts a non-expression argument (Func<...>
) for the column setters. If you were building expression trees to dynamically create the column setters argument, your code will no longer compile - but can be replaced with a much simpler alternative (see below).
The fact that the column setters parameter was an expression tree made it quite difficult to do dynamic construction of the column setters, where some setters are only present based on some condition (see Mitigations below for an example).
Code that was building expression trees to dynamically create the column setters argument will need to be rewritten - but the result will be much simpler. For example, let's assume we want to update a Blog's Views, but conditionally also its Name. Since the setters argument was an expression tree, code such as the following needed to be written:
// Base setters - update the Views only
Expression<Func<SetPropertyCalls<Blog>, SetPropertyCalls<Blog>>> setters =
s => s.SetProperty(b => b.Views, 8);
// Conditionally add SetProperty(b => b.Name, "foo") to setters, based on the value of nameChanged
if (nameChanged)
{
var blogParameter = Expression.Parameter(typeof(Blog), "b");
setters = Expression.Lambda<Func<SetPropertyCalls<Blog>, SetPropertyCalls<Blog>>>(
Expression.Call(
instance: setters.Body,
methodName: nameof(SetPropertyCalls<Blog>.SetProperty),
typeArguments: [typeof(string)],
arguments:
[
Expression.Lambda<Func<Blog, string>>(Expression.Property(blogParameter, nameof(Blog.Name)), blogParameter),
Expression.Constant("foo")
]),
setters.Parameters);
}
await context.Blogs.ExecuteUpdateAsync(setters);
Manually creating expression trees is complicated and error-prone, and made this common scenario much more difficult than it should have been. Starting with EF 10, you can now write the following instead:
await context.Blogs.ExecuteUpdateAsync(s =>
{
s.SetProperty(b => b.Views, 8);
if (nameChanged)
{
s.SetProperty(b => b.Name, "foo");
}
});
Previously, when using GetDateTimeOffset
on a textual timestamp that did not have an offset (e.g., 2014-04-15 10:47:16
), Microsoft.Data.Sqlite would assume the value was in the local time zone. I.e. the value was parsed as 2014-04-15 10:47:16+02:00
(assuming local time zone was UTC+2).
Starting with Microsoft.Data.Sqlite 10.0, when using GetDateTimeOffset
on a textual timestamp that does not have an offset, Microsoft.Data.Sqlite will assume the value is in UTC.
Is is to align with SQLite's behavior where timestamps without an offset are treated as UTC.
Code should be adjusted accordingly.
As a last/temporary resort, you can revert to previous behavior by setting Microsoft.Data.Sqlite.Pre10TimeZoneHandling
AppContext switch to true
, see AppContext for library consumers for more details.
AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);
Previously, when writing a DateTimeOffset
value into a REAL column, Microsoft.Data.Sqlite would write the value without taking the offset into account.
Starting with Microsoft.Data.Sqlite 10.0, when writing a DateTimeOffset
value into a REAL column, Microsoft.Data.Sqlite will convert the value to UTC before doing the conversions and writing it.
The value written was incorrect, not aligning with SQLite's behavior where REAL timestamps are asummed to be UTC.
Code should be adjusted accordingly.
As a last/temporary resort, you can revert to previous behavior by setting Microsoft.Data.Sqlite.Pre10TimeZoneHandling
AppContext switch to true
, see AppContext for library consumers for more details.
AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);
Previously, when using GetDateTime
on a textual timestamp that had an offset (e.g., 2014-04-15 10:47:16+02:00
), Microsoft.Data.Sqlite would return the value with DateTimeKind.Local
(even if the offset was not local). The time was parsed correctly taking the offset into account.
Starting with Microsoft.Data.Sqlite 10.0, when using GetDateTime
on a textual timestamp that has an offset, Microsoft.Data.Sqlite will convert the value to UTC and return it with DateTimeKind.Utc
.
Even though the time was parsed correctly it was dependent on the machine-configured local time zone, which could lead to unexpected results.
Code should be adjusted accordingly.
As a last/temporary resort, you can revert to previous behavior by setting Microsoft.Data.Sqlite.Pre10TimeZoneHandling
AppContext switch to true
, see AppContext for library consumers for more details.
AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);
.NET feedback
.NET is an open source project. Select a link to provide feedback:
Training
Module
Use a database with minimal API, Entity Framework Core, and ASP.NET Core - Training
Learn how to add a database to a minimal API application.
Ask Learn is an AI assistant that can answer questions, clarify concepts, and define terms using trusted Microsoft documentation.
Please sign in to use Ask Learn.
Sign in