Bounded bulk mutations¶
EF Core's ExecuteUpdateAsync and ExecuteDeleteAsync change or delete the rows a query selects, on the server, in one atomic call of at most 10,000 rows. Use them to change many entities without loading them, when you do not need the change tracker's per-entity behavior.
var changed = await db.People
.Where(p => ids.Contains(p.Id))
.ExecuteUpdateAsync(set => set
.SetProperty(p => p.Nickname, "Background actor")
.SetProperty(p => p.Born, 2000), cancellationToken);
var removed = await db.People
.Where(p => ids.Contains(p.Id))
.ExecuteDeleteAsync(cancellationToken);
Each call runs immediately and returns the number of affected rows. It works on node roots, native-reference dependents, association edges and identified edges, using the same LINQ filters, ordering and paging.
The change tracker is bypassed¶
Bulk calls skip tracked entity updates, SaveChanges interception and EF's original-value concurrency checks. Entities you already loaded keep their old values, including their row version; read them again in a fresh context.
- A provider-generated
longrow version (IsRowVersion()) advances once per updated row, so a later save of a stale tracked copy fails as usual. - Application concurrency tokens are not checked for you. Put the expected token in the
Whereand, where needed, set the new token explicitly. A stale predicate matches zero rows without throwing.
Setters¶
| Setter | Example | Rule |
|---|---|---|
| Constant or captured value | SetProperty(p => p.Nickname, "updated") |
Converted with the property's EF mapping. Setting a nullable property to null removes it; a required property rejects null. |
| Copy of another property | SetProperty(p => p.Nickname, p => p.Name) |
Same model CLR type. |
| Integer increment | SetProperty(p => p.Age, p => p.Age + 1) |
A property plus or minus a value, where source and target are int or long of the same type, without converters. Arithmetic is checked; overflow aborts the call. |
All setters read the same original row, so copying a property that another setter also changes uses its old value. One call accepts 1 to 64 distinct setters. Unmapped graph properties are preserved.
Refused before transport with Neo4jQueryTranslationException, usually GBQ160: setting a key, a relationship endpoint, a foreign key or a generated property; duplicate setters; computed expressions other than the integer increment; Include, aggregates or projections in the source query; synchronous ExecuteUpdate/ExecuteDelete; and entity types with store-generated properties other than the key or a long row version. Relationship roots keep their endpoint key and identity checks, and native-reference updates cannot move a dependent to another parent.
Atomicity and limits¶
- 10,000 selected rows per call. One extra row is read as a witness; if it exists, the whole call aborts, and nothing is partially committed. Use your own filter and paging for larger work; GraphBridge does not split a call into several commits. The cap bounds rows, not row size, server sorting or time.
- Locked and rechecked. The call checks the required uniqueness constraints, selects candidates, locks them, rechecks every mapped value and then submits one parameterized
UNWINDbatch. A candidate that changed or disappeared in between aborts the call with a concurrency failure; decide deliberately whether to run it again. Physical Neo4j IDs locate candidates only within that transaction and never become EF keys. No predicate or range lock is taken, so this is not serializable isolation against other writers. - Transactions. Without an explicit transaction, the call commits on its own. Inside
BeginTransactionAsync, success leaves commit or rollback to you, and a failure aborts that whole transaction, because Neo4j has no savepoints. There is no automatic retry, and deadlocks surface as failures. - Restrictive deletes. Deletion uses plain
DELETE, never an implicitDETACH DELETE. A node with remaining relationships fails, and the call rolls back. Deleting a native-reference dependent also removes its modeled parent link, as a tracked delete would. Deleting relationships keeps their endpoint nodes. - Cancellation. Checked before transport, between driver operations and while reading the bounded selection. An in-flight command is not abandoned. Cancellation observed before commit rolls back; it cannot undo an acknowledged commit. A lost commit acknowledgement leaves the outcome unknown: see transactions and concurrency.
A non-empty call on plain nodes submits four statements (schema check, selection, lock and recheck, mutation) plus transaction exchanges; relationship and reference roots add a separate identity read. Bulk calls are traced as GraphBridge.Bulk with command tracing.
Try it¶
- Workshop lesson 21 ยท Bulk update and delete: update and delete on the server, a stale tracked copy and a refused key setter.
Evidence: bounded bulk increment; tests for protocol rules, nodes, concurrency and the cap, associations, identified edges, required and optional references. Design: ADR-0038.