dotnet/efcore · error · InvalidOperationException
'{entityType}' is mapped to the stored procedure '{sproc}' w
Error message
'{entityType}' is mapped to the stored procedure '{sproc}' while '{otherEntityType}' is mapped to the stored procedure '{otherSproc}'. Map all the entity types in the hierarchy to the same stored procedure, or remove the discriminator and map them all to different stored procedures. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. What it means
ValidateTphMapping for stored procedures (Insert/Delete/Update): in a TPH hierarchy every type must use the same sproc as the root for a given operation. If a derived type resolves to a different stored-procedure StoreObjectIdentifier than the root, the model is rejected. (Distinct from StoredProcedureTphDuplicate, which fires when the derived type is on the SAME store object but declares its own sproc.)
Source
Thrown at src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs:2347
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(
entityType.DisplayName(), entityId.Value.DisplayName(),
rootEntityType.DisplayName(), rootId?.DisplayName()));
case StoreObjectType.Function:
throw new InvalidOperationException(
RelationalStrings.TphDbFunctionMismatch(
entityType.DisplayName(), entityId.Value.DisplayName(),
rootEntityType.DisplayName(), rootId?.DisplayName()));
case StoreObjectType.InsertStoredProcedure:
case StoreObjectType.DeleteStoredProcedure:
case StoreObjectType.UpdateStoredProcedure:
throw new InvalidOperationException(
RelationalStrings.TphStoredProcedureMismatch(
entityType.DisplayName(), entityId.Value.DisplayName(),
rootEntityType.DisplayName(), rootId?.DisplayName()));
}
}
}
/// <inheritdoc />
protected override bool IsRedundant(IForeignKey foreignKey)
=> base.IsRedundant(foreignKey)
&& !foreignKey.DeclaringEntityType.GetMappingFragments().Any();
/// <summary>
/// Validates the mapping fragments for an entity type.
/// </summary>
/// <param name="entityType">The entity type to validate.</param>
/// <param name="logger">The logger to use.</param>
protected virtual void ValidateMappingFragment(View on GitHub (pinned to dbf9771522)
Solutions
- Remove the derived type's .InsertStoredProcedure/.UpdateStoredProcedure/.DeleteStoredProcedure so it shares the root sproc.
- Move the hierarchy to TPT/TPC if per-type sprocs are genuinely required (no discriminator).
- Map the derived type to the SAME sproc name/schema as the root and define all parameters there.
Example fix
// before
modelBuilder.Entity<Base>().HasDiscriminator(b => b.Kind)
.InsertStoredProcedure(s => s.HasName("sp_Base_Insert"));
modelBuilder.Entity<Derived>().InsertStoredProcedure(s => s.HasName("sp_Derived_Insert"));
// after
modelBuilder.Entity<Base>().HasDiscriminator(b => b.Kind)
.InsertStoredProcedure(s => s.HasName("sp_Base_Insert"));
// derived omitted, shares root sproc Defensive patterns
Strategy: validation
Validate before calling
bool TphAllTypesSameSproc(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 rootSo = StoreObjectIdentifier.Create(root, sot);
if (rootSo is null) continue;
foreach (var d in root.GetDerivedTypes())
if (StoreObjectIdentifier.Create(d, sot) is { } ds && ds != rootSo) return false;
}
}
return true;
} Try / catch
try { _ = context.Model; }
catch (InvalidOperationException ex) when (ex.Message.Contains("stored procedure", StringComparison.Ordinal) && ex.Message.Contains("same stored procedure", StringComparison.Ordinal))
{
throw new InvalidOperationException("A TPH hierarchy must use one sproc per operation, declared on the root. Remove the derived sproc mapping or drop the discriminator.", ex);
} Prevention
- Configure TPH sprocs only on the root entity.
- If per-type sprocs are needed, remove the discriminator and switch to TPT/TPC.
- Add a test that builds the model after sproc configuration.
When it happens
Trigger: Root has a discriminator (TPH) and an .InsertStoredProcedure(s => s.HasName("sp_Base")), and a derived type is mapped to a DIFFERENT sproc name so its StoreObjectIdentifier differs from the root's, hitting the switch default in ValidateTphMapping.
Common situations: TPH model where a derived type was given its own named sproc; scaffolding sprocs per type then enabling TPH without unifying names.
Related errors
- Both '{entityType}' and '{otherEntityType}' are explicitly
- 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/66a086a5a5d7cff7.
Report an issue: GitHub.