dotnet/efcore · error · InvalidOperationException
Both '{entityType}' and '{otherEntityType}' are explicitly
Error message
Both '{entityType}' and '{otherEntityType}' are explicitly mapped to the stored procedure '{sproc}' using the 'TPH' mapping strategy. Configure the stored procedure mapping on the root entity type, including all parameters for the derived types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. What it means
ValidateTphMapping handles stored procedures specially: when a derived type resolves to the SAME store object as the root (correct TPH behavior), EF still checks that the derived type did not declare its OWN stored procedure distinct from the root's. A TPH hierarchy must funnel all inserts/updates/deletes through one root sproc that carries parameters for every derived type; per-derived sprocs are illegal.
Source
Thrown at src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs:2318
var rootSproc = isSproc ? StoredProcedure.FindDeclaredStoredProcedure(rootEntityType, storeObjectType) : null;
var rootId = StoreObjectIdentifier.Create(rootEntityType, storeObjectType);
foreach (var entityType in rootEntityType.GetDerivedTypes())
{
var entityId = StoreObjectIdentifier.Create(entityType, storeObjectType);
if (entityId == null)
{
continue;
}
if (rootId == entityId)
{
if (rootSproc != null)
{
var sproc = StoredProcedure.FindDeclaredStoredProcedure(entityType, storeObjectType);
if (sproc != null
&& sproc != rootSproc)
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureTphDuplicate(
entityType.DisplayName(), rootEntityType.DisplayName(), rootId?.DisplayName()));
}
}
continue;
}
switch (storeObjectType)
{
case StoreObjectType.Table:
throw new InvalidOperationException(
RelationalStrings.TphTableMismatch(
entityType.DisplayName(), entityId.Value.DisplayName(),
rootEntityType.DisplayName(), rootId?.DisplayName()));
case StoreObjectType.View:
throw new InvalidOperationException(
RelationalStrings.TphViewMismatch(View on GitHub (pinned to dbf9771522)
Solutions
- Remove the .InsertStoredProcedure/.UpdateStoredProcedure/.DeleteStoredProcedure configuration from the derived types and define it once on the root.
- On the root sproc, include parameters for every derived type's extra properties (use .HasParameter or original-value parameters) so EF can route all operations through it.
- If you genuinely need per-type sprocs, change the hierarchy to TPT/TPC where each type owns its sproc.
Example fix
// before
modelBuilder.Entity<Base>().HasDiscriminator().InsertStoredProcedure(s => s.HasName("InsertBase"));
modelBuilder.Entity<Derived>().InsertStoredProcedure(s => s.HasName("InsertDerived")); // illegal in TPH
// after
modelBuilder.Entity<Base>().HasDiscriminator()
.InsertStoredProcedure(s => s.HasName("InsertBase")
.HasParameter("ExtraProp") /* derived-only column */); Defensive patterns
Strategy: validation
Validate before calling
bool TphSprocsDeclaredOnRootOnly(DbContext context)
{
foreach (var root in context.Model.GetEntityTypes()
.Where(e => e.BaseType == null && e.FindDiscriminatorProperty() != null))
{
foreach (var sot in new[] { StoreObjectType.InsertStoredProcedure, StoreObjectType.DeleteStoredProcedure, StoreObjectType.UpdateStoredProcedure })
{
if (StoredProcedure.FindDeclaredStoredProcedure(root, sot) is null) continue;
foreach (var d in root.GetDerivedTypes())
{
if (StoredProcedure.FindDeclaredStoredProcedure(d, sot) != null) return false;
}
}
}
return true;
} Try / catch
try { _ = context.Model; }
catch (InvalidOperationException ex) when (ex.Message.Contains("stored procedure", StringComparison.Ordinal) && ex.Message.Contains("TPH", StringComparison.Ordinal))
{
throw new InvalidOperationException("In TPH, configure stored procedures only on the root and include all derived parameters there.", ex);
} Prevention
- In TPH, declare .InsertStoredProcedure/.UpdateStoredProcedure/.DeleteStoredProcedure on the root only.
- Add parameters for every derived type's properties to the root sproc.
- For per-type sprocs, switch the hierarchy to TPT/TPC instead.
When it happens
Trigger: Root has TPH (a discriminator) and a declared sproc (StoredProcedure.FindDeclaredStoredProcedure(root) is non-null); a derived type that maps to the same store object (rootId == entityId) ALSO declares a stored procedure for the same operation, and it is a different StoredProcedure instance than the root's.
Common situations: Copying .InsertStoredProcedure(...) calls from the root onto derived types in a TPH model; scaffolding sprocs per type then enabling TPH; misunderstanding that TPH sprocs must be centralized on the root.
Related errors
- '{entityType}' is mapped to the stored procedure '{sproc}' w
- Both '{entityType}' and '{otherEntityType}' are mapped to th
- '{entityType}' is mapped to the table '{table}' while '{othe
- '{entityType}' is mapped to the view '{view}' while '{otherE
- '{entityType}' is mapped to the database function '{function
AI-assisted analysis of dotnet/efcore@dbf9771522 (2026-08-06).
Data as JSON: /api/errors/8e2e73128cdfb400.
Report an issue: GitHub.