Skip to content

Commit d67832d

Browse files
committed
Throw if table-sharing principal is in TPC and has derived types or the dependent doesn't use TPH
Don't configure table name for abstract types in TPC that have a corresponding DbSet property Make `ToTable(b => {})` map the entity type to the default table Fixes #3170 Fixes #27608 Fixes #27945 Fixes #27947
1 parent 0d83a29 commit d67832d

File tree

12 files changed

+2944
-290
lines changed

12 files changed

+2944
-290
lines changed

src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs

+6
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ public static EntityTypeBuilder<TEntity> ToTable<TEntity>(
187187
{
188188
Check.NotNull(buildAction, nameof(buildAction));
189189

190+
var entityTypeConventionBuilder = entityTypeBuilder.GetInfrastructure();
191+
if (entityTypeConventionBuilder.Metadata[RelationalAnnotationNames.TableName] == null)
192+
{
193+
entityTypeConventionBuilder.ToTable(entityTypeBuilder.Metadata.GetDefaultTableName());
194+
}
195+
190196
buildAction(new TableBuilder<TEntity>(null, null, entityTypeBuilder));
191197

192198
return entityTypeBuilder;

src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs

+57-3
Original file line numberDiff line numberDiff line change
@@ -1364,17 +1364,26 @@ protected override void ValidateInheritanceMapping(
13641364
}
13651365
else
13661366
{
1367-
var pk = entityType.FindPrimaryKey();
1367+
var primaryKey = entityType.FindPrimaryKey();
13681368
if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy)
13691369
{
1370-
var storeGeneratedProperty = pk?.Properties.FirstOrDefault(p => (p.ValueGenerated & ValueGenerated.OnAdd) != 0);
1370+
var storeGeneratedProperty = primaryKey?.Properties.FirstOrDefault(p => (p.ValueGenerated & ValueGenerated.OnAdd) != 0);
13711371
if (storeGeneratedProperty != null
13721372
&& entityType.GetTableName() != null)
13731373
{
13741374
logger.TpcStoreGeneratedIdentityWarning(storeGeneratedProperty);
13751375
}
1376+
1377+
if (entityType.GetDirectlyDerivedTypes().Any())
1378+
{
1379+
foreach (var fk in entityType.GetDeclaredReferencingForeignKeys())
1380+
{
1381+
AssertNonInternal(fk, StoreObjectType.View);
1382+
AssertNonInternal(fk, StoreObjectType.Table);
1383+
}
1384+
}
13761385
}
1377-
else if (pk == null)
1386+
else if (primaryKey == null)
13781387
{
13791388
throw new InvalidOperationException(
13801389
RelationalStrings.KeylessMappingStrategy(mappingStrategy ?? RelationalAnnotationNames.TptMappingStrategy, entityType.DisplayName()));
@@ -1384,6 +1393,31 @@ protected override void ValidateInheritanceMapping(
13841393
ValidateNonTphMapping(entityType, forTables: true);
13851394
}
13861395
}
1396+
1397+
static void AssertNonInternal(IForeignKey foreignKey, StoreObjectType storeObjectType)
1398+
{
1399+
if (!foreignKey.PrincipalKey.IsPrimaryKey()
1400+
|| foreignKey.PrincipalEntityType == foreignKey.DeclaringEntityType
1401+
|| !foreignKey.IsUnique
1402+
#pragma warning disable EF1001 // Internal EF Core API usage.
1403+
|| !PropertyListComparer.Instance.Equals(foreignKey.Properties, foreignKey.PrincipalKey.Properties))
1404+
#pragma warning restore EF1001 // Internal EF Core API usage.
1405+
{
1406+
return;
1407+
}
1408+
1409+
var storeObjectId = StoreObjectIdentifier.Create(foreignKey.DeclaringEntityType, storeObjectType);
1410+
if (storeObjectId == null
1411+
|| storeObjectId != StoreObjectIdentifier.Create(foreignKey.PrincipalEntityType, storeObjectType))
1412+
{
1413+
return;
1414+
}
1415+
1416+
throw new InvalidOperationException(RelationalStrings.TpcTableSharing(
1417+
foreignKey.DeclaringEntityType.DisplayName(),
1418+
storeObjectId.Value.DisplayName(),
1419+
foreignKey.PrincipalEntityType.DisplayName()));
1420+
}
13871421
}
13881422

13891423
/// <summary>
@@ -1429,6 +1463,26 @@ private static void ValidateNonTphMapping(IEntityType rootEntityType, bool forTa
14291463

14301464
derivedTypes[(name, schema)] = entityType;
14311465
}
1466+
1467+
var storeObject = StoreObjectIdentifier.Create(rootEntityType, forTables ? StoreObjectType.Table : StoreObjectType.View);
1468+
if (storeObject == null)
1469+
{
1470+
return;
1471+
}
1472+
1473+
var internalForeignKey = rootEntityType.FindRowInternalForeignKeys(storeObject.Value).FirstOrDefault();
1474+
if (internalForeignKey != null
1475+
&& derivedTypes.Count > 1
1476+
&& rootEntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy)
1477+
{
1478+
var derivedTypePair = derivedTypes.First(kv => kv.Value != rootEntityType);
1479+
var (derivedName, derivedSchema) = derivedTypePair.Key;
1480+
throw new InvalidOperationException(RelationalStrings.TpcTableSharingDependent(
1481+
rootEntityType.DisplayName(),
1482+
storeObject.Value.DisplayName(),
1483+
derivedTypePair.Value.DisplayName(),
1484+
derivedSchema == null ? derivedName : $"{derivedSchema}.{derivedName}"));
1485+
}
14321486
}
14331487

