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.
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.
Execute filtered set-based UPDATE/DELETE operations without materializing entities.
Explain immediate execution, one round trip per invocation, and lack of tracker synchronization.
Reproduce stale tracked state after ExecuteUpdate and repair it with context boundaries/reload.
Use rows-affected plus a concurrency predicate when implementing manual optimistic concurrency.
Coordinate several direct DML operations with an explicit transaction when atomicity is required.
Distinguish EF10 JSON-complex ExecuteUpdate support from provider-independent behavior.
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
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}");
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
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.
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
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.
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
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
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.
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
- Reset the SQLite database and count candidate work orders with a no-tracking query.
-
Run a filtered
ExecuteUpdateAsync; record rows affected and generated SQL. - Reproduce stale tracked state by tracking one row first; verify the CLR object does not change automatically.
-
Repair by using a fresh context, then repeat with
ReloadAsyncto observe the alternative. -
Run a lab-only
ExecuteDeleteAsyncover notes with a restrictive predicate. - Wrap two direct operations in an explicit transaction and inject an exception before commit; verify rollback.
- Implement Revision in the predicate and prove zero rows becomes a manual concurrency signal.
Check your understanding
- When does ExecuteUpdate execute?
- Does it update tracked CLR objects?
- Can two ExecuteUpdate calls be batched into one SaveChanges?
- How do you make several direct operations atomic?
- How do you implement optimistic concurrency?
- 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
- ExecuteUpdate and ExecuteDelete - EF Core — immediate execution, tracking, transactions, concurrency, limitations.
- Saving Data - EF Core — comparison with SaveChanges.
- What’s New in EF Core 10 — JSON complex ExecuteUpdate and setter improvements.
- Using Transactions - EF Core — explicit transaction coordination.
- Handling Concurrency Conflicts - EF Core — optimistic concurrency concepts used by the rows-affected pattern.