dotnet/efcore · error · InvalidOperationException

Both '{entityType}' and '{otherEntityType}' are mapped to th

Error message

Both '{entityType}' and '{otherEntityType}' are mapped to the stored procedure '{sproc}'. All the entity types in a non-TPH hierarchy (one that doesn't have a discriminator) must be mapped to different stored procedures. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information.

What it means

ValidateNonTphMapping applies the same uniqueness rule to stored procedures (Insert/Delete/Update): in a non-TPH hierarchy each type must own its own sproc, because there is no discriminator to route a single sproc's result to multiple types. When two types resolve to the same stored-procedure StoreObjectIdentifier, EF throws.

Source

Thrown at src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs:2251

                continue;
            }

            if (derivedTypes.TryGetValue(storeObject.Value, out var otherType))
            {
                switch (storeObjectType)
                {
                    case StoreObjectType.Table:
                        throw new InvalidOperationException(
                            RelationalStrings.NonTphTableClash(
                                entityType.DisplayName(), otherType.DisplayName(), storeObject.Value.DisplayName()));
                    case StoreObjectType.View:
                        throw new InvalidOperationException(
                            RelationalStrings.NonTphViewClash(
                                entityType.DisplayName(), otherType.DisplayName(), storeObject.Value.DisplayName()));
                    case StoreObjectType.InsertStoredProcedure:
                    case StoreObjectType.DeleteStoredProcedure:
                    case StoreObjectType.UpdateStoredProcedure:
                        throw new InvalidOperationException(
                            RelationalStrings.NonTphStoredProcedureClash(
                                entityType.DisplayName(), otherType.DisplayName(), storeObject.Value.DisplayName()));
                }
            }

            if (isTpc)
            {
                var rowInternalFk = entityType.FindDeclaredReferencingRowInternalForeignKeys(storeObject.Value)
                    .FirstOrDefault();
                if (rowInternalFk != null
                    && entityType.GetDirectlyDerivedTypes().Any())
                {
                    throw new InvalidOperationException(
                        RelationalStrings.TpcTableSharing(
                            rowInternalFk.DeclaringEntityType.DisplayName(),
                            storeObject.Value.DisplayName(),
                            rowInternalFk.PrincipalEntityType.DisplayName()));
                }

View on GitHub (pinned to dbf9771522)

Solutions

  1. Give each type a distinct stored-procedure name per operation: .InsertStoredProcedure(s => s.HasName("Insert_Manager")).
  2. If you want one shared sproc for the whole hierarchy, use TPH and configure the sproc only on the root (see StoredProcedureTphDuplicate guidance).
  3. Remove the redundant HasName on the derived type so its default name is used.

Example fix

// before
modelBuilder.Entity<Manager>().InsertStoredProcedure(s => s.HasName("UpsertPerson"));
modelBuilder.Entity<Employee>().InsertStoredProcedure(s => s.HasName("UpsertPerson"));

// after
modelBuilder.Entity<Manager>().InsertStoredProcedure(s => s.HasName("UpsertManager"));
modelBuilder.Entity<Employee>().InsertStoredProcedure(s => s.HasName("UpsertEmployee"));
Defensive patterns

Strategy: validation

Validate before calling

bool NoNonTphSprocClash(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 names = new HashSet<StoreObjectIdentifier>();
            foreach (var et in root.GetDerivedTypesInclusive())
            {
                var so = StoreObjectIdentifier.Create(et, sot);
                if (so.HasValue && !names.Add(so.Value)) return false;
            }
        }
    }
    return true;
}

Try / catch

try { _ = context.Model; }
catch (InvalidOperationException ex) when (ex.Message.Contains("non-TPH hierarchy", StringComparison.Ordinal) && ex.Message.Contains("stored procedure '", StringComparison.Ordinal))
{
    throw new InvalidOperationException("Two types in a TPT/TPC hierarchy share a stored procedure. Give each type its own sproc name, or use TPH with a single root sproc.", ex);
}

Prevention

When it happens

Trigger: Two sibling types in a TPT/TPC hierarchy both call .InsertStoredProcedure(s => s.HasName("Upsert")) / Delete / Update with the same name, colliding in the derivedTypes dictionary for StoreObjectType.InsertStoredProcedure/DeleteStoredProcedure/UpdateStoredProcedure.

Common situations: Sharing one upsert/delete sproc across a hierarchy; copy-pasting stored-procedure configuration across derived types; scaffolding sprocs and forgetting to suffix per-type names.

Related errors


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