From e4667ae0d49af4cdbeb65b183b1c73f1b795548f Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Tue, 20 Sep 2022 21:53:26 +0100 Subject: [PATCH] EF7 Breaking changes Fixes #3751 Fixes #4019 Fixes #3801 Fixes #3883 Part of #3915 Fixes #4010 Fixes #4029 Fixes #4039 Fixes #4047 --- .../core/change-tracking/entity-entries.md | 2 +- .../core/providers/sql-server/misc.md | 10 +- .../ef-core-7.0/breaking-changes.md | 392 +++++++++++++++++- .../CaseInsensitiveStrings.cs | 2 +- .../core/SqlServer/Misc/TriggersContext.cs | 39 ++ samples/core/SqlServer/SqlServer.csproj | 2 +- 6 files changed, 435 insertions(+), 12 deletions(-) diff --git a/entity-framework/core/change-tracking/entity-entries.md b/entity-framework/core/change-tracking/entity-entries.md index 1b3c33b44d..b425045499 100644 --- a/entity-framework/core/change-tracking/entity-entries.md +++ b/entity-framework/core/change-tracking/entity-entries.md @@ -10,7 +10,7 @@ uid: core/change-tracking/entity-entries There are four main APIs for accessing entities tracked by a : -- returns an instance for a given entity instance. +- returns an instance for a given entity instance. - returns instances for all tracked entities, or for all tracked entities of a given type. - , , , and find a single entity by primary key, first looking in tracked entities, and then querying the database if needed. - returns actual entities (not EntityEntry instances) for entities of the entity type represented by the DbSet. diff --git a/entity-framework/core/providers/sql-server/misc.md b/entity-framework/core/providers/sql-server/misc.md index d96653c9a7..36e85a0f09 100644 --- a/entity-framework/core/providers/sql-server/misc.md +++ b/entity-framework/core/providers/sql-server/misc.md @@ -2,7 +2,7 @@ title: Miscellaneous - Microsoft SQL Server Database Provider - EF Core description: Miscellaneous for the Microsoft SQL Server database provider author: roji -ms.date: 06/07/2021 +ms.date: 09/20/2022 uid: core/providers/sql-server/misc --- # Miscellaneous notes for SQL Server @@ -16,3 +16,11 @@ You can let EF Core know that the target table has a trigger; doing so will reve [!code-csharp[Main](../../../../samples/core/SqlServer/Misc/TriggersContext.cs?name=TriggerConfiguration&highlight=4)] 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. + +A model building convention can be used to configure all tables with triggers: + +[!code-csharp[Main](../../../../samples/core/SqlServer/Misc/TriggersContext.cs?name=BlankTriggerAddingConvention)] + +Use the convention on your `DbContext` by overriding `ConfigureConventions`: + +[!code-csharp[Main](../../../../samples/core/SqlServer/Misc/TriggersContext.cs?name=ConfigureConventions)] diff --git a/entity-framework/core/what-is-new/ef-core-7.0/breaking-changes.md b/entity-framework/core/what-is-new/ef-core-7.0/breaking-changes.md index 0235ea2ad4..84c7263d02 100644 --- a/entity-framework/core/what-is-new/ef-core-7.0/breaking-changes.md +++ b/entity-framework/core/what-is-new/ef-core-7.0/breaking-changes.md @@ -1,23 +1,111 @@ --- -title: Breaking changes in EF Core 7.0 - EF Core -description: Complete list of breaking changes introduced in Entity Framework Core 7.0 +title: Breaking changes in EF Core 7.0 (EF7) - EF Core +description: Complete list of breaking changes introduced in Entity Framework Core 7.0 (EF7) author: ajcvickers -ms.date: 12/15/2021 +ms.date: 09/20/2022 uid: core/what-is-new/ef-core-7.0/breaking-changes --- -# Breaking changes in EF Core 7.0 +# Breaking changes in EF Core 7.0 (EF7) -API and behavior changes have the potential to break existing applications updating to EF Core 7.0.0 will be documented here. +API and behavior changes have the potential to break existing applications updating to EF Core 7.0 (EF7) are be documented here. + +## 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 -| **Breaking change** | **Impact** | -|:------------------------------------------------------------------------------------------------------------ | ----------- | -| [SQL Server tables with triggers now require special EF Core configuration](#sqlserver-tables-with-triggers) | High | +| **Breaking change** | **Impact** | +|:-------------------------------------------------------------------------------------------------------------|------------| +| [`Encrypt` defaults to `true` for SQL Server connections](#encrypt-true) | High | +| [Some warnings will again throw exceptions by default](#warnings-as-errors) | High | +| [SQL Server tables with triggers now require special EF Core configuration](#sqlserver-tables-with-triggers) | High | +| [Orphaned dependents of optional relationships are not automatically deleted](#optional-deletes) | Medium | +| [Cascade delete is configured between tables when using TPT mapping with SQL Server](#tpt-cascade-delete) | Medium | +| [Key properties may need to be configured with a provider value comparer](#provider-value-comparer) | Low | +| [Check constraints and other table facets are now configured on the table](#table-configuration) | Low | +| [Navigations from new entities to deleted entities are not fixed up](#deleted-fixup) | Low | +| [Using `FromSqlRaw` and related methods from the wrong provider throws](#use-the-correct-method) | Low | ## High-impact changes + + +### `Encrypt` defaults to `true` for SQL Server connections + +[Tracking Issue: SqlClient #1210](https://github.com/dotnet/SqlClient/pull/1210) + +> [!IMPORTANT] +> This is a severe breaking change in the [Microsoft.Data.SqlClient](https://www.nuget.org/packages/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](https://github.com/dotnet/SqlClient) or contact a [Microsoft Support Professional](https://support.serviceshub.microsoft.com/supportforbusiness/onboarding?origin=/supportforbusiness/create) for additional questions or help. + +#### Old behavior + +[SqlClient connection strings](/sql/connect/ado-net/connection-string-syntax) 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](/sql/connect/ado-net/connection-string-syntax) 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 nol 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: + +1. [Install a valid certificate on the server](/sql/database-engine/configure-windows/enable-encrypted-connections-to-the-database-engine). Note that this is an involved process and requires a obtaining a certificate and ensuring it is signed by an authority trusted by the client. +2. If the server has a certificate, but it is not trusted by the client, then `TrustServerCertificate=True` to allow bypassing the normal trust mechanims. +3. Explicitly add `Encrypt=False` to the connection string. + +> [!WARNING] +> Options 2 and three both leave the server in a potentially insecure state. + + + +### Some warnings will again throw exceptions by default + +[Tracking Issue #29069](https://github.com/dotnet/efcore/issues/29069) + +#### 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 | +|----------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| | An application may have expected an ambient transaction to be used when it was actually ignored. | +| | An index specifies properties some of which are mapped and some of which are not mapped to a column in a table. | +| | An index specifies properties which map to columns on non-overlapping tables. | +| | A foreign key specifies properties which don't map to the related tables. | + +#### New behavior + +Starting with EF Core 7.0, these warnings will 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](xref:core/logging-events-diagnostics/extensions-logging#configuration-for-specific-messages). For example: + +```csharp +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .ConfigureWarnings(b => b.Ignore(RelationalEventId.AmbientTransactionWarning)); +``` + ### SQL Server tables with triggers now require special EF Core configuration @@ -43,3 +131,291 @@ You can let EF Core know that the target table has a trigger; doing so will reve [!code-csharp[Main](../../../../samples/core/SqlServer/Misc/TriggersContext.cs?name=TriggerConfiguration&highlight=4)] 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. + +A model building convention can be used to configure all tables with triggers: + +[!code-csharp[Main](../../../../samples/core/SqlServer/Misc/TriggersContext.cs?name=BlankTriggerAddingConvention)] + +Use the convention on your `DbContext` by overriding `ConfigureConventions`: + +[!code-csharp[Main](../../../../samples/core/SqlServer/Misc/TriggersContext.cs?name=ConfigureConventions)] + +## Medium-impact changes + + + +### Orphaned dependents of optional relationships are not automatically deleted + +[Tracking Issue #27217](https://github.com/dotnet/efcore/issues/27217) + +#### Old behavior + +A [relationship is optional](xref:core/modeling/relationships) 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](xref:core/saving/cascade-delete), 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 live 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: + +```csharp +context.Remove(blog); +``` + +Or `SaveChanges` can be overridden or intercepted to delete dependents with no principal reference. For example: + +```csharp +context.SavingChanges += (c, _) => + { + foreach (var entry in ((DbContext)c!).ChangeTracker + .Entries() + .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 + +[Tracking Issue #28532](https://github.com/dotnet/efcore/issues/28532) + +#### Old behavior + +When [mapping an inheritance hierarchy using the TPT strategy](xref:core/modeling/inheritance), 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](xref:core/saving/cascade-delete) 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: + +```csharp +[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: + +```csharp +modelBuilder + .Entity() + .HasOne(e => e.ReferencePost) + .WithMany() + .OnDelete(DeleteBehavior.ClientCascade); +``` + +Or to change the implicit relationship created for the TPT mapping: + +```csharp +modelBuilder + .Entity() + .HasOne() + .WithOne() + .HasForeignKey(e => e.Id) + .OnDelete(DeleteBehavior.ClientCascade); +``` + +## Low-impact changes + + + +### Key properties may need to be configured with a provider value comparer + +[Tracking Issue #27738](https://github.com/dotnet/efcore/issues/27738) + +#### 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](xref:core/modeling/value-comparers) 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: + +```csharp +var blogKeyComparer = new ValueComparer( + (l, r) => string.Equals(l.Id, r.Id, StringComparison.OrdinalIgnoreCase), + v => v.Id.ToUpper().GetHashCode(), + v => v); + +var blogKeyConverter = new ValueConverter( + v => v.Id, + v => new BlogKey(v)); + +modelBuilder.Entity() + .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: + +```csharp +var caseInsensitiveComparer = new ValueComparer( + (l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase), + v => v.ToUpper().GetHashCode(), + v => v); + +var blogKeyComparer = new ValueComparer( + (l, r) => string.Equals(l.Id, r.Id, StringComparison.OrdinalIgnoreCase), + v => v.Id.ToUpper().GetHashCode(), + v => v); + +var blogKeyConverter = new ValueConverter( + v => v.Id, + v => new BlogKey(v)); + +modelBuilder.Entity() + .Property(e => e.Id).HasConversion( + blogKeyConverter, blogKeyComparer, caseInsensitiveComparer); +``` + + + +### Check constraints and other table facets are now configured on the table + +[Tracking Issue #28205](https://github.com/dotnet/efcore/issues/28205) + +#### Old behavior + +In EF Core 6.0, `HasCheckConstraint`, `HasComment`, and `IsMemoryOptimized` were called directly on the entity type builder. For example: + +```csharp +modelBuilder + .Entity() + .HasCheckConstraint("CK_Blog_TooFewBits", "Id > 1023"); + +modelBuilder + .Entity() + .HasComment("It's my table, and I'll delete it if I want to."); + +modelBuilder + .Entity() + .IsMemoryOptimized(); +``` + +#### New behavior + +Starting with EF Core 7.0, these methods are instead called on the table builder: + +```csharp +modelBuilder + .Entity() + .ToTable(b => b.HasCheckConstraint("CK_Blog_TooFewBits", "Id > 1023")); + +modelBuilder + .Entity() + .ToTable(b => b.HasComment("It's my table, and I'll delete it if I want to.")); + +modelBuilder + .Entity() + .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 + +[Tracking Issue #28249](https://github.com/dotnet/efcore/issues/28249) + +#### Old behavior + +In EF Core 6.0, when a new entity is tracked either from a [tracking query](xref:core/querying/tracking) or by [attaching it](xref:core/change-tracking/explicit-tracking) to the `DbContext`, then navigations to and from related entities in the [`Deleted` state](xref:core/change-tracking/index#entity-states) are [fixed up](xref:core/change-tracking/relationship-changes#relationship-fixup). + +#### 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 + +[Tracking Issue #26502](https://github.com/dotnet/efcore/issues/26502) + +#### Old behavior + +In EF Core 6.0, using the Cosmos extension method when using a relational provider, or the relational extension method when using the Cosmos provider could silently fail. + +#### New behavior + +Starting with EF Core 7.0, using the wrong extension method 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: + +```csharp +var result = CosmosQueryableExtensions.FromSqlRaw(context.Blogs, "SELECT ...").ToList(); +``` + +Or: + +```csharp +var result = RelationalQueryableExtensions.FromSqlRaw(context.Blogs, "SELECT ...").ToList(); +``` diff --git a/samples/core/Modeling/ValueConversions/CaseInsensitiveStrings.cs b/samples/core/Modeling/ValueConversions/CaseInsensitiveStrings.cs index 868cd062af..96a463d786 100644 --- a/samples/core/Modeling/ValueConversions/CaseInsensitiveStrings.cs +++ b/samples/core/Modeling/ValueConversions/CaseInsensitiveStrings.cs @@ -104,4 +104,4 @@ public class Post public Blog Blog { get; set; } } #endregion -} \ No newline at end of file +} diff --git a/samples/core/SqlServer/Misc/TriggersContext.cs b/samples/core/SqlServer/Misc/TriggersContext.cs index ad6977439f..134d46c851 100644 --- a/samples/core/SqlServer/Misc/TriggersContext.cs +++ b/samples/core/SqlServer/Misc/TriggersContext.cs @@ -1,5 +1,9 @@ +using System.Linq; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; namespace SqlServer.Faq; @@ -14,4 +18,39 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .ToTable(tb => tb.HasTrigger("SomeTrigger")); } #endregion + + #region ConfigureConventions + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + configurationBuilder.Conventions.Add(_ => new BlankTriggerAddingConvention()); + } + #endregion +} + +#region BlankTriggerAddingConvention +public class BlankTriggerAddingConvention : IModelFinalizingConvention +{ + public virtual void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext 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.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"); + } + } + } + } } +#endregion diff --git a/samples/core/SqlServer/SqlServer.csproj b/samples/core/SqlServer/SqlServer.csproj index 0f9ddaa6fc..0d07ed26f1 100644 --- a/samples/core/SqlServer/SqlServer.csproj +++ b/samples/core/SqlServer/SqlServer.csproj @@ -6,7 +6,7 @@ - +