Breaking changes in EF Core 7.0 (EF7)
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:
Target Framework
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.
Summary
High-impact changes
Encrypt
defaults to true
for SQL Server connections
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.
Old behavior
SqlClient connection strings use Encrypt=False
by default. This allows connections on development machines where the local server does not have a valid certificate.
New behavior
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.)
Why
This change was made to ensure that, by default, either the connection is secure or the application will fail to connect.
Mitigations
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 mechanism. - Explicitly add
Encrypt=False
to the connection string.
Warning
Options 2 and 3 both leave the server in a potentially insecure state.
Some warnings throw exceptions by default again
Old behavior
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 |
---|---|
RelationalEventId.AmbientTransactionWarning | An application may have expected an ambient transaction to be used when it was actually ignored. |
RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable | An index specifies properties some of which are mapped and some of which are not mapped to a column in a table. |
RelationalEventId.IndexPropertiesMappedToNonOverlappingTables | An index specifies properties which map to columns on non-overlapping tables. |
RelationalEventId.ForeignKeyPropertiesMappedToUnrelatedTables | A foreign key specifies properties which don't map to the related tables. |
New behavior
Starting with EF Core 7.0, these warnings again, by default, result in an exception being thrown.
Why
These are issues that very likely indicate an error in the application code that should be fixed.
Mitigations
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
Old behavior
Previous versions of the SQL Server provider saved changes via a less efficient technique which always worked.
New behavior
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.
Why
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.
Mitigations
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:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.ToTable(tb => tb.HasTrigger("SomeTrigger"));
}
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:
public class BlankTriggerAddingConvention : IModelFinalizingConvention
{
public virtual void ProcessModelFinalizing(
IConventionModelBuilder modelBuilder,
IConventionContext<IConventionModelBuilder> context)
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
var table = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table);
if (table != null
&& entityType.GetDeclaredTriggers().All(t => t.GetDatabaseName(table.Value) == null)
&& (entityType.BaseType == null
|| entityType.GetMappingStrategy() != RelationalAnnotationNames.TphMappingStrategy))
{
entityType.Builder.HasTrigger(table.Value.Name + "_Trigger");
}
foreach (var fragment in entityType.GetMappingFragments(StoreObjectType.Table))
{
if (entityType.GetDeclaredTriggers().All(t => t.GetDatabaseName(fragment.StoreObject) == null))
{
entityType.Builder.HasTrigger(fragment.StoreObject.Name + "_Trigger");
}
}
}
}
}
Use the convention on your DbContext
by overriding ConfigureConventions
:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(_ => new BlankTriggerAddingConvention());
}
This effectively calls HasTrigger
on all your model's tables, instead of you having to do it manually for each and every table.
SQLite tables with AFTER triggers and virtual tables now require special EF Core configuration
Old behavior
Previous versions of the SQLite provider saved changes via a less efficient technique which always worked.
New behavior
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.
Why
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.
Mitigations
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>();
Medium-impact changes
Orphaned dependents of optional relationships are not automatically deleted
Old behavior
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.
New behavior
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.
Why
The dependent can exist without any relationship to a principal, so severing the relationship should not cause the entity to be deleted.
Mitigations
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;
}
}
};
Cascade delete is configured between tables when using TPT mapping with SQL Server
Old behavior
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.
New behavior
Starting with EF Core 7.0, the cascade deletes are now being created for SQL Server just as they always were for other databases.
Why
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.
Mitigations
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);
Higher chance of busy/locked errors on SQLite when not using write-ahead logging
Old behavior
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.
New behavior
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.
Why
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.
Mitigations
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>();
Low-impact changes
Key properties may need to be configured with a provider value comparer
Old behavior
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.
New behavior
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.
Why
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.
Mitigations
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);
Check constraints and other table facets are now configured on the table
Old behavior
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();
New behavior
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.
Why
These facets apply to tables only. They will not be applied to any mapped views, functions, or stored procedures.
Mitigations
Use the table builder methods, as shown above.
Navigations from new entities to deleted entities are not fixed up
Old behavior
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.
New behavior
Starting with EF Core 7.0, navigations to and from Deleted
entities are not fixed up.
Why
Once an entity is marked as Deleted
it rarely makes sense to associate it with non-deleted entities.
Mitigations
Query or attach entities before marking entities as Deleted
, or manually set navigation properties to and from the deleted entity.
Using FromSqlRaw
and related methods from the wrong provider throws use-the-correct-method
Old behavior
In EF Core 6.0, using the Azure Cosmos DB FromSqlRaw extension method when using a relational provider, or the relational FromSqlRaw 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.
New behavior
Starting with EF Core 7.0, using an extension method designed for one provider on a different provider will throw an exception.
Why
The correct extension method must be used for it to function correctly in all situations.
Mitigations
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();
Scaffolded OnConfiguring
no longer calls IsConfigured
Old behavior
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");
}
}
New behavior
Starting with EF Core 7.0, the call to IsConfigured
is no longer included.
Why
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.
Mitigations
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
.