dotnet/efcore · error · InvalidOperationException
Both '{entityType}' and '{otherEntityType}' are mapped to th
Error message
Both '{entityType}' and '{otherEntityType}' are mapped to the table '{table}'. All the entity types in a non-TPH hierarchy (one that doesn't have a discriminator) must be mapped to different tables. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. What it means
ValidateNonTphMapping tracks each store object that an entity type in a non-TPH hierarchy maps to. Because a non-TPH hierarchy has no discriminator to disambiguate rows, every type must land in its own table; when two types in the same hierarchy resolve to the same Table StoreObjectIdentifier, EF throws to prevent ambiguous inserts/queries.
Source
Thrown at src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs:2241
&& entityType.GetDerivedTypes().Any(derived => StoreObjectIdentifier.Create(derived, storeObjectType) != null))
{
throw new InvalidOperationException(
RelationalStrings.UnmappedNonTPHOwner(
entityType.DisplayName(),
unmappedOwnedType.FindOwnership()!.PrincipalToDependent?.Name,
unmappedOwnedType.DisplayName(),
storeObjectType));
}
continue;
}
if (derivedTypes.TryGetValue(storeObject.Value, out var otherType))
{
switch (storeObjectType)
{
case StoreObjectType.Table:
throw new InvalidOperationException(
RelationalStrings.NonTphTableClash(
entityType.DisplayName(), otherType.DisplayName(), storeObject.Value.DisplayName()));
case StoreObjectType.View:
throw new InvalidOperationException(
RelationalStrings.NonTphViewClash(
entityType.DisplayName(), otherType.DisplayName(), storeObject.Value.DisplayName()));
case StoreObjectType.InsertStoredProcedure:
case StoreObjectType.DeleteStoredProcedure:
case StoreObjectType.UpdateStoredProcedure:
throw new InvalidOperationException(
RelationalStrings.NonTphStoredProcedureClash(
entityType.DisplayName(), otherType.DisplayName(), storeObject.Value.DisplayName()));
}
}
if (isTpc)
{
var rowInternalFk = entityType.FindDeclaredReferencingRowInternalForeignKeys(storeObject.Value)View on GitHub (pinned to dbf9771522)
Solutions
- Give each derived type in the TPT/TPC hierarchy a distinct ToTable name.
- If you actually want all types in one table, switch the root to TPH (configure a discriminator property) so the validator takes the TPH path.
- Remove the redundant ToTable call on the derived type so it falls back to its own default name.
Example fix
// before
modelBuilder.Entity<Manager>().ToTable("People");
modelBuilder.Entity<Employee>().ToTable("People"); // TPT without discriminator
// after
modelBuilder.Entity<Manager>().ToTable("Managers");
modelBuilder.Entity<Employee>().ToTable("Employees"); Defensive patterns
Strategy: validation
Validate before calling
bool NoNonTphTableClash(DbContext context)
{
foreach (var root in context.Model.GetEntityTypes()
.Where(e => e.BaseType == null && e.FindDiscriminatorProperty() == null && e.GetDerivedTypes().Any()))
{
var names = new HashSet<StoreObjectIdentifier>();
foreach (var et in root.GetDerivedTypesInclusive())
{
var so = StoreObjectIdentifier.Create(et, StoreObjectType.Table);
if (so.HasValue && !names.Add(so.Value)) return false;
}
}
return true;
} Try / catch
try { _ = context.Model; }
catch (InvalidOperationException ex) when (ex.Message.Contains("non-TPH hierarchy", StringComparison.Ordinal) && ex.Message.Contains("table '", StringComparison.Ordinal))
{
throw new InvalidOperationException("Two types in a TPT/TPC hierarchy share a table. Give each a distinct ToTable, or switch to TPH.", ex);
} Prevention
- In TPT/TPC, always set a distinct ToTable per concrete type.
- If one shared table is the goal, configure a discriminator and use TPH instead.
- Write a test that asserts each hierarchy member maps to a unique table.
When it happens
Trigger: Produced when two entity types in a discriminator-less hierarchy both call ToTable("X") (or inherit the same table name) and the validator detects storeObjectType == StoreObjectType.Table collision in derivedTypes dictionary. Common when explicit ToTable names are copy-pasted across TPT siblings.
Common situations: TPT mapping where sibling derived types accidentally share a ToTable name; scaffolded mappings that reused the base table for a derived type; renaming a table and forgetting to update one sibling.
Related errors
- The short name for '{entityType1}' is '{discriminatorValue}'
- Both '{entityType}' and '{otherEntityType}' are mapped to th
- Both '{entityType}' and '{otherEntityType}' are mapped to th
- The derived entity type '{entityType}' was configured with t
- The mapping strategy '{mappingStrategy}' specified on '{enti
AI-assisted analysis of dotnet/efcore@dbf9771522 (2026-08-06).
Data as JSON: /api/errors/f258c440a713b4dc.
Report an issue: GitHub.