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

  1. Remove the .InsertStoredProcedure/.UpdateStoredProcedure/.DeleteStoredProcedure configuration from the derived types and define it once on the root.
  2. 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.
  3. 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

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


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