From 7e44fc055b7ca9b7437b307a27ebde61b8f24d15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:54:29 +0000 Subject: [PATCH 1/6] Initial plan From 3110e6b853698b522eb01b08e2de22a7c208eb4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:02:53 +0000 Subject: [PATCH 2/6] Add KeysUniqueAcrossSchemas property to SharedTableConvention Add a new protected virtual bool property KeysUniqueAcrossSchemas (default true) that controls whether key constraint names must be unique across schemas. Group the table iteration by schema and clear the keys dictionary at schema boundaries when KeysUniqueAcrossSchemas is false. Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Conventions/SharedTableConvention.cs | 80 +++++++++++-------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs index 52f4f9e51a5..b2b2b1807fb 100644 --- a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs @@ -53,50 +53,58 @@ public virtual void ProcessModelFinalizing( var checkConstraints = new Dictionary<(string, string?), (IConventionCheckConstraint, StoreObjectIdentifier)>(); var defaultConstraints = new Dictionary<(string, string?), (IConventionProperty, StoreObjectIdentifier)>(); var triggers = new Dictionary(); - foreach (var ((tableName, schema), conventionEntityTypes) in tables) + foreach (var schemaGroup in tables.GroupBy(t => t.Key.Schema)) { - columns.Clear(); - - if (!KeysUniqueAcrossTables) + if (!KeysUniqueAcrossSchemas) { keys.Clear(); } - if (!ForeignKeysUniqueAcrossTables) + foreach (var ((tableName, schema), conventionEntityTypes) in schemaGroup) { - foreignKeys.Clear(); - } + columns.Clear(); - if (!IndexesUniqueAcrossTables) - { - indexes.Clear(); - } + if (!KeysUniqueAcrossTables) + { + keys.Clear(); + } - if (!CheckConstraintsUniqueAcrossTables) - { - checkConstraints.Clear(); - } + if (!ForeignKeysUniqueAcrossTables) + { + foreignKeys.Clear(); + } - if (!DefaultConstraintsUniqueAcrossTables) - { - defaultConstraints.Clear(); - } + if (!IndexesUniqueAcrossTables) + { + indexes.Clear(); + } - if (!TriggersUniqueAcrossTables) - { - triggers.Clear(); - } + if (!CheckConstraintsUniqueAcrossTables) + { + checkConstraints.Clear(); + } - var storeObject = StoreObjectIdentifier.Table(tableName, schema); - foreach (var entityType in conventionEntityTypes) - { - UniquifyColumnNames(entityType, columns, storeObject, maxLength); - UniquifyKeyNames(entityType, keys, storeObject, maxLength); - UniquifyForeignKeyNames(entityType, foreignKeys, storeObject, maxLength); - UniquifyIndexNames(entityType, indexes, storeObject, maxLength); - UniquifyCheckConstraintNames(entityType, checkConstraints, storeObject, maxLength); - UniquifyDefaultConstraintNames(entityType, defaultConstraints, storeObject, maxLength); - UniquifyTriggerNames(entityType, triggers, storeObject, maxLength); + if (!DefaultConstraintsUniqueAcrossTables) + { + defaultConstraints.Clear(); + } + + if (!TriggersUniqueAcrossTables) + { + triggers.Clear(); + } + + var storeObject = StoreObjectIdentifier.Table(tableName, schema); + foreach (var entityType in conventionEntityTypes) + { + UniquifyColumnNames(entityType, columns, storeObject, maxLength); + UniquifyKeyNames(entityType, keys, storeObject, maxLength); + UniquifyForeignKeyNames(entityType, foreignKeys, storeObject, maxLength); + UniquifyIndexNames(entityType, indexes, storeObject, maxLength); + UniquifyCheckConstraintNames(entityType, checkConstraints, storeObject, maxLength); + UniquifyDefaultConstraintNames(entityType, defaultConstraints, storeObject, maxLength); + UniquifyTriggerNames(entityType, triggers, storeObject, maxLength); + } } } } @@ -107,6 +115,12 @@ public virtual void ProcessModelFinalizing( protected virtual bool KeysUniqueAcrossTables => false; + /// + /// Gets a value indicating whether key names should be unique across schemas. + /// + protected virtual bool KeysUniqueAcrossSchemas + => true; + /// /// Gets a value indicating whether foreign key names should be unique across tables. /// From 62747345a02a0184360933f32a27a0d9da0d80ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:42:25 +0000 Subject: [PATCH 3/6] Add tests for KeysUniqueAcrossSchemas override with false Add SharedTableConventionTest with two tests: - Keys_are_not_uniquified_across_schemas_when_KeysUniqueAcrossSchemas_is_false - Keys_are_uniquified_across_schemas_when_KeysUniqueAcrossSchemas_is_true Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Conventions/SharedTableConventionTest.cs | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs new file mode 100644 index 00000000000..9eda915644f --- /dev/null +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; + +// ReSharper disable InconsistentNaming +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +public class SharedTableConventionTest +{ + [ConditionalFact] + public virtual void Keys_are_not_uniquified_across_schemas_when_KeysUniqueAcrossSchemas_is_false() + { + var modelBuilder = GetModelBuilder(keysUniqueAcrossSchemas: false); + modelBuilder.Entity().ToTable("MyTable", "Schema1").HasKey(e => e.Id); + modelBuilder.Entity().ToTable("MyTable", "Schema2").HasKey(e => e.Id); + + var model = modelBuilder.Model; + model.FinalizeModel(); + + var orderEntityType = model.FindEntityType(typeof(Order))!; + var customerEntityType = model.FindEntityType(typeof(Customer))!; + + var orderPkName = orderEntityType.FindPrimaryKey()!.GetName( + StoreObjectIdentifier.Table("MyTable", "Schema1")); + var customerPkName = customerEntityType.FindPrimaryKey()!.GetName( + StoreObjectIdentifier.Table("MyTable", "Schema2")); + + Assert.Equal("PK_MyTable", orderPkName); + Assert.Equal("PK_MyTable", customerPkName); + } + + [ConditionalFact] + public virtual void Keys_are_uniquified_across_schemas_when_KeysUniqueAcrossSchemas_is_true() + { + var modelBuilder = GetModelBuilder(keysUniqueAcrossSchemas: true); + modelBuilder.Entity().ToTable("MyTable", "Schema1").HasKey(e => e.Id); + modelBuilder.Entity().ToTable("MyTable", "Schema2").HasKey(e => e.Id); + + var model = modelBuilder.Model; + model.FinalizeModel(); + + var orderEntityType = model.FindEntityType(typeof(Order))!; + var customerEntityType = model.FindEntityType(typeof(Customer))!; + + var orderPkName = orderEntityType.FindPrimaryKey()!.GetName( + StoreObjectIdentifier.Table("MyTable", "Schema1")); + var customerPkName = customerEntityType.FindPrimaryKey()!.GetName( + StoreObjectIdentifier.Table("MyTable", "Schema2")); + + Assert.NotEqual(orderPkName, customerPkName); + } + + private class Order + { + public int Id { get; set; } + } + + private class Customer + { + public int Id { get; set; } + } + + private class TestSharedTableConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + : SharedTableConvention(dependencies, relationalDependencies) + { + protected override bool KeysUniqueAcrossTables + => true; + + protected override bool KeysUniqueAcrossSchemas + => false; + } + + private ModelBuilder GetModelBuilder(bool keysUniqueAcrossSchemas) + { + var conventionSet = new ConventionSet(); + + var dependencies = CreateDependencies() + .With(new CurrentDbContext(new DbContext(new DbContextOptions()))); + var relationalDependencies = CreateRelationalDependencies(); + + if (keysUniqueAcrossSchemas) + { + conventionSet.ModelFinalizingConventions.Add( + new KeysUniqueAcrossTablesSharedTableConvention(dependencies, relationalDependencies)); + } + else + { + conventionSet.ModelFinalizingConventions.Add( + new TestSharedTableConvention(dependencies, relationalDependencies)); + } + + return new ModelBuilder(conventionSet); + } + + private class KeysUniqueAcrossTablesSharedTableConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + : SharedTableConvention(dependencies, relationalDependencies) + { + protected override bool KeysUniqueAcrossTables + => true; + } + + private ProviderConventionSetBuilderDependencies CreateDependencies() + => FakeRelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + + private RelationalConventionSetBuilderDependencies CreateRelationalDependencies() + => FakeRelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); +} From 18a222d3cddbf8a9013bf0f1678e6dc3ff52d568 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:41:22 +0000 Subject: [PATCH 4/6] Use finalized model for assertions in SharedTableConventionTest Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Conventions/SharedTableConventionTest.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs index 9eda915644f..ac4c8729276 100644 --- a/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs @@ -15,11 +15,10 @@ public virtual void Keys_are_not_uniquified_across_schemas_when_KeysUniqueAcross modelBuilder.Entity().ToTable("MyTable", "Schema1").HasKey(e => e.Id); modelBuilder.Entity().ToTable("MyTable", "Schema2").HasKey(e => e.Id); - var model = modelBuilder.Model; - model.FinalizeModel(); + var finalizedModel = modelBuilder.Model.FinalizeModel(); - var orderEntityType = model.FindEntityType(typeof(Order))!; - var customerEntityType = model.FindEntityType(typeof(Customer))!; + var orderEntityType = finalizedModel.FindEntityType(typeof(Order))!; + var customerEntityType = finalizedModel.FindEntityType(typeof(Customer))!; var orderPkName = orderEntityType.FindPrimaryKey()!.GetName( StoreObjectIdentifier.Table("MyTable", "Schema1")); @@ -37,11 +36,10 @@ public virtual void Keys_are_uniquified_across_schemas_when_KeysUniqueAcrossSche modelBuilder.Entity().ToTable("MyTable", "Schema1").HasKey(e => e.Id); modelBuilder.Entity().ToTable("MyTable", "Schema2").HasKey(e => e.Id); - var model = modelBuilder.Model; - model.FinalizeModel(); + var finalizedModel = modelBuilder.Model.FinalizeModel(); - var orderEntityType = model.FindEntityType(typeof(Order))!; - var customerEntityType = model.FindEntityType(typeof(Customer))!; + var orderEntityType = finalizedModel.FindEntityType(typeof(Order))!; + var customerEntityType = finalizedModel.FindEntityType(typeof(Customer))!; var orderPkName = orderEntityType.FindPrimaryKey()!.GetName( StoreObjectIdentifier.Table("MyTable", "Schema1")); From 1ceb3a786a71196ed0988917a8eb334ae8f19c03 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Thu, 5 Mar 2026 13:13:35 -0800 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Metadata/Conventions/SharedTableConventionTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs index ac4c8729276..cfc4cf01ee5 100644 --- a/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs @@ -46,7 +46,8 @@ public virtual void Keys_are_uniquified_across_schemas_when_KeysUniqueAcrossSche var customerPkName = customerEntityType.FindPrimaryKey()!.GetName( StoreObjectIdentifier.Table("MyTable", "Schema2")); - Assert.NotEqual(orderPkName, customerPkName); + Assert.Equal("PK_MyTable", orderPkName); + Assert.Equal("PK_MyTable1", customerPkName); } private class Order From 5ea7971869886b67d0441311a86ec385049473ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:22:43 +0000 Subject: [PATCH 6/6] Order tables by name within schema groups and dispose DbContext in test Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Conventions/SharedTableConvention.cs | 4 +- .../Conventions/SharedTableConventionTest.cs | 61 +++++++++++-------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs index b2b2b1807fb..23c1f55fed3 100644 --- a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs @@ -53,14 +53,14 @@ public virtual void ProcessModelFinalizing( var checkConstraints = new Dictionary<(string, string?), (IConventionCheckConstraint, StoreObjectIdentifier)>(); var defaultConstraints = new Dictionary<(string, string?), (IConventionProperty, StoreObjectIdentifier)>(); var triggers = new Dictionary(); - foreach (var schemaGroup in tables.GroupBy(t => t.Key.Schema)) + foreach (var schemaGroup in tables.GroupBy(t => t.Key.Schema).OrderBy(g => g.Key)) { if (!KeysUniqueAcrossSchemas) { keys.Clear(); } - foreach (var ((tableName, schema), conventionEntityTypes) in schemaGroup) + foreach (var ((tableName, schema), conventionEntityTypes) in schemaGroup.OrderBy(t => t.Key.TableName)) { columns.Clear(); diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs index cfc4cf01ee5..77444652465 100644 --- a/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs @@ -11,43 +11,49 @@ public class SharedTableConventionTest [ConditionalFact] public virtual void Keys_are_not_uniquified_across_schemas_when_KeysUniqueAcrossSchemas_is_false() { - var modelBuilder = GetModelBuilder(keysUniqueAcrossSchemas: false); - modelBuilder.Entity().ToTable("MyTable", "Schema1").HasKey(e => e.Id); - modelBuilder.Entity().ToTable("MyTable", "Schema2").HasKey(e => e.Id); + var (modelBuilder, context) = GetModelBuilder(keysUniqueAcrossSchemas: false); + using (context) + { + modelBuilder.Entity().ToTable("MyTable", "Schema1").HasKey(e => e.Id); + modelBuilder.Entity().ToTable("MyTable", "Schema2").HasKey(e => e.Id); - var finalizedModel = modelBuilder.Model.FinalizeModel(); + var finalizedModel = modelBuilder.Model.FinalizeModel(); - var orderEntityType = finalizedModel.FindEntityType(typeof(Order))!; - var customerEntityType = finalizedModel.FindEntityType(typeof(Customer))!; + var orderEntityType = finalizedModel.FindEntityType(typeof(Order))!; + var customerEntityType = finalizedModel.FindEntityType(typeof(Customer))!; - var orderPkName = orderEntityType.FindPrimaryKey()!.GetName( - StoreObjectIdentifier.Table("MyTable", "Schema1")); - var customerPkName = customerEntityType.FindPrimaryKey()!.GetName( - StoreObjectIdentifier.Table("MyTable", "Schema2")); + var orderPkName = orderEntityType.FindPrimaryKey()!.GetName( + StoreObjectIdentifier.Table("MyTable", "Schema1")); + var customerPkName = customerEntityType.FindPrimaryKey()!.GetName( + StoreObjectIdentifier.Table("MyTable", "Schema2")); - Assert.Equal("PK_MyTable", orderPkName); - Assert.Equal("PK_MyTable", customerPkName); + Assert.Equal("PK_MyTable", orderPkName); + Assert.Equal("PK_MyTable", customerPkName); + } } [ConditionalFact] public virtual void Keys_are_uniquified_across_schemas_when_KeysUniqueAcrossSchemas_is_true() { - var modelBuilder = GetModelBuilder(keysUniqueAcrossSchemas: true); - modelBuilder.Entity().ToTable("MyTable", "Schema1").HasKey(e => e.Id); - modelBuilder.Entity().ToTable("MyTable", "Schema2").HasKey(e => e.Id); + var (modelBuilder, context) = GetModelBuilder(keysUniqueAcrossSchemas: true); + using (context) + { + modelBuilder.Entity().ToTable("MyTable", "Schema1").HasKey(e => e.Id); + modelBuilder.Entity().ToTable("MyTable", "Schema2").HasKey(e => e.Id); - var finalizedModel = modelBuilder.Model.FinalizeModel(); + var finalizedModel = modelBuilder.Model.FinalizeModel(); - var orderEntityType = finalizedModel.FindEntityType(typeof(Order))!; - var customerEntityType = finalizedModel.FindEntityType(typeof(Customer))!; + var orderEntityType = finalizedModel.FindEntityType(typeof(Order))!; + var customerEntityType = finalizedModel.FindEntityType(typeof(Customer))!; - var orderPkName = orderEntityType.FindPrimaryKey()!.GetName( - StoreObjectIdentifier.Table("MyTable", "Schema1")); - var customerPkName = customerEntityType.FindPrimaryKey()!.GetName( - StoreObjectIdentifier.Table("MyTable", "Schema2")); + var orderPkName = orderEntityType.FindPrimaryKey()!.GetName( + StoreObjectIdentifier.Table("MyTable", "Schema1")); + var customerPkName = customerEntityType.FindPrimaryKey()!.GetName( + StoreObjectIdentifier.Table("MyTable", "Schema2")); - Assert.Equal("PK_MyTable", orderPkName); - Assert.Equal("PK_MyTable1", customerPkName); + Assert.Equal("PK_MyTable", orderPkName); + Assert.Equal("PK_MyTable1", customerPkName); + } } private class Order @@ -72,12 +78,13 @@ protected override bool KeysUniqueAcrossSchemas => false; } - private ModelBuilder GetModelBuilder(bool keysUniqueAcrossSchemas) + private (ModelBuilder, DbContext) GetModelBuilder(bool keysUniqueAcrossSchemas) { var conventionSet = new ConventionSet(); + var context = new DbContext(new DbContextOptions()); var dependencies = CreateDependencies() - .With(new CurrentDbContext(new DbContext(new DbContextOptions()))); + .With(new CurrentDbContext(context)); var relationalDependencies = CreateRelationalDependencies(); if (keysUniqueAcrossSchemas) @@ -91,7 +98,7 @@ private ModelBuilder GetModelBuilder(bool keysUniqueAcrossSchemas) new TestSharedTableConvention(dependencies, relationalDependencies)); } - return new ModelBuilder(conventionSet); + return (new ModelBuilder(conventionSet), context); } private class KeysUniqueAcrossTablesSharedTableConvention(