dotnet/efcore · error · InvalidOperationException

The operation '{operation}' is being applied on entity type

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

TranslateExecuteUpdate resolves the target table from the setters; if it resolves to a TpcTablesExpression (the multi-table form produced when ExecuteUpdate is applied to a non-leaf TPC entity), the operation is refused. TPC non-leaf types span multiple concrete tables, and a single parametrized UPDATE cannot target them all.

Source

Thrown at src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs:43

    {
        Check.DebugAssert(setters.Count > 0, "Empty setters list");

        // Our source may have IncludeExpressions because of owned entities or auto-include; unwrap these, as they're meaningless for
        // ExecuteUpdate's lambdas. Note that we don't currently support updates across tables.
        source = source.UpdateShaperExpression(new IncludePruner().Visit(source.ShaperExpression));

        var selectExpression = (SelectExpression)source.QueryExpression;

        // Translate the setters: the left (property) selectors get translated to ColumnExpressions, the right (value) selectors to
        // arbitrary SqlExpressions.
        // Note that if the query isn't natively supported, we'll do a pushdown (see PushdownWithPkInnerJoinPredicate below); if that
        // happens, we'll have to re-translate the setters over the new query (which includes a JOIN). However, we still translate here
        // since we need the target table in order to perform the check below.
        var translatedSetters = TranslateSetters(source, setters, out var targetTable);

        if (targetTable is TpcTablesExpression tpcTablesExpression)
        {
            throw new InvalidOperationException(
                RelationalStrings.ExecuteOperationOnTPC(
                    nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate),
                    tpcTablesExpression.EntityType.DisplayName()));
        }

        // Check if the provider has a native translation for the update represented by the select expression.
        // The default relational implementation handles simple, universally-supported cases (i.e. no operators except for predicate).
        // Providers may override IsValidSelectExpressionForExecuteUpdate to add support for more cases via provider-specific UPDATE syntax.
        if (IsValidSelectExpressionForExecuteUpdate(selectExpression, targetTable, out var tableExpression))
        {
            selectExpression.ReplaceProjection([]);
            selectExpression.ApplyProjection();

            return new UpdateExpression(tableExpression, selectExpression, translatedSetters);
        }

        return PushdownWithPkInnerJoinPredicate();

View on GitHub (pinned to 3a2006ef56)

Solutions

  1. Target each concrete leaf subtype separately with its own ExecuteUpdate call.
  2. Switch the hierarchy to TPH or a strategy that yields a single update target.
  3. Load entities and use SaveChanges, or issue raw SQL UPDATEs against each concrete table.

Example fix

// before (TPC non-leaf update -> 598)
await db.Set<TpcBase>()
    .Where(b => b.Active)
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.UpdatedAt, DateTime.UtcNow));
// after (per leaf type)
await db.Set<TpcLeafA>().Where(b => b.Active)
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.UpdatedAt, DateTime.UtcNow));
await db.Set<TpcLeafB>().Where(b => b.Active)
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.UpdatedAt, DateTime.UtcNow));
Defensive patterns

Strategy: validation

Validate before calling

// Refuse ExecuteUpdate on non-leaf TPC types before invoking.
var et = db.Model.FindEntityType(typeof(TEntity))!;
if (et.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy
    && et.GetDirectlyDerivedTypes().Any())
{
    throw new InvalidOperationException(
        $"{et.Name} is a non-leaf TPC type; call ExecuteUpdate per concrete leaf subtype.");
}

Prevention

When it happens

Trigger: context.Set<TpcAbstract>().ExecuteUpdate(s => s.SetProperty(...)) where TpcAbstract is TPC-mapped and has derived types. The setters' entity resolves to the abstract type, producing a TpcTablesExpression that aggregates all concrete subtype tables.

Common situations: Bulk-updating against a TPC root or intermediate node; assuming ExecuteUpdate fans out across subtype tables.

Related errors


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