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 TranslateExecuteUpdate when the resolved target table is a TpcTablesExpression - i.e. the entity uses TPC (Table-Per-Concrete-Type) mapping. Unlike ExecuteDelete (which tolerates TPC leaf types), ExecuteUpdate has no support for TPC at all, because an update would have to fan out across multiple concrete tables and EF cannot generate that.

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 dbf9771522)

Solutions

  1. Switch the hierarchy to TPH so ExecuteUpdate is supported.
  2. Load entities and update via SaveChanges (which supports TPC).
  3. Issue ExecuteUpdate per concrete leaf type by querying each DbSet separately, if feasible.
  4. Restructure so the updatable type is not part of a TPC hierarchy.

Example fix

// before
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
await db.Animals.Where(a => a.Id == id)
    .ExecuteUpdateAsync(s => s.SetProperty(a => a.Name, "x"));
// after - TPH supports bulk update
modelBuilder.Entity<Animal>().UseTphMappingStrategy();
await db.Animals.Where(a => a.Id == id)
    .ExecuteUpdateAsync(s => s.SetProperty(a => a.Name, "x"));
// or tracked update
var a = await db.Animals.FindAsync(id);
a.Name = "x";
await db.SaveChangesAsync();
Defensive patterns

Strategy: validation

Validate before calling

var strategy = entityType.GetMappingStrategy();
if (strategy == RelationalAnnotationNames.TpcMappingStrategy)
    throw new InvalidOperationException("ExecuteUpdate is not supported on TPC hierarchies; use TPH or SaveChanges.");

Try / catch

try { await db.Animals.Where(predicate).ExecuteUpdateAsync(setters); }
catch (InvalidOperationException ex) when (ex.Message.Contains("TPC mapping strategy"))
{
    var entities = await db.Animals.Where(predicate).ToListAsync();
    foreach (var a in entities) ApplySetters(a);
    await db.SaveChangesAsync();
}

Prevention

When it happens

Trigger: Configuring a hierarchy with .UseTpcMappingStrategy() and calling ExecuteUpdate on any type (leaf or not) in that hierarchy.

Common situations: Choosing TPC for insert performance and then needing bulk updates; targeting a TPC leaf type with ExecuteUpdate (still unsupported).

Related errors


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