diff --git a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs index 52f4f9e51a5..23c1f55fed3 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).OrderBy(g => g.Key)) { - columns.Clear(); - - if (!KeysUniqueAcrossTables) + if (!KeysUniqueAcrossSchemas) { keys.Clear(); } - if (!ForeignKeysUniqueAcrossTables) + foreach (var ((tableName, schema), conventionEntityTypes) in schemaGroup.OrderBy(t => t.Key.TableName)) { - 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. /// 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..77444652465 --- /dev/null +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/SharedTableConventionTest.cs @@ -0,0 +1,118 @@ +// 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, 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 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")); + + 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, 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 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")); + + Assert.Equal("PK_MyTable", orderPkName); + Assert.Equal("PK_MyTable1", 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, DbContext) GetModelBuilder(bool keysUniqueAcrossSchemas) + { + var conventionSet = new ConventionSet(); + + var context = new DbContext(new DbContextOptions()); + var dependencies = CreateDependencies() + .With(new CurrentDbContext(context)); + var relationalDependencies = CreateRelationalDependencies(); + + if (keysUniqueAcrossSchemas) + { + conventionSet.ModelFinalizingConventions.Add( + new KeysUniqueAcrossTablesSharedTableConvention(dependencies, relationalDependencies)); + } + else + { + conventionSet.ModelFinalizingConventions.Add( + new TestSharedTableConvention(dependencies, relationalDependencies)); + } + + return (new ModelBuilder(conventionSet), context); + } + + 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(); +}