14341488
private static void ValidateTphMapping(IEntityType rootEntityType, bool forTables)

src/EFCore.Relational/Metadata/Conventions/TableNameFromDbSetConvention.cs

+16-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
99
/// <remarks>
1010
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see> for more information and examples.
1111
/// </remarks>
12-
public class TableNameFromDbSetConvention : IEntityTypeAddedConvention, IEntityTypeBaseTypeChangedConvention, IModelFinalizingConvention
12+
public class TableNameFromDbSetConvention :
13+
IEntityTypeAddedConvention,
14+
IEntityTypeBaseTypeChangedConvention,
15+
IModelFinalizingConvention
1316
{
1417
private readonly IDictionary<Type, string> _sets;
1518

@@ -115,11 +118,20 @@ public virtual void ProcessModelFinalizing(
115118
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
116119
{
117120
if (entityType.GetTableName() != null
118-
&& entityType.GetViewNameConfigurationSource() != null
119121
&& _sets.ContainsKey(entityType.ClrType))
120122
{
121-
// Undo the convention change if the entity type is mapped to a view
122-
entityType.Builder.HasNoAnnotation(RelationalAnnotationNames.TableName);
123+
if (entityType.GetViewNameConfigurationSource() != null)
124+
{
125+
// Undo the convention change if the entity type is mapped to a view
126+
entityType.Builder.HasNoAnnotation(RelationalAnnotationNames.TableName);
127+
}
128+
129+
if (entityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy
130+
&& entityType.IsAbstract())
131+
{
132+
// Undo the convention change if the entity type is mapped using TPC
133+
entityType.Builder.HasNoAnnotation(RelationalAnnotationNames.TableName);
134+
}
123135
}
124136
}
125137
}

src/EFCore.Relational/Metadata/ITable.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt
108108
}
109109

110110
builder.Append(Name);
111-
112-
if (IsExcludedFromMigrations)
111+
112+
if (EntityTypeMappings.First().EntityType is not RuntimeEntityType
113+
&& IsExcludedFromMigrations)
113114
{
114115
builder.Append(" ExcludedFromMigrations");
115116
}

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

+17-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

+7-1
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,12 @@
838838
<data name="TooFewReaderFields" xml:space="preserve">
839839
<value>The underlying reader doesn't have as many fields as expected. Expected: {expected}, actual: {actual}.</value>
840840
</data>
841+
<data name="TpcTableSharing" xml:space="preserve">
842+
<value>The entity type '{dependentType}' is mapped to '{storeObject}'. However the principal entity type '{principalEntityType}' is also mapped to '{storeObject}' and it's using the TPC mapping strategy. Only leaf entity types in a TPC hierarchy can use table-sharing.</value>
843+
</data>
844+
<data name="TpcTableSharingDependent" xml:space="preserve">
845+
<value>The entity type '{dependentType}' is mapped to '{storeObject}'. However one of its derived types '{derivedType}' is mapped to '{otherStoreObject}'. Hierarchies using table-sharing cannot be mapped using the TPC mapping strategy.</value>
846+
</data>
841847
<data name="TphTableMismatch" xml:space="preserve">
842848
<value>'{entityType}' is mapped to the table '{table}' while '{otherEntityType}' is mapped to the table '{otherTable}'. Map all the entity types in the hierarchy to the same table, or remove the discriminator and map them all to different tables. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information.</value>
843849
</data>
@@ -916,4 +922,4 @@
916922
<data name="VisitChildrenMustBeOverridden" xml:space="preserve">
917923
<value>'VisitChildren' must be overridden in the class deriving from 'SqlExpression'.</value>
918924
</data>
919-
</root>
925+
</root>

test/EFCore.Relational.Specification.Tests/Query/TPCGearsOfWarQueryRelationalFixture.cs

-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
3131
modelBuilder.Entity<Faction>().UseTpcMappingStrategy();
3232
modelBuilder.Entity<LocustLeader>().UseTpcMappingStrategy();
3333

