Chapter 12 · Saving Data: Insert, Update, Delete, Batching, ExecuteUpdate, and ExecuteDelete

ExecuteUpdate and ExecuteDelete: Set-Based DML Without Loading or Tracking Entities

Use ExecuteUpdate and ExecuteDelete for immediate set-based DML, expose tracker staleness and transaction/concurrency boundaries, and repair unsafe mixing with tracked writes.

Advanced130–165 minutesset-based DML + stale-tracker labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

Learning outcomes

ServiceHub sometimes needs to change many rows without loading hundreds of entities—for example, append an operational marker to all work orders matching a narrow predicate, or purge disposable lab notes. ExecuteUpdate and ExecuteDelete express that as relational set-based DML, but they intentionally bypass the change tracker. That efficiency changes the correctness contract.

01

Execute filtered set-based UPDATE/DELETE operations without materializing entities.

02

Explain immediate execution, one round trip per invocation, and lack of tracker synchronization.

03

Reproduce stale tracked state after ExecuteUpdate and repair it with context boundaries/reload.

04

Use rows-affected plus a concurrency predicate when implementing manual optimistic concurrency.

05

Coordinate several direct DML operations with an explicit transaction when atomicity is required.

06

Distinguish EF10 JSON-complex ExecuteUpdate support from provider-independent behavior.

Reproducible baseline

Mandatory labs use .NET SDK 10.0.400, .NET runtime 10.0.11, Microsoft.EntityFrameworkCore/SQLite 10.0.11, dotnet-ef 10.0.11, the disposable servicehub-lab.db, deterministic ServiceHub seed data, and no paid tooling. SQL Server/PostgreSQL native-loader examples are optional provider comparisons. EF Core 11 previews are excluded from the mandatory path.

1. ExecuteUpdate/Delete are query-to-DML operators

csharp · set-based update from a LINQ predicate
var affected = await db.WorkOrders    .Where(w => EF.Property<DateTime>(w, "CreatedUtc") < cutoffUtc)    .ExecuteUpdateAsync(setters => setters        .SetProperty(w => w.Summary, w => w.Summary + " [legacy-review]"), ct);Console.WriteLine($"Rows updated: {affected}");
sql · representative SQLite shape
UPDATE "work_orders" AS "w"SET "summary" = "w"."summary" || ' [legacy-review]'WHERE "w"."created_utc" < @cutoffUtc;

No WorkOrder instance is loaded, tracked, or passed through domain methods. This is excellent when the database operation itself is the intended invariant-preserving operation; it is inappropriate when per-aggregate business rules must run in memory.

2. The deliberate trap: a tracked entity becomes stale

csharp · broken mix of tracked and direct writes
var tracked = await db.WorkOrders.SingleAsync(w => w.Id == id, ct);Console.WriteLine(tracked.Summary); // "Inspect pump"await db.WorkOrders    .Where(w => w.Id == id)    .ExecuteUpdateAsync(s => s        .SetProperty(w => w.Summary, "Updated directly in the database"), ct);Console.WriteLine(tracked.Summary); // still "Inspect pump"tracked.AdvanceRevision();await db.SaveChangesAsync(ct); // may now write based on stale tracked values/state

ExecuteUpdate does not synchronize CurrentValues, OriginalValues, or IsModified. The safest default is a separate short-lived context/unit of work for direct DML. If one context must continue, explicitly reload or clear/requery after understanding the consequences.

csharp · repair with a new unit of work
await using (var direct = await factory.CreateDbContextAsync(ct)){    await direct.WorkOrders        .Where(w => w.Id == id)        .ExecuteUpdateAsync(s => s.SetProperty(w => w.Summary, newSummary), ct);}await using var verify = await factory.CreateDbContextAsync(ct);var fresh = await verify.WorkOrders.AsNoTracking().SingleAsync(w => w.Id == id, ct);

3. ExecuteDelete is immediate and relational constraints still apply

csharp · delete disposable notes, not principals by accident
var deleted = await db.Set<WorkOrderNote>()    .Where(n => n.WorkOrderId == orderId && n.Text.StartsWith("LAB:"))    .ExecuteDeleteAsync(ct);Console.WriteLine($"Rows deleted: {deleted}");

