dotnet/efcore · error · InvalidOperationException

ExecuteOperationOnKeylessEntityTypeWithUnsupportedOperator

ExecuteOperationOnKeylessEntityTypeWithUnsupportedOperator

Error message

The operation '{operation}' cannot be performed on keyless entity type '{entityType}', since it contains an operator not natively supported by the database provider.

What it means

EF Core's ExecuteUpdate usually renders a single UPDATE...WHERE. When the query contains an operator the provider cannot natively express inside an UPDATE (join, group by, distinct, ordering, limit/offset), EF performs a 'pushdown': it wraps the query as a subquery and INNER JOINs it back to the target table on the primary key columns (see PushdownWithPkInnerJoinPredicate at line 62). A keyless entity type (HasNoKey, a view, or a raw SQL root) has no primary key, so the join predicate cannot be built and this error is thrown at line 96-102.

Source

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

            if (!IsMemberAccess(
                    RemapLambdaBody(source, firstPropertySelector).UnwrapTypeConversion(out _),
                    RelationalDependencies.Model,
                    out var baseExpression)
                || _sqlTranslator.TranslateProjection(baseExpression) is not StructuralTypeShaperExpression shaper)
            {
                throw new InvalidOperationException(RelationalStrings.InvalidPropertyInSetProperty(firstPropertySelector));
            }

            // TODO: #36336
            if (shaper.StructuralType is not IEntityType entityType)
            {
                throw new InvalidOperationException(
                    RelationalStrings.ExecuteUpdateSubqueryNotSupportedOverComplexTypes(shaper.StructuralType.DisplayName()));
            }

            if (entityType.FindPrimaryKey() is not { } pk)
            {
                throw new InvalidOperationException(
                    RelationalStrings.ExecuteOperationOnKeylessEntityTypeWithUnsupportedOperator(
                        nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate),
                        entityType.DisplayName()));
            }

            // Generate the INNER JOIN around the original query, on the PK properties.
            var outer = (ShapedQueryExpression)Visit(new EntityQueryRootExpression(entityType));
            var inner = source;
            var outerParameter = Expression.Parameter(entityType.ClrType);
            var outerKeySelector = Expression.Lambda(outerParameter.CreateKeyValuesExpression(pk.Properties), outerParameter);
            var firstPropertyLambdaExpression = setters[0].PropertySelector;
            var entitySource = GetEntitySource(RelationalDependencies.Model, firstPropertyLambdaExpression.Body);
            var innerKeySelector = Expression.Lambda(
                entitySource.CreateKeyValuesExpression(pk.Properties), firstPropertyLambdaExpression.Parameters);

            var joinPredicate = CreateJoinPredicate(outer, outerKeySelector, inner, innerKeySelector);

            Check.DebugAssert(joinPredicate != null, "Join predicate shouldn't be null");

View on GitHub (pinned to dbf9771522)

Solutions

  1. Remove the operator that forces pushdown (OrderBy, Take, Skip, Distinct, GroupBy) so the provider renders a native UPDATE over the keyless type.
  2. Define a primary key on the entity type (HasKey in OnModelCreating or by convention) so the pushdown INNER JOIN has join columns.
  3. If the entity is backed by a view, map it to a real mutable table (ToTable) instead of ToView.
  4. Fall back to loading the entities and using SaveChanges, or issue the update via raw SQL (FromSql/ExecuteSqlRaw).

Example fix

// before
db.OrderSummaries.HasNoKey().ToView("OrderSummaries")
    .OrderBy(v => v.Total).Take(100)
    .ExecuteUpdate(s => s.SetProperty(v => v.Archived, true));

// after (drop pushdown-forcing operator so native UPDATE is used)
db.OrderSummaries
    .Where(v => v.Archived == false)
    .ExecuteUpdate(s => s.SetProperty(v => v.Archived, true));
Defensive patterns

Strategy: validation

Validate before calling

// Before ExecuteUpdate, confirm the entity has a key and the query has no pushdown-forcing operator.
var entityType = db.Model.FindEntityType(typeof(MyView))!;
bool keyless = entityType.FindPrimaryKey() is null;
bool hasPushdownOperator = /* inspect your query chain for OrderBy/Take/Skip/Distinct/GroupBy/Join */;
if (keyless && hasPushdownOperator)
    throw new InvalidOperationException("ExecuteUpdate will fail: keyless entity + pushdown operator.");

Try / catch

try { await query.ExecuteUpdateAsync(s => ...); }
catch (InvalidOperationException ex) when (ex.Message.Contains("keyless entity type"))
{ /* fall back to SaveChanges or raw SQL, or remove the pushdown operator */ }

Prevention

When it happens

Trigger: Calling ExecuteUpdate over a keyless entity type (HasNoKey or ToView-only) combined with any operator that makes IsValidSelectExpressionForExecuteUpdate return false: OrderBy, Take/Skip (Limit/Offset), Distinct, GroupBy, Having, a multi-table FROM that is not a liftable INNER/CROSS JOIN. The error surfaces only when pushdown is required, not for trivial predicates.

Common situations: Bulk-updating a read model mapped with HasNoKey() or ToView(); chaining .OrderBy(...).Take(...).ExecuteUpdate(...) on a view-backed entity; querying through FromSql/TVFs whose shape is keyless and then updating; migrating a SaveChanges loop to ExecuteUpdate on an entity that was modeled keyless for query-only use.

Related errors


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