title | description | author | ms.date | uid |
---|---|---|---|---|
Breaking changes in EF Core 7.0 (EF7) - EF Core |
Complete list of breaking changes introduced in Entity Framework Core 7.0 (EF7) |
ajcvickers |
09/20/2023 |
core/what-is-new/ef-core-7.0/breaking-changes |
This page documents API and behavior changes that have the potential to break existing applications updating from EF Core 6 to EF Core 7. Make sure to review earlier breaking changes if updating from an earlier version of EF Core:
- Breaking changes in EF Core 6
EF Core 7.0 targets .NET 6. This means that existing applications that target .NET 6 can continue to do so. Applications targeting older .NET, .NET Core, and .NET Framework versions will need to target .NET 6 or .NET 7 to use EF Core 7.0.
Tracking Issue: SqlClient #1210
Important
This is a severe breaking change in the Microsoft.Data.SqlClient package. There is nothing that can be done in EF Core to revert or mitigate this change. Please direct feedback to the Microsoft.Data.SqlClient GitHub Repo or contact a Microsoft Support Professional for additional questions or help.
SqlClient connection strings use Encrypt=False
by default. This allows connections on development machines where the local server does not have a valid certificate.
SqlClient connection strings use Encrypt=True
by default. This means that:
- The server must be configured with a valid certificate
- The client must trust this certificate
If these conditions are not met, then a SqlException
will be thrown. For example:
A connection was successfully established with the server, but then an error occurred during the login process. (provider: SSL Provider, error: 0 - The certificate chain was issued by an authority that is not trusted.)
This change was made to ensure that, by default, either the connection is secure or the application will fail to connect.
There are three ways to proceed:
- Install a valid certificate on the server. Note that this is an involved process and requires obtaining a certificate and ensuring it is signed by an authority trusted by the client.
- If the server has a certificate, but it is not trusted by the client, then
TrustServerCertificate=True
to allow bypassing the normal trust mechanims. - Explicitly add
Encrypt=False
to the connection string.
Warning
Options 2 and 3 both leave the server in a potentially insecure state.
In EF Core 6.0, a bug in the SQL Server provider meant that some warnings that are configured to throw exceptions by default were instead being logged but not throwing exceptions. These warnings are:
EventId | Description |
---|---|
xref:Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.AmbientTransactionWarning?displayProperty=nameWithType | An application may have expected an ambient transaction to be used when it was actually ignored. |
xref:Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable?displayProperty=nameWithType | An index specifies properties some of which are mapped and some of which are not mapped to a column in a table. |
xref:Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.IndexPropertiesMappedToNonOverlappingTables?displayProperty=nameWithType | An index specifies properties which map to columns on non-overlapping tables. |
xref:Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.ForeignKeyPropertiesMappedToUnrelatedTables?displayProperty=nameWithType | A foreign key specifies properties which don't map to the related tables. |
Starting with EF Core 7.0, these warnings again, by default, result in an exception being thrown.
These are issues that very likely indicate an error in the application code that should be fixed.
Fix the underlying issue that is the reason for the warning.
Alternately, the warning level can be changed so that it is logged only or suppressed entirely. For example:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.ConfigureWarnings(b => b.Ignore(RelationalEventId.AmbientTransactionWarning));
SQL Server tables with triggers or certain computed columns now require special EF Core configuration
Previous versions of the SQL Server provider saved changes via a less efficient technique which always worked.
By default, EF Core now saves changes via a significantly more efficient technique; unfortunately, this technique is not supported on SQL Server if the target table has database triggers, or certain types of computed columns. See the SQL Server documentation for more details.
The performance improvements linked to the new method are significant enough that it's important to bring them to users by default. At the same time, we estimate usage of database triggers or the affected computed columns in EF Core applications to be low enough that the negative breaking change consequences are outweighed by the performance gain.
Starting with EF Core 8.0, the use or not of the "OUTPUT" clause can be configured explicitly. For example:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.ToTable(tb => tb.UseSqlOutputClause(false));
}
In EF7 or later, If the target table has a trigger, then you can let EF Core know this, and EF will revert to the previous, less efficient technique. This can be done by configuring the corresponding entity type as follows:
[!code-csharpMain]
Note that doing this doesn't actually make EF Core create or manage the trigger in any way - it currently only informs EF Core that triggers are present on the table. As a result, any trigger name can be used. Specifying a trigger can be used to revert the old behavior even if there isn't actually a trigger in the table.
If most or all of your tables have triggers, you can opt out of using the newer, efficient technique for all your model's tables by using the following model building convention:
[!code-csharpMain]
Use the convention on your DbContext
by overriding ConfigureConventions
:
[!code-csharpMain]
This effectively calls HasTrigger
on all your model's tables, instead of you having to do it manually for each and every table.
Previous versions of the SQLite provider saved changes via a less efficient technique which always worked.
By default, EF Core now saves changes via a more efficient technique, using the RETURNING clause. Unfortunately, this technique is not supported on SQLite if target table is has database AFTER triggers, is virtual, or if older versions of SQLite are being used. See the SQLite documentation for more details.
The simplifications and performance improvements linked to the new method are significant enough that it's important to bring them to users by default. At the same time, we estimate usage of database triggers and virtual tables in EF Core applications to be low enough that the negative breaking change consequences are outweighed by the performance gain.
In EF Core 8.0, the UseSqlReturningClause
method has been introduced to explicitly revert back to the older, less efficient SQL. For example:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.ToTable(tb => tb.UseSqlReturningClause(false));
}
If you are still using EF Core 7.0, then it's possible to revert to the old mechanism for the entire application by inserting the following code in your context configuration:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlite(...)
.ReplaceService<IUpdateSqlGenerator, SqliteLegacyUpdateSqlGenerator>();
A relationship is optional if its foreign key is nullable. Setting the foreign key to null allows the dependent entity exist without any related principal entity. Optional relationships can be configured to use cascade deletes, although this is not the default.
An optional dependent can be severed from its principal by either setting its foreign key to null, or clearing the navigation to or from it. In EF Core 6.0, this would cause the dependent to be deleted when the relationship was configured for cascade delete.
Starting with EF Core 7.0, the dependent is no longer deleted. Note that if the principal is deleted, then the dependent will still be deleted since cascade deletes are configured for the relationship.
The dependent can exist without any relationship to a principal, so severing the relationship should not cause the entity to be deleted.
The dependent can be explicitly deleted:
context.Remove(blog);
Or SaveChanges
can be overridden or intercepted to delete dependents with no principal reference. For example:
context.SavingChanges += (c, _) =>
{
foreach (var entry in ((DbContext)c!).ChangeTracker
.Entries<Blog>()
.Where(e => e.State == EntityState.Modified))
{
if (entry.Reference(e => e.Author).CurrentValue == null)
{
entry.State = EntityState.Deleted;
}
}
};
When mapping an inheritance hierarchy using the TPT strategy, the base table must contain a row for every entity saved, regardless of the actual type of that entity. Deleting the row in the base table should delete rows in all the other tables. EF Core configures a cascade deletes for this.
In EF Core 6.0, a bug in the SQL Server database provider meant that these cascade deletes were not being created.
Starting with EF Core 7.0, the cascade deletes are now being created for SQL Server just as they always were for other databases.
Cascade deletes from the base table to the sub-tables in TPT allow an entity to be deleted by deleting its row in the base table.
In most cases, this change should not cause any issues. However, SQL Server is very restrictive when there are multiple cascade behaviors configured between tables. This means that if there is an existing cascading relationship between tables in the TPT mapping, then SQL Server may generate the following error:
Microsoft.Data.SqlClient.SqlException: The DELETE statement conflicted with the REFERENCE constraint "FK_Blogs_People_OwnerId". The conflict occurred in database "Scratch", table "dbo.Blogs", column 'OwnerId'. The statement has been terminated.
For example, this model creates a cycle of cascading relationships:
[Table("FeaturedPosts")]
public class FeaturedPost : Post
{
public int ReferencePostId { get; set; }
public Post ReferencePost { get; set; } = null!;
}
[Table("Posts")]
public class Post
{
public int Id { get; set; }
public string? Title { get; set; }
public string? Content { get; set; }
}
One of these will need to be configured to not use cascade deletes on the server. For example, to change the explicit relationship:
modelBuilder
.Entity<FeaturedPost>()
.HasOne(e => e.ReferencePost)
.WithMany()
.OnDelete(DeleteBehavior.ClientCascade);
Or to change the implicit relationship created for the TPT mapping:
modelBuilder
.Entity<FeaturedPost>()
.HasOne<Post>()
.WithOne()
.HasForeignKey<FeaturedPost>(e => e.Id)
.OnDelete(DeleteBehavior.ClientCascade);
Previous versions of the SQLite provider saved changes via a less efficient technique which was able to automatically retry when the table was locked/busy and write-ahead logging (WAL) was not enabled.
By default, EF Core now saves changes via a more efficient technique, using the RETURNING clause. Unfortunately, this technique is not able to automatically retry when busy/locked. In a multi-threaded application (like a web app) not using write-ahead logging, it is common to encounter these errors.
The simplifications and performance improvements linked to the new method are significant enough that it's important to bring them to users by default. Databases created by EF Core also enable write-ahead logging by default. The SQLite team also recommends enabling write-ahead logging by default.
If possible, you should enable write-ahead logging on your database. If your database was created by EF, this should already be the case. If not, you can enable write-ahead logging by executing the following command.
PRAGMA journal_mode = 'wal';
If, for some reason, you can't enable write-ahead logging, it's possible to revert to the old mechanism for the entire application by inserting the following code in your context configuration:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlite(...)
.ReplaceService<IUpdateSqlGenerator, SqliteLegacyUpdateSqlGenerator>();
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(_ => new DoNotUseReturningClauseConvention());
}
class DoNotUseReturningClauseConvention : IModelFinalizingConvention
{
public void ProcessModelFinalizing(
IConventionModelBuilder modelBuilder,
IConventionContext<IConventionModelBuilder> context)
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
entityType.UseSqlReturningClause(false);
}
}
}
In EF Core 6.0, key values taken directly from the properties of entity types were used for comparison of key values when saving changes. This would make use of any custom value comparer configured on these properties.
Starting with EF Core 7.0, database values are used for these comparisons. This "just works" for the vast majority of cases. However, if the properties were using a custom comparer, and that comparer cannot be applied to the database values, then a "provider value comparer" may be needed, as shown below.
Various entity-splitting and table-splitting can result in multiple properties mapped to the same database column, and vice-versa. This requires values to be compared after conversion to value that will be used in the database.
Configure a provider value comparer. For example, consider the case where a value object is being used as a key, and the comparer for that key uses case-insensitive string comparisons:
var blogKeyComparer = new ValueComparer<BlogKey>(
(l, r) => string.Equals(l.Id, r.Id, StringComparison.OrdinalIgnoreCase),
v => v.Id.ToUpper().GetHashCode(),
v => v);
var blogKeyConverter = new ValueConverter<BlogKey, string>(
v => v.Id,
v => new BlogKey(v));
modelBuilder.Entity<Blog>()
.Property(e => e.Id).HasConversion(
blogKeyConverter, blogKeyComparer);
The database values (strings) cannot directly use the comparer defined for BlogKey
types. Therefore, a provider comparer for case-insensitive string comparisons must be configured:
var caseInsensitiveComparer = new ValueComparer<string>(
(l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase),
v => v.ToUpper().GetHashCode(),
v => v);
var blogKeyComparer = new ValueComparer<BlogKey>(
(l, r) => string.Equals(l.Id, r.Id, StringComparison.OrdinalIgnoreCase),
v => v.Id.ToUpper().GetHashCode(),
v => v);
var blogKeyConverter = new ValueConverter<BlogKey, string>(
v => v.Id,
v => new BlogKey(v));
modelBuilder.Entity<Blog>()
.Property(e => e.Id).HasConversion(
blogKeyConverter, blogKeyComparer, caseInsensitiveComparer);
In EF Core 6.0, HasCheckConstraint
, HasComment
, and IsMemoryOptimized
were called directly on the entity type builder. For example:
modelBuilder
.Entity<Blog>()
.HasCheckConstraint("CK_Blog_TooFewBits", "Id > 1023");
modelBuilder
.Entity<Blog>()
.HasComment("It's my table, and I'll delete it if I want to.");
modelBuilder
.Entity<Blog>()
.IsMemoryOptimized();
Starting with EF Core 7.0, these methods are instead called on the table builder:
modelBuilder
.Entity<Blog>()
.ToTable(b => b.HasCheckConstraint("CK_Blog_TooFewBits", "Id > 1023"));
modelBuilder
.Entity<Blog>()
.ToTable(b => b.HasComment("It's my table, and I'll delete it if I want to."));
modelBuilder
.Entity<Blog>()
.ToTable(b => b.IsMemoryOptimized());
The existing methods have been marked as Obsolete
. They currently have the same behavior as the new methods, but will be removed in a future release.
These facets apply to tables only. They will not be applied to any mapped views, functions, or stored procedures.
Use the table builder methods, as shown above.
In EF Core 6.0, when a new entity is tracked either from a tracking query or by attaching it to the DbContext
, then navigations to and from related entities in the Deleted
state are fixed up.
Starting with EF Core 7.0, navigations to and from Deleted
entities are not fixed up.
Once an entity is marked as Deleted
it rarely makes sense to associate it with non-deleted entities.
Query or attach entities before marking entities as Deleted
, or manually set navigation properties to and from the deleted entity.
In EF Core 6.0, using the Azure Cosmos DB xref:Microsoft.EntityFrameworkCore.CosmosQueryableExtensions.FromSqlRaw%2A extension method when using a relational provider, or the relational xref:Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSqlRaw%2A extension method when using the Azure Cosmos DB provider could silently fail. Likewise, using relational methods on the in-memory provider is a silent no-op.
Starting with EF Core 7.0, using an extension method designed for one provider on a different provider will throw an exception.
The correct extension method must be used for it to function correctly in all situations.
Use the correct extension method for the provider being used. If multiple providers are referenced, then call the extension method as a static method. For example:
var result = CosmosQueryableExtensions.FromSqlRaw(context.Blogs, "SELECT ...").ToList();
Or:
var result = RelationalQueryableExtensions.FromSqlRaw(context.Blogs, "SELECT ...").ToList();
In EF Core 6.0, the DbContext
type scaffolded from an existing database contained a call to IsConfigured
. For example:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
optionsBuilder.UseNpgsql("MySecretConnectionString");
}
}
Starting with EF Core 7.0, the call to IsConfigured
is no longer included.
There are very limited scenarios where the database provider is configured inside your DbContext in some cases, but only if the context is not configured already. Instead, leaving OnConfiguring
here makes it more likely that a connection string containing sensitive information is left in the code, despite the compile-time warning. Thus the extra safely and cleaner code from removing this was deemed worthwhile, especially given that the --no-onconfiguring
(.NET CLI) or -NoOnConfiguring
(Visual Studio Package Manager Console) flag can be used to prevent scaffolding of the OnConfiguring
method, and that customizable templates exist to add back IsConfigured
if it is really needed.
Either:
- Use the
--no-onconfiguring
(.NET CLI) or-NoOnConfiguring
(Visual Studio Package Manager Console) argument when scaffolding from an existing database. - Customize the T4 templates to add back the call to
IsConfigured
.