The database executes the DELETE immediately. FK restrictions/cascades from Chapter 05 still apply. EF does not load dependents to perform client-side cascade behavior; therefore a relationship configured as ClientCascade does not magically make an unloaded direct DELETE safe.

Predicate-first safety

For destructive direct DML, print/inspect the predicate and count candidates first in the disposable lab. Never turn an accidental missing Where into a production incident.

4. Several Execute* calls need an explicit transaction for atomicity

csharp · atomic direct-DML sequence
await using var tx = await db.Database.BeginTransactionAsync(ct);await db.Set<WorkOrderNote>()    .Where(n => n.WorkOrderId == orderId && n.Text.StartsWith("LAB:"))    .ExecuteDeleteAsync(ct);await db.WorkOrders    .Where(w => w.Id == orderId)    .ExecuteUpdateAsync(s => s        .SetProperty(w => w.Summary, "Lab notes purged"), ct);await tx.CommitAsync(ct);

Without the explicit transaction, each direct DML invocation is its own immediate database operation. Chapter 14 will cover transaction/isolation/retry semantics in depth; for now, the key point is that “I used one DbContext” is not an atomicity guarantee for multiple Execute* calls.

5. Concurrency becomes explicit rows-affected logic

csharp · manual optimistic predicate
var nextRevision = expectedRevision + 1;var affected = await db.WorkOrders    .Where(w => w.Id == id && w.Revision == expectedRevision)    .ExecuteUpdateAsync(s => s        .SetProperty(w => w.Summary, newSummary)        .SetProperty(w => w.Revision, nextRevision), ct);if (affected == 0){    throw new ConcurrencyConflictException(id, expectedRevision);}

ExecuteUpdate does not automatically apply the tracker's original concurrency token logic because there is no tracked original. Put the token in the predicate and interpret rows affected deliberately. Chapter 13 formalizes conflict-resolution strategies.

6. EF Core 10 adds more set-based update shapes, but providers still matter

EF Core 10 supports ExecuteUpdate over complex types mapped to relational JSON and makes dynamic setter construction easier. This is an EF10 capability, but the resulting JSON update SQL depends on the provider/database. The ServiceHub SQLite mandatory path should inspect actual SQL before assuming the same shape as SQL Server 2025 or PostgreSQL.

Do not infer feature parity

A LINQ setter compiling against EF Core 10 does not prove identical provider translation, JSON function availability, row-count semantics, or performance.

7. Hands-on lab

  1. Reset the SQLite database and count candidate work orders with a no-tracking query.
  2. Run a filtered ExecuteUpdateAsync; record rows affected and generated SQL.
  3. Reproduce stale tracked state by tracking one row first; verify the CLR object does not change automatically.
  4. Repair by using a fresh context, then repeat with ReloadAsync to observe the alternative.
  5. Run a lab-only ExecuteDeleteAsync over notes with a restrictive predicate.
  6. Wrap two direct operations in an explicit transaction and inject an exception before commit; verify rollback.
  7. Implement Revision in the predicate and prove zero rows becomes a manual concurrency signal.

Check your understanding

  1. When does ExecuteUpdate execute?
  2. Does it update tracked CLR objects?
  3. Can two ExecuteUpdate calls be batched into one SaveChanges?
  4. How do you make several direct operations atomic?
  5. How do you implement optimistic concurrency?
  6. Why can ExecuteDelete conflict with ClientCascade expectations?
Review the answers

Immediately when invoked; it does not wait for SaveChanges.

No. The change tracker is not synchronized.

No. Each invocation is its own direct operation/round trip.

Use an explicit database transaction where supported.

Include the expected token in the predicate and inspect rows affected.

It does not load dependents for EF client-side cascade behavior; database constraints govern direct DELETE execution.

8. Production judgment and bridge

Choose ExecuteUpdate/Delete when the operation is naturally set-based and can preserve invariants directly in SQL. Prefer tracked SaveChanges for aggregate/domain behavior and automatic optimistic concurrency. Keep the two models in separate short-lived units of work unless you deliberately reload state. Lesson 4 broadens the decision beyond EF to native loaders and third-party bulk tooling.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n