34-
// Work-around for issue#27947
35-
modelBuilder.Entity<Faction>().ToTable((string)null);
36-
3734
modelBuilder.Entity<Gear>().ToTable("Gears");
3835
modelBuilder.Entity<Officer>().ToTable("Officers");
3936

test/EFCore.Relational.Specification.Tests/Query/TPCInheritanceQueryFixture.cs

-4
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
2525
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
2626
modelBuilder.Entity<Drink>().UseTpcMappingStrategy();
2727

28-
// Work-around for issue#27947
29-
modelBuilder.Entity<Animal>().ToTable((string)null);
30-
modelBuilder.Entity<Plant>().ToTable((string)null);
31-
3228
modelBuilder.Entity<Flower>().ToTable("Flowers");
3329
modelBuilder.Entity<Rose>().ToTable("Roses");
3430
modelBuilder.Entity<Daisy>().ToTable("Daisies");

test/EFCore.Relational.Specification.Tests/Query/TPCRelationshipsQueryRelationalFixture.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ public abstract class TPCRelationshipsQueryRelationalFixture : InheritanceRelati
1111

1212
public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
1313
=> base.AddOptions(builder).ConfigureWarnings(
14-
w =>
15-
w.Log(RelationalEventId.ForeignKeyTpcPrincipalWarning));
14+
w => w.Log(RelationalEventId.ForeignKeyTpcPrincipalWarning));
1615

1716
protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
1817
{
1918
base.OnModelCreating(modelBuilder, context);
2019

21-
modelBuilder.Entity<BaseInheritanceRelationshipEntity>().UseTpcMappingStrategy();
20+
modelBuilder.Entity<BaseInheritanceRelationshipEntity>().UseTpcMappingStrategy()
21+
// Table-sharing is not supported in TPC mapping
22+
.OwnsMany(e => e.OwnedCollectionOnBase, e => e.ToTable("OwnedCollections"))
23+
.OwnsOne(e => e.OwnedReferenceOnBase, e => e.ToTable("OwnedReferences"));
2224
modelBuilder.Entity<BaseReferenceOnBase>().UseTpcMappingStrategy();
2325
modelBuilder.Entity<BaseCollectionOnBase>().UseTpcMappingStrategy();
2426
modelBuilder.Entity<BaseReferenceOnDerived>().UseTpcMappingStrategy();

test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs

+34
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,22 @@ public virtual void Passes_on_valid_table_sharing_with_TPT()
16771677

16781678
Validate(modelBuilder);
16791679
}
1680+
1681+
[ConditionalFact]
1682+
public virtual void Detects_table_sharing_with_TPT_on_dependent()
1683+
{
1684+
var modelBuilder = CreateConventionalModelBuilder();
1685+
1686+
modelBuilder.Entity<Animal>()
1687+
.ToTable("Animal")
1688+
.HasOne(c => c.FavoritePerson).WithOne().HasForeignKey<Person>(c => c.Id);
1689+
1690+
modelBuilder.Entity<Person>().ToTable("Animal");
1691+
modelBuilder.Entity<Employee>().ToTable("Employee");
1692+
1693+
VerifyError(RelationalStrings.TpcTableSharingDependent("Person", "Animal", "Employee", "Employee"),
1694+
modelBuilder);
1695+
}
16801696

16811697
[ConditionalFact]
16821698
public virtual void Detects_linking_relationship_on_derived_type_in_TPT()
@@ -1929,6 +1945,24 @@ public virtual void Passes_on_valid_view_sharing_with_TPC()
19291945

19301946
Validate(modelBuilder);
19311947
}
1948+
1949+
[ConditionalFact]
1950+
public virtual void Detects_view_sharing_on_base_with_TPC()
1951+
{
1952+
var modelBuilder = CreateConventionalModelBuilder();
1953+
1954+
modelBuilder.Entity<Animal>()
1955+
.UseTpcMappingStrategy()
1956+
.ToView("Animal")
1957+
.HasOne(c => c.FavoritePerson).WithOne().HasForeignKey<Person>(c => c.Id);
1958+
1959+
modelBuilder.Entity<Cat>(x => x.ToView("Cat"));
1960+
1961+
modelBuilder.Entity<Person>().ToView("Animal");
1962+
1963+
VerifyError(RelationalStrings.TpcTableSharing("Person", "Animal", "Animal"),
1964+
modelBuilder);
1965+
}
19321966

19331967
[ConditionalFact]
19341968
public virtual void Detects_linking_relationship_on_derived_type_in_TPC()

test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -919,7 +919,7 @@ private IRelationalModel CreateTestModel(bool mapToTables = false, bool mapToVie
919919

920920
if (mapToTables)
921921
{
922-
cb.ToTable("AbstractBase");
922+
cb.ToTable(t => { });
923923
}
924924
}
925925

0 commit comments

Comments
 (0)