dotnet/efcore · error · InvalidOperationException
Both '{entityType}' and '{otherEntityType}' are mapped to th
Error message
Both '{entityType}' and '{otherEntityType}' are mapped to the stored procedure '{sproc}'. All the entity types in a non-TPH hierarchy (one that doesn't have a discriminator) must be mapped to different stored procedures. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. What it means
ValidateNonTphMapping applies the same uniqueness rule to stored procedures (Insert/Delete/Update): in a non-TPH hierarchy each type must own its own sproc, because there is no discriminator to route a single sproc's result to multiple types. When two types resolve to the same stored-procedure StoreObjectIdentifier, EF throws.
Source
Thrown at src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs:2251
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)
.FirstOrDefault();
if (rowInternalFk != null
&& entityType.GetDirectlyDerivedTypes().Any())
{
throw new InvalidOperationException(
RelationalStrings.TpcTableSharing(
rowInternalFk.DeclaringEntityType.DisplayName(),
storeObject.Value.DisplayName(),
rowInternalFk.PrincipalEntityType.DisplayName()));
}View on GitHub (pinned to dbf9771522)
Solutions
- Give each type a distinct stored-procedure name per operation: .InsertStoredProcedure(s => s.HasName("Insert_Manager")).
- If you want one shared sproc for the whole hierarchy, use TPH and configure the sproc only on the root (see StoredProcedureTphDuplicate guidance).
- Remove the redundant HasName on the derived type so its default name is used.
Example fix
// before
modelBuilder.Entity<Manager>().InsertStoredProcedure(s => s.HasName("UpsertPerson"));
modelBuilder.Entity<Employee>().InsertStoredProcedure(s => s.HasName("UpsertPerson"));
// after
modelBuilder.Entity<Manager>().InsertStoredProcedure(s => s.HasName("UpsertManager"));
modelBuilder.Entity<Employee>().InsertStoredProcedure(s => s.HasName("UpsertEmployee")); Defensive patterns
Strategy: validation
Validate before calling
bool NoNonTphSprocClash(DbContext context)
{
foreach (var root in context.Model.GetEntityTypes()
.Where(e => e.BaseType == null && e.FindDiscriminatorProperty() == null && e.GetDerivedTypes().Any()))
{
foreach (var sot in new[] { StoreObjectType.InsertStoredProcedure, StoreObjectType.DeleteStoredProcedure, StoreObjectType.UpdateStoredProcedure })
{
var names = new HashSet<StoreObjectIdentifier>();
foreach (var et in root.GetDerivedTypesInclusive())
{
var so = StoreObjectIdentifier.Create(et, sot);
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("stored procedure '", StringComparison.Ordinal))
{
throw new InvalidOperationException("Two types in a TPT/TPC hierarchy share a stored procedure. Give each type its own sproc name, or use TPH with a single root sproc.", ex);
} Prevention
- Name sprocs per concrete type in TPT/TPC (e.g. Insert_Manager).
- For a shared sproc, switch to TPH and configure it on the root only.
- Test the model build in CI to catch sproc-name collisions early.
When it happens
Trigger: Two sibling types in a TPT/TPC hierarchy both call .InsertStoredProcedure(s => s.HasName("Upsert")) / Delete / Update with the same name, colliding in the derivedTypes dictionary for StoreObjectType.InsertStoredProcedure/DeleteStoredProcedure/UpdateStoredProcedure.
Common situations: Sharing one upsert/delete sproc across a hierarchy; copy-pasting stored-procedure configuration across derived types; scaffolding sprocs and forgetting to suffix per-type names.
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/800ef22109d3ecf6.
Report an issue: GitHub.