dotnet/efcore · error · InvalidOperationException

Both ' ' and ' ' are explicitly mapped to the stored…

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

Thrown inside ValidateTphMapping when, under TPH, a derived entity type has its own declared stored procedure (StoredProcedure.FindDeclaredStoredProcedure returns one) that differs from the stored procedure declared on the root (rootSproc). In TPH all derived types must use the root's stored procedure; you cannot give a derived type a separate sproc.

Solutions

  1. Remove the stored procedure mapping from the derived type — define all parameters (including derived-type columns) on the root's stored procedure.
  2. If separate stored procedures per type are truly required, switch the hierarchy to TPT/TPC.

Example fix

// before (TPH, derived type declares its own sproc)
modelBuilder.Entity<Person>().DeleteUsingStoredProcedure("sp_DeletePerson")
    .HasParameter(p => p.Id);
modelBuilder.Entity<Customer>().DeleteUsingStoredProcedure("sp_DeleteCustomer"); // throws 431

// after - only the root has the sproc; add Customer-specific parameters there
modelBuilder.Entity<Person>().DeleteUsingStoredProcedure("sp_DeletePerson")
    .HasParameter("ShippingAddress", p => ((Customer)p).ShippingAddress);
Defensive patterns

Strategy: validation

Validate before calling

foreach (var root in modelBuilder.Model.GetEntityTypes().Where(t => t.BaseType == null))
{
    if (root.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy
        || root.FindDiscriminatorProperty() == null) continue;
    foreach (var sot in new[] { StoreObjectType.InsertStoredProcedure, StoreObjectType.DeleteStoredProcedure, StoreObjectType.UpdateStoredProcedure })
    {
        var rootSproc = StoredProcedure.FindDeclaredStoredProcedure(root, sot);
        if (rootSproc == null) continue;
        foreach (var et in root.GetDerivedTypes())
        {
            var sproc = StoredProcedure.FindDeclaredStoredProcedure(et, sot);
            if (sproc != null && sproc != rootSproc)
                throw new InvalidOperationException($"Derived {et.DisplayName()} declares its own {sot}; define it on {root.DisplayName()} only.");
        }
    }
}

Prevention

When it happens

Trigger: A TPH hierarchy where the root has *UsingStoredProcedure and a derived type ALSO calls *UsingStoredProcedure with a different stored procedure name. The check fires when rootId == entityId (same store object) but sproc != rootSproc.

Common situations: Adding a per-type stored procedure while still on TPH; copy-pasting sproc config onto derived types; misunderstanding that TPH sproc mapping lives only on the root.

Related errors


AI-assisted analysis of dotnet/efcore@3a2006ef56 (2026-08-11). Data as JSON: /api/errors/8e2e73128cdfb400. Report an issue: GitHub.

Appendix: 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 3a2006ef56)