Table of Contents

Cross-tree atomic writes

This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at cross-tree-atomic-writes.md, and llms.txt lists every page.

Part of Lattice Public API Reference.

IGrainFactory / cluster-client extension methods (LatticeCrossTreeAtomicWriteExtensions) that commit a batch spanning two or more distinct ILattice trees all-or-nothing, with the same atomic-visibility guarantee SetManyAtomicAsync gives within a single tree. A stable operationId is required (no auto-generated overload) because a cross-tree saga touches multiple registries and a stable idempotency key is mandatory for safe retry; it must not be null, empty, or whitespace and must not contain '/'.

Method Signature
SetManyAtomicAsync Task<CrossTreeAtomicWriteOutcome> SetManyAtomicAsync(this IGrainFactory factory, IReadOnlyList<LatticeTreeBatch> batches, string operationId, CancellationToken = default)
BeginAtomicWrite LatticeAtomicWriteBuilder BeginAtomicWrite(this IGrainFactory factory, string operationId)

The fluent builder LatticeAtomicWriteBuilder accumulates per-tree slices and commits them as one cross-tree saga:

Method Signature
ForTree LatticeAtomicWriteBuilder ForTree(string treeId)
Set LatticeAtomicWriteBuilder Set(string key, byte[] value)
Set<T> LatticeAtomicWriteBuilder Set<T>(string key, T value, ILatticeSerializer<T> serializer)
Set<T> (default serializer) LatticeAtomicWriteBuilder Set<T>(string key, T value)
Set (staged CRDT) LatticeAtomicWriteBuilder Set(LatticeStagedCrdtWrite staged)
SetMany (staged CRDT batch) LatticeAtomicWriteBuilder SetMany(IEnumerable<LatticeStagedCrdtWrite> staged)
SetWhere<T> LatticeAtomicWriteBuilder SetWhere<T>(string key, T value, Expression<Func<T, bool>> predicate, ILatticeSerializer<T> serializer)
SetWhere<T> (default serializer) LatticeAtomicWriteBuilder SetWhere<T>(string key, T value, Expression<Func<T, bool>> predicate)
Delete LatticeAtomicWriteBuilder Delete(string key)
CommitAsync Task<CrossTreeAtomicWriteOutcome> CommitAsync(CancellationToken = default)

CommitAsync (and SetManyAtomicAsync) returns CrossTreeAtomicWriteOutcome.Committed when every tree's optional guard passed and all writes committed, or CrossTreeAtomicWriteOutcome.PreconditionFailed when a guard failed and nothing committed on any tree. Before anything is staged it throws ArgumentException when an entry exceeds its tree's LatticeOptions.MaxKeyLength or MaxValueSizeBytes, and LatticeQuotaExceededException when a tree whose slice carries an upsert has reached an enforcing MaxLiveKeys or MaxEstimatedBytes cap. It throws InvalidOperationException if a write fails (after the saga compensates), or LatticeIdempotencyKeyMismatchException (a subclass of InvalidOperationException) if the same operationId is re-submitted with a different tree-set or key-set. A saga state write that loses an optimistic-concurrency (ETag) check makes the affected saga reload its durable state on a fresh activation; the call re-attaches by operationId up to three attempts (after 1 s and 2 s) and, if the conflict persists, throws LatticeStateWriteFailedException with Conflict set - re-submitting the same batch under the same operationId resumes the saga, and its durable decision is never lost or applied twice. When a slice targets a tree declared for cross-cluster replication, that slice must use the tree's declared shape - a plain Set/Delete for an LwwRegister tree, or a staged CRDT Set(LatticeStagedCrdtWrite) for a CRDT-mode tree (see Replication modes - Single shape per tree). Supporting public types: LatticeTreeBatch (per-tree slice: TreeId, Entries, optional Predicate, optional EntryDeltas, optional EntryDeletes), the CrossTreeAtomicWriteOutcome enum (Committed, PreconditionFailed), and LatticeStagedCrdtWrite (the client-side staging token a CRDT accessor's Stage* method returns; see CRDT value-surface accessors). See Atomic Writes - Cross-tree atomic writes.

The Delete(key) builder method stages a retraction (tombstone) delete that rides the all-or-nothing cross-tree batch alongside any sibling upserts, so a re-key projection (a row moving from one view key to another) flips the upsert at the new key and the delete at the old key as a single atomic visibility change. On LatticeTreeBatch the optional EntryDeletes list (when non-null) is aligned 1:1 with Entries: a true slot marks that entry a tombstone delete; the whole list is null for an upsert-only slice.

The Set(LatticeStagedCrdtWrite) overload couples a typed CRDT mutation (prepared by a CRDT accessor's Stage* method) into the cross-tree saga so it commits all-or-nothing alongside sibling LWW writes. The ForTree(...) it is added under must be the same CRDT-mode tree the accessor was obtained from. The staged merged state is stored locally and replicated through the prepared/terminal path, which now also carries the staged typed delta and merge mode to the receiver: the receiver folds the delta into its current visible state on the saga's terminal commit, so concurrent same-key writes from multiple clusters converge by the per-replica typed-delta union (a +5 and a +3 reach 8 on both clusters), identical to the live accessor path. See Atomic Writes - Coupling a CRDT mutation into an atomic write.