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

  1. Remove the derived type's .InsertStoredProcedure/.UpdateStoredProcedure/.DeleteStoredProcedure so it shares the root sproc.
  2. Move the hierarchy to TPT/TPC if per-type sprocs are genuinely required (no discriminator).
  3. 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

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


AI-assisted analysis of dotnet/efcore@dbf9771522 (2026-08-06). Data as JSON: /api/errors/66a086a5a5d7cff7. Report an issue: GitHub.