diff --git a/src/EFCore.SqlServer/Diagnostics/Internal/SqlServerLoggingDefinitions.cs b/src/EFCore.SqlServer/Diagnostics/Internal/SqlServerLoggingDefinitions.cs index 6456e512209..12bc41e89e9 100644 --- a/src/EFCore.SqlServer/Diagnostics/Internal/SqlServerLoggingDefinitions.cs +++ b/src/EFCore.SqlServer/Diagnostics/Internal/SqlServerLoggingDefinitions.cs @@ -149,6 +149,14 @@ public class SqlServerLoggingDefinitions : RelationalLoggingDefinitions /// public EventDefinitionBase? LogReflexiveConstraintIgnored; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public EventDefinitionBase? LogDuplicateForeignKeyConstraintIgnored; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.SqlServer/Diagnostics/SqlServerEventId.cs b/src/EFCore.SqlServer/Diagnostics/SqlServerEventId.cs index 9e234a59dfd..a15bcaae125 100644 --- a/src/EFCore.SqlServer/Diagnostics/SqlServerEventId.cs +++ b/src/EFCore.SqlServer/Diagnostics/SqlServerEventId.cs @@ -62,7 +62,8 @@ private enum Id IndexFound, ForeignKeyFound, ForeignKeyPrincipalColumnMissingWarning, - ReflexiveConstraintIgnored + ReflexiveConstraintIgnored, + DuplicateForeignKeyConstraintIgnored, } private static readonly string _validationPrefix = DbLoggerCategory.Model.Validation.Name + "."; @@ -230,5 +231,11 @@ private static EventId MakeScaffoldingId(Id id) /// This event is in the category. /// public static readonly EventId ReflexiveConstraintIgnored = MakeScaffoldingId(Id.ReflexiveConstraintIgnored); + + /// + /// A duplicate foreign key constraint was skipped. + /// This event is in the category. + /// + public static readonly EventId DuplicateForeignKeyConstraintIgnored = MakeScaffoldingId(Id.DuplicateForeignKeyConstraintIgnored); } } diff --git a/src/EFCore.SqlServer/Extensions/Internal/SqlServerLoggerExtensions.cs b/src/EFCore.SqlServer/Extensions/Internal/SqlServerLoggerExtensions.cs index 67ab46e253f..3793ab6d8a4 100644 --- a/src/EFCore.SqlServer/Extensions/Internal/SqlServerLoggerExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/Internal/SqlServerLoggerExtensions.cs @@ -511,6 +511,28 @@ public static void ReflexiveConstraintIgnored( // No DiagnosticsSource events because these are purely design-time messages } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static void DuplicateForeignKeyConstraintIgnored( + this IDiagnosticsLogger diagnostics, + string foreignKeyName, + string tableName, + string duplicateForeignKeyName) + { + var definition = SqlServerResources.DuplicateForeignKeyConstraintIgnored(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, foreignKeyName, tableName, duplicateForeignKeyName); + } + + // No DiagnosticsSource events because these are purely design-time messages + } + /// /// Logs for the event. /// diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index 80f340df44d..afd28b83c94 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -804,6 +804,31 @@ public static EventDefinition LogReflexiveConstraintIgnored(IDia return (EventDefinition)definition; } + /// + /// Skipping foreign key '{foreignKeyName}' on table '{tableName}' since it is a duplicate of '{duplicateForeignKeyName}'. + /// + public static EventDefinition DuplicateForeignKeyConstraintIgnored(IDiagnosticsLogger logger) + { + var definition = ((Diagnostics.Internal.SqlServerLoggingDefinitions)logger.Definitions).LogDuplicateForeignKeyConstraintIgnored; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((Diagnostics.Internal.SqlServerLoggingDefinitions)logger.Definitions).LogDuplicateForeignKeyConstraintIgnored, + logger, + static logger => new EventDefinition( + logger.Options, + SqlServerEventId.DuplicateForeignKeyConstraintIgnored, + LogLevel.Warning, + "SqlServerEventId.DuplicateForeignKeyConstraintIgnored", + level => LoggerMessage.Define( + level, + SqlServerEventId.DuplicateForeignKeyConstraintIgnored, + _resourceManager.GetString("DuplicateForeignKeyConstraintIgnored")!))); + } + + return (EventDefinition)definition; + } + /// /// Savepoints are disabled because Multiple Active Result Sets (MARS) is enabled. If 'SaveChanges' fails, then the transaction cannot be automatically rolled back to a known clean state. Instead, the transaction should be rolled back by the application before retrying 'SaveChanges'. See https://go.microsoft.com/fwlink/?linkid=2149338 for more information. To identify the code which triggers this warning, call 'ConfigureWarnings(w => w.Throw(SqlServerEventId.SavepointsDisabledBecauseOfMARS))'. /// diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index cc45f62a28b..1ed0c6f040c 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -252,6 +252,10 @@ Skipping foreign key '{foreignKeyName}' on table '{tableName}' since all of its columns reference themselves. Debug SqlServerEventId.ReflexiveConstraintIgnored string string + + Skipping foreign key '{foreignKeyName}' on table '{tableName}' since it is a duplicate of '{duplicateForeignKeyName}'. + Warning SqlServerEventId.DuplicateForeignKeyConstraintIgnored string string string + Savepoints are disabled because Multiple Active Result Sets (MARS) is enabled. If 'SaveChanges' fails, then the transaction cannot be automatically rolled back to a known clean state. Instead, the transaction should be rolled back by the application before retrying 'SaveChanges'. See https://go.microsoft.com/fwlink/?linkid=2149338 for more information. To identify the code which triggers this warning, call 'ConfigureWarnings(w => w.Throw(SqlServerEventId.SavepointsDisabledBecauseOfMARS))'. Warning SqlServerEventId.SavepointsDisabledBecauseOfMARS diff --git a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs index 54f9ceab50c..3ba479ad344 100644 --- a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs +++ b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs @@ -1231,6 +1231,18 @@ FROM [sys].[foreign_keys] AS [f] } else { + var duplicated = table.ForeignKeys + .FirstOrDefault(k => k.Columns.SequenceEqual(foreignKey.Columns) + && k.PrincipalTable.Equals(foreignKey.PrincipalTable)); + if (duplicated != null) + { + _logger.DuplicateForeignKeyConstraintIgnored( + foreignKey.Name!, + DisplayName(table.Schema, table.Name!), + duplicated.Name!); + continue; + } + table.ForeignKeys.Add(foreignKey); } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs index f25710fa5fe..f92326d4c64 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs @@ -2370,6 +2370,45 @@ CONSTRAINT MYFK FOREIGN KEY (Id) REFERENCES PrincipalTable(Id) DROP TABLE PrincipalTable;"); } + [ConditionalFact] + public void Skip_duplicate_foreign_key() + { + Test( + @"CREATE TABLE PrincipalTable ( + Id int PRIMARY KEY, +); + +CREATE TABLE OtherPrincipalTable ( + Id int PRIMARY KEY, +); + +CREATE TABLE DependentTable ( + Id int PRIMARY KEY, + ForeignKeyId int, + CONSTRAINT MYFK1 FOREIGN KEY (ForeignKeyId) REFERENCES PrincipalTable(Id), + CONSTRAINT MYFK2 FOREIGN KEY (ForeignKeyId) REFERENCES PrincipalTable(Id), + CONSTRAINT MYFK3 FOREIGN KEY (ForeignKeyId) REFERENCES OtherPrincipalTable(Id), +);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var (level, _, message, _, _) = Assert.Single( + Fixture.ListLoggerFactory.Log, t => t.Id == SqlServerEventId.DuplicateForeignKeyConstraintIgnored); + Assert.Equal(LogLevel.Warning, level); + Assert.Equal( + SqlServerResources.DuplicateForeignKeyConstraintIgnored(new TestLogger()) + .GenerateMessage("MYFK2", "dbo.DependentTable", "MYFK1"), message); + + var table = dbModel.Tables.Single(t => t.Name == "DependentTable"); + Assert.Equal(2, table.ForeignKeys.Count); + }, + @" +DROP TABLE DependentTable; +DROP TABLE PrincipalTable; +DROP TABLE OtherPrincipalTable;"); + } + #endregion private void Test(