dotnet/efcore · error · InvalidOperationException

ExecuteOperationOnTPC

ExecuteOperationOnTPC

Error message

The operation '{operation}' is being applied on entity type '{entityType}', which is using the TPC mapping strategy and is not a leaf type. 'ExecuteDelete'/'ExecuteUpdate' operations on entity types participating in TPC hierarchies is only supported for leaf types.

What it means

Thrown by TranslateExecuteDelete when the target entity type uses the TPC (Table-Per-Concrete-Type) strategy and is NOT a leaf - i.e. it has derived types. In TPC, ExecuteDelete/ExecuteUpdate can only safely target a leaf type (one concrete table); a non-leaf has no single concrete table to modify, so derived rows elsewhere would be missed.

Source

Thrown at src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteDelete.cs:36

        }

        if (entityType.IsMappedToJson())
        {
            throw new InvalidOperationException(
                RelationalStrings.ExecuteOperationOnOwnedJsonIsNotSupported("ExecuteDelete", entityType.DisplayName()));
        }

        switch (entityType.GetMappingStrategy())
        {
            case RelationalAnnotationNames.TptMappingStrategy:
                throw new InvalidOperationException(
                    RelationalStrings.ExecuteOperationOnTPT(
                        nameof(EntityFrameworkQueryableExtensions.ExecuteDelete),
                        entityType.DisplayName()));

            // Note that we do allow TPC if the target is a leaf type
            case RelationalAnnotationNames.TpcMappingStrategy when entityType.GetDirectlyDerivedTypes().Any():
                throw new InvalidOperationException(
                    RelationalStrings.ExecuteOperationOnTPC(
                        nameof(EntityFrameworkQueryableExtensions.ExecuteDelete),
                        entityType.DisplayName()));
        }

        // Find the table model that maps to the entity type; there must be exactly one (e.g. no entity splitting).
        var targetTable = entityType.GetTableMappings().ToList() switch
        {
            [] => throw new InvalidOperationException(
                RelationalStrings.ExecuteUpdateDeleteOnEntityNotMappedToTable(entityType.DisplayName())),
            [var singleTableMapping] => singleTableMapping.Table,
            _ => throw new InvalidOperationException(
                RelationalStrings.ExecuteOperationOnEntitySplitting(
                    nameof(EntityFrameworkQueryableExtensions.ExecuteDelete), entityType.DisplayName())),
        };
        var selectExpression = (SelectExpression)source.QueryExpression;

        // Find the table expression in the SelectExpression that corresponds to the projected entity type.

View on GitHub (pinned to dbf9771522)

Solutions

  1. Target a leaf type in the TPC hierarchy (a type with no derived types).
  2. Issue separate ExecuteDelete calls per concrete leaf type, or union them explicitly.
  3. Switch the hierarchy to TPH for full base-type bulk delete support.
  4. Load and delete via SaveChanges if you must delete across the whole TPC hierarchy.

Example fix

// before - Animal is the TPC root, has derived Dog/Cat
await db.Animals.Where(a => a.Id == id).ExecuteDeleteAsync();
// after - delete each leaf explicitly, or query the right leaf
await db.Set<Dog>().Where(d => d.Id == id).ExecuteDeleteAsync();
await db.Set<Cat>().Where(c => c.Id == id).ExecuteDeleteAsync();
Defensive patterns

Strategy: validation

Validate before calling

static bool IsTpcLeaf(IEntityType et)
    => et.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy
       && !et.GetDirectlyDerivedTypes().Any();

if (!IsTpcLeaf(entityType))
    throw new InvalidOperationException("ExecuteDelete on TPC requires a leaf type.");

Try / catch

try { await db.Animals.Where(predicate).ExecuteDeleteAsync(); }
catch (InvalidOperationException ex) when (ex.Message.Contains("TPC mapping strategy"))
{
    // fan out to leaf types explicitly
    await db.Set<Dog>().Where(predicate).ExecuteDeleteAsync();
    await db.Set<Cat>().Where(predicate).ExecuteDeleteAsync();
}

Prevention

When it happens

Trigger: Configuring a hierarchy with .UseTpcMappingStrategy() and calling ExecuteDelete/ExecuteUpdate on a base or intermediate type that has directly derived types.

Common situations: Choosing TPC and then trying to bulk-delete from the root entity; targeting an abstract base or a non-leaf node.

Related errors


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