---
title: "CRDT value-surface accessors - Lattice Public API Reference"
url: "https://nsta1.github.io/Orleans.Lattice/docs/lattice/api/crdt-value-surface-accessors.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/docs/lattice/api.md?plain=1#L1888-L2139"
package: "Orleans.Lattice"
version: "9.9.0"
documents: "Orleans.Lattice 9.9.0 (release line 9.9)"
built: "2026-10-04"
all-pages: "https://nsta1.github.io/Orleans.Lattice/llms.txt"
bundle: "https://nsta1.github.io/Orleans.Lattice/docs/lattice/llms-full.txt"
---
# CRDT value-surface accessors

Part of [Lattice Public API Reference](../api.md).

The CRDT value-surface methods on `ILattice` return lightweight,
allocation-free accessors that read and write a single key through the
primitive's natural mutation API instead of forcing callers to hand-roll
byte arrays or CRDT deltas. The underlying state types are CRDTs whose `Merge` is
commutative, associative, and idempotent, so concurrent updates from
multiple replicas converge without coordination. Each accessor writes
through `ApplyCrdtDeltaAsync`, so writing through an accessor whose mode
differs from a tree's declared cross-cluster replication mode throws
`LatticeReplicationModeMismatchException` (see [Replication modes - Single shape per tree](../../lattice.replication/replication-modes.md#single-shape-per-tree)).

`ILattice.Sequence<T>(key)` adds a Replicated Growable Array (RGA)
sequence accessor for collaborative ordered lists and text;
concurrent inserts under the same parent converge on a deterministic
order via the standard RGA descending `(Counter, ReplicaId)`
tie-break, and removes tombstone the targeted node so a later
re-insert against the same parent still resolves correctly.

`ILattice.OrFlag(key)` adds an observed-remove (enable-wins) flag
accessor - the single-element specialisation of the OR-Set. It
tracks a single presence bit (enabled / disabled) that converges
enable-wins under concurrent active-active enable and disable, and
is the minimal observed-remove primitive for composite-key
membership rows such as a tag/key secondary index.

`ILattice.RwFlag(key)` adds the remove-wins (disable-wins) inverse
of the OR-Flag. It tracks the same single presence bit but converges
the *opposite* way under conflict: a concurrent disable that an
enable never observed suppresses the flag, so ties and unobserved
withdrawals resolve to disabled. Reach for it when the safe outcome
of a race is the withdrawn state - a revocation, kill-switch, or
opt-out bit.

> See [`state-primitives.md`](../state-primitives.md) for the
> convergence semantics, merge rules, and example use cases of the
> CRDT primitives - including when to prefer one primitive over
> another and the recursive `ICrdt<TSelf>` contract that lets `OrMap`
> nest other CRDTs as values.

```csharp verify
using Orleans.Lattice;

// Observed-remove set: concurrent adds and removes converge.
await tree.OrSet("tags:42").AddAsync("urgent"u8.ToArray(), replicaId: "siloA");
await tree.OrSet("tags:42").AddAsync("review"u8.ToArray(), replicaId: "siloA");
bool isUrgent = await tree.OrSet("tags:42").ContainsAsync("urgent"u8.ToArray());

// Positive-negative counter: concurrent increments across replicas sum.
await tree.PnCounter("hits:home").IncrementAsync("siloA");
await tree.PnCounter("hits:home").IncrementAsync("siloA", amount: 3);
long hits = await tree.PnCounter("hits:home").ValueAsync();

// Version vector: track per-replica causal history.
await tree.VersionVector("vv:order:1").TickAsync("siloA");
var vv = await tree.VersionVector("vv:order:1").GetAsync();

// Multi-value register: concurrent writes survive as conflict candidates.
await tree.MvRegister<string>("cart:42").SetAsync("siloA", "Alice's cart");
IReadOnlyList<string> candidates = await tree.MvRegister<string>("cart:42").ValuesAsync();

// Observed-remove map of CRDT-typed values: per-key values fold via
// the value CRDT's MergeFrom, so concurrent writes under the same
// map key converge into a single recursively-merged value.
var tagsByUser = tree.OrMap<string, OrSet>("tags-by-user");
var localTags = new OrSet();
localTags.Add("urgent"u8.ToArray(), replicaId: "siloA", counter: 1);
await tagsByUser.SetAsync("alice", "siloA", localTags);
OrSet? aliceTags = await tagsByUser.GetValueAsync("alice");

// Replicated Growable Array (RGA) sequence: collaborative
// ordered list / text. Concurrent inserts under the same
// parent converge on a deterministic order, removes tombstone
// nodes to keep causal stability for re-inserts.
var transcript = tree.Sequence<string>("chat:42");
await transcript.InsertAtAsync(0, "siloA", "Hello");
await transcript.InsertAtAsync(1, "siloA", "World");
IReadOnlyList<string> lines = await transcript.ToListAsync();

// Observed-remove (enable-wins) flag: a single presence bit that
// converges enable-wins under concurrent enable / disable. Ideal
// for composite-key membership rows (a tag/key secondary index).
await tree.OrFlag("tag/urgent/order:42").EnableAsync(replicaId: "siloA");
bool tagged = await tree.OrFlag("tag/urgent/order:42").IsEnabledAsync();
await tree.OrFlag("tag/urgent/order:42").DisableAsync();

// Remove-wins (disable-wins) flag: the inverse of the OR-Flag. A
// concurrent disable beats a concurrent enable, so the flag fails
// closed under conflict. Ideal for revocation / kill-switch bits.
await tree.RwFlag("access/order:42").EnableAsync(replicaId: "siloA");
bool granted = await tree.RwFlag("access/order:42").IsEnabledAsync();
await tree.RwFlag("access/order:42").DisableAsync(replicaId: "siloB");
```

Each accessor is obtained from a `CrdtLatticeExtensions` entry point on
`ILattice`; every entry point rejects a null or empty key:

| Entry point | Returns |
|-------------|---------|
| `OrSet(string key)` | `OrSetAccessor` |
| `PnCounter(string key)` | `PnCounterAccessor` |
| `GCounter(string key)` | `GCounterAccessor` |
| `GSet(string key)` | `GSetAccessor` |
| `VersionVector(string key)` | `VersionVectorAccessor` |
| `MvRegister<T>(string key, ILatticeSerializer<T>? serializer = null)` | `MvRegisterAccessor<T>` |
| `OrMap<TKey, TValue>(string key)` (`TValue : ICrdt<TValue>, new()`) | `OrMapAccessor<TKey, TValue>` |
| `Sequence<T>(string key, ILatticeSerializer<T>? serializer = null)` | `RgaAccessor<T>` |
| `OrFlag(string key)` | `OrFlagAccessor` |
| `RwFlag(string key)` | `RwFlagAccessor` |
| `RwSet(string key)` | `RwSetAccessor` |
| `MaxRegister<T>(string key, Func<T, byte[]> orderKeySelector, ILatticeSerializer<T>? serializer = null)` | `MaxRegisterAccessor<T>` |
| `MinRegister<T>(string key, Func<T, byte[]> orderKeySelector, ILatticeSerializer<T>? serializer = null)` | `MinRegisterAccessor<T>` |

`CrdtLatticeExtensions` also carries two batched OR-Flag helpers.
`EnableManyAsync(IReadOnlyCollection<string> keys, string replicaId, CancellationToken cancellationToken = default)`
enables many flags with one batched read plus one `ApplyCrdtDeltaManyAsync`
call instead of two round trips per key; like the underlying batch it is not
atomic, and a retry converges. `StageEnableManyAsync` (same parameters) stages
the same enables as `LatticeStagedCrdtWrite` tokens for
`LatticeAtomicWriteBuilder.SetMany`, minting every delta from one batched read.
Both require a non-empty `replicaId`, exactly as the per-key `EnableAsync` does,
and throw `ArgumentException` for an empty one.
See [OR-Flag - Marking many flags at once](../../crdt/orflag.md#marking-many-flags-at-once).

| Accessor | Method | Description |
|----------|--------|-------------|
| `OrSetAccessor` | `Task<OrSet> GetAsync()` | Reads the current set; returns an empty `OrSet` when absent or tombstoned. |
| `OrSetAccessor` | `Task AddAsync(byte[] element, string replicaId)` | Adds `element` with a fresh causal dot. Concurrent adds from other replicas survive a later remove that did not observe them. |
| `OrSetAccessor` | `Task RemoveAsync(byte[] element)` | Tombstones every dot currently observed for `element`. A no-op when the element is absent. |
| `OrSetAccessor` | `Task<bool> ContainsAsync(byte[] element)` | Returns `true` when `element` is a member of the set. |
| `OrSetAccessor` | `Task MergeAsync(OrSet other)` | Merges `other` into the stored state, applied as one typed delta. |
| `PnCounterAccessor` | `Task<PnCounter> GetAsync()` | Reads the current counter state. |
| `PnCounterAccessor` | `Task<long> ValueAsync()` | Reads the current scalar value. |
| `PnCounterAccessor` | `Task IncrementAsync(string replicaId, long amount = 1)` | Advances the positive component for `replicaId`. `amount` must be non-negative. Throws `OverflowException`, and writes nothing, when the advance would take the component past `long.MaxValue`. |
| `PnCounterAccessor` | `Task DecrementAsync(string replicaId, long amount = 1)` | Advances the negative component for `replicaId`. `amount` must be non-negative. Throws `OverflowException`, and writes nothing, when the advance would take the component past `long.MaxValue`. |
| `PnCounterAccessor` | `Task MergeAsync(PnCounter other)` | Merges `other` into the stored state, applied as one typed delta. |
| `GCounterAccessor` | `Task<GCounter> GetAsync()` | Reads the current grow-only counter state. |
| `GCounterAccessor` | `Task<long> ValueAsync()` | Reads the current scalar value: the sum of all replica components. |
| `GCounterAccessor` | `Task IncrementAsync(string replicaId, long amount = 1)` | Advances the grow-only component for `replicaId`. `amount` must be non-negative. Throws `OverflowException`, and writes nothing, when the advance would take the component past `long.MaxValue`. |
| `GCounterAccessor` | `Task MergeAsync(GCounter other)` | Merges `other` into the stored state by pointwise-max per replica. |
| `GSetAccessor` | `Task<GSet> GetAsync()` | Reads the current grow-only set state. |
| `GSetAccessor` | `Task AddAsync(byte[] element)` | Adds `element`; duplicate adds are idempotent. |
| `GSetAccessor` | `Task<bool> ContainsAsync(byte[] element)` | Returns `true` when `element` is in the set. |
| `GSetAccessor` | `Task<IReadOnlyList<byte[]>> ToListAsync()` | Reads members in deterministic order. |
| `GSetAccessor` | `Task MergeAsync(GSet other)` | Merges `other` into the stored state by set union. |
| `VersionVectorAccessor` | `Task<VersionVector> GetAsync()` | Reads the current vector state. |
| `VersionVectorAccessor` | `Task TickAsync(string replicaId)` | Advances the entry for `replicaId` and persists the result. |
| `VersionVectorAccessor` | `Task MergeAsync(VersionVector other)` | Merges `other` into the stored state, applied as one typed delta. |
| `MvRegisterAccessor<T>` | `Task<MvRegister> GetAsync()` | Reads the raw register state, including every dot-tagged entry. |
| `MvRegisterAccessor<T>` | `Task<IReadOnlyList<T>> ValuesAsync()` | Returns the live deserialised values. A single-valued register returns one element; a concurrently-written register returns every conflict candidate in deterministic order. |
| `MvRegisterAccessor<T>` | `Task SetAsync(string replicaId, T value)` | Writes `value` from `replicaId`. Drops every dot the writer observed and mints a fresh one - concurrent writes from other replicas survive the next merge. |
| `MvRegisterAccessor<T>` | `Task MergeAsync(MvRegister other)` | Merges `other` into the stored state, applied as one typed delta. Entries observed in only one side are preserved; pointwise-max is applied to the dot context. |
| `OrMapAccessor<TKey, TValue>` | `Task<OrMap<TKey, TValue>> GetAsync()` | Reads the current map state. |
| `OrMapAccessor<TKey, TValue>` | `Task<TValue?> GetValueAsync(TKey mapKey)` | Returns the lattice-merged value at `mapKey`, or `null` when the key is absent or every observed dot has been tombstoned. |
| `OrMapAccessor<TKey, TValue>` | `Task<bool> ContainsKeyAsync(TKey mapKey)` | Returns `true` when `mapKey` has at least one live (un-tombstoned) dot. |
| `OrMapAccessor<TKey, TValue>` | `Task SetAsync(TKey mapKey, string replicaId, TValue value)` | Writes `value` at `mapKey` from `replicaId`, minting a fresh causal dot. Concurrent writes survive the next merge and are folded into a single per-key value via `ICrdt<TValue>.MergeFrom`. |
| `OrMapAccessor<TKey, TValue>` | `Task RemoveAsync(TKey mapKey)` | Tombstones every dot currently observed for `mapKey`. Concurrent writes on other replicas survive the next merge (add-wins). |
| `OrMapAccessor<TKey, TValue>` | `Task MergeAsync(OrMap<TKey, TValue> other)` | Merges `other` into the stored state, applied as one typed delta. Per-key values are folded recursively through `TValue`'s `MergeFrom`. |
| `RgaAccessor<T>` | `Task<Rga> GetAsync()` | Reads the raw sequence state, including tombstoned nodes preserved for causal stability. |
| `RgaAccessor<T>` | `Task<IReadOnlyList<T>> ToListAsync()` | Returns the live values in resolved in-order projection (descending `(Counter, ReplicaId)` sibling tie-break). |
| `RgaAccessor<T>` | `Task<OrSetDot> InsertAtAsync(int index, string replicaId, T value)` | Inserts `value` at the visible position `index` in the materialised projection. Index `0` inserts at the head; an index equal to the count appends at the tail. Returns the new node's stable cursor dot. |
| `RgaAccessor<T>` | `Task<OrSetDot> InsertAfterAsync(OrSetDot parentDot, string replicaId, T value)` | Inserts as a child of `parentDot` (or `Rga.Root` for a top-level insert). Useful for tooling that captured a stable cursor identity from a previous read. |
| `RgaAccessor<T>` | `Task RemoveAtAsync(int index)` | Tombstones the live node at the visible position `index`. |
| `RgaAccessor<T>` | `Task RemoveAsync(OrSetDot dot)` | Tombstones the node identified by `dot`. A no-op when the dot is absent or already tombstoned. |
| `RgaAccessor<T>` | `Task MergeAsync(Rga other)` | Merges `other` into the stored state, applied as one typed delta. |
| `OrFlagAccessor` | `Task<OrFlag> GetAsync()` | Reads the current flag state; returns a disabled `OrFlag` when absent or tombstoned. |
| `OrFlagAccessor` | `Task<bool> IsEnabledAsync()` | Returns `true` when the flag is currently enabled. |
| `OrFlagAccessor` | `Task EnableAsync(string replicaId)` | Enables the flag with a fresh causal dot. A concurrent enable on another replica survives a disable that did not observe it (enable-wins). |
| `OrFlagAccessor` | `Task DisableAsync()` | Tombstones every enable dot currently observed. A no-op when the flag is not enabled. |
| `OrFlagAccessor` | `Task MergeAsync(OrFlag other)` | Merges `other` into the stored state, applied as one typed delta. |
| `RwFlagAccessor` | `Task<RwFlag> GetAsync()` | Reads the current flag state; returns a disabled `RwFlag` when absent or tombstoned. |
| `RwFlagAccessor` | `Task<bool> IsEnabledAsync()` | Returns `true` when at least one enable dot survives and no live disable suppresses it. |
| `RwFlagAccessor` | `Task EnableAsync(string replicaId)` | Mints a fresh enable dot and tombstones every disable dot currently observed. A concurrent disable the enabler never saw still suppresses the flag (remove-wins). |
| `RwFlagAccessor` | `Task DisableAsync(string replicaId)` | Mints a fresh disable dot. Additive - the disable survives until an enable observes and tombstones it. |
| `RwFlagAccessor` | `Task MergeAsync(RwFlag other)` | Merges `other` into the stored state, applied as one typed delta. |
| `RwSetAccessor` | `Task<RwSet> GetAsync()` | Reads the current remove-wins set state. |
| `RwSetAccessor` | `Task AddAsync(byte[] element, string replicaId)` | Adds `element` with a fresh causal dot and cancels observed removes. Concurrent unobserved removes still win. |
| `RwSetAccessor` | `Task RemoveAsync(byte[] element, string replicaId)` | Mints a fresh remove dot. A concurrent add that did not observe it is suppressed. |
| `RwSetAccessor` | `Task<bool> ContainsAsync(byte[] element)` | Returns `true` when `element` has an add dot and no live remove dot. |
| `RwSetAccessor` | `Task<IReadOnlyList<byte[]>> ToListAsync()` | Reads the current live members. |
| `RwSetAccessor` | `Task MergeAsync(RwSet other)` | Merges add, remove, and tombstone dots from `other`. |
| `MaxRegisterAccessor<T>` | `Task SetAsync(T value)` | Proposes `value`; the register advances only if its order key is greater than the current key. |
| `MaxRegisterAccessor<T>` | `Task<T?> GetAsync()` | Reads the greatest value seen, or `default` when never written. |
| `MaxRegisterAccessor<T>` | `Task<bool> HasValueAsync()` | Returns whether the register has been written at least once. |
| `MaxRegisterAccessor<T>` | `Task<BoundedRegister> GetRegisterAsync()` | Reads the raw bounded-register state. |
| `MaxRegisterAccessor<T>` | `Task MergeAsync(BoundedRegister other)` | Merges `other` under the max direction. |
| `MinRegisterAccessor<T>` | `Task SetAsync(T value)` | Proposes `value`; the register advances only if its order key is less than the current key. |
| `MinRegisterAccessor<T>` | `Task<T?> GetAsync()` | Reads the smallest value seen, or `default` when never written. |
| `MinRegisterAccessor<T>` | `Task<bool> HasValueAsync()` | Returns whether the register has been written at least once. |
| `MinRegisterAccessor<T>` | `Task<BoundedRegister> GetRegisterAsync()` | Reads the raw bounded-register state. |
| `MinRegisterAccessor<T>` | `Task MergeAsync(BoundedRegister other)` | Merges `other` under the min direction. |

Each accessor's primary write also has a per-entry time-to-live overload that
takes a `TimeSpan ttl` after the write's own arguments (ahead of the trailing
`CancellationToken` and `maxAttempts`): `OrSetAccessor.AddAsync`,
`PnCounterAccessor.IncrementAsync` and `GCounterAccessor.IncrementAsync` (where
`amount` is then required), `GSetAccessor.AddAsync`,
`VersionVectorAccessor.TickAsync`, `MvRegisterAccessor<T>.SetAsync`,
`OrMapAccessor<TKey, TValue>.SetAsync`, `RgaAccessor<T>.InsertAtAsync`,
`OrFlagAccessor.EnableAsync`, `RwFlagAccessor.EnableAsync`,
`RwSetAccessor.AddAsync`, and the bounded registers' `SetAsync`. The expiry
converges exactly as for the `ApplyCrdtDeltaAsync` TTL overload; see
[TTL - Per-entry TTL on CRDT writes](../ttl.md#per-entry-ttl-on-crdt-writes).

Accessors that support cross-tree staged CRDT writes expose a `Stage*`
counterpart for their live mutators. A `Stage*` method reads the key's
current snapshot once, mints
the typed CRDT delta **once** (the same dot-minting logic the live
mutator uses), folds the delta into the snapshot to produce the merged
state, and returns a `LatticeStagedCrdtWrite` carrying the key, the
serialized merged state, and the serialized typed delta. It performs no
durable write - hand the token to
`LatticeAtomicWriteBuilder.Set(LatticeStagedCrdtWrite)` under the
matching CRDT-mode tree so the mutation rides a cross-tree atomic write.
See [Atomic Writes - Coupling a CRDT mutation into an atomic write](../atomic-writes.md#coupling-a-crdt-mutation-into-an-atomic-write).

| Accessor | Staging method |
|----------|----------------|
| `OrSetAccessor` | `Task<LatticeStagedCrdtWrite> StageAddAsync(byte[] element, string replicaId, CancellationToken = default)` |
| `OrSetAccessor` | `Task<LatticeStagedCrdtWrite> StageRemoveAsync(byte[] element, CancellationToken = default)` |
| `PnCounterAccessor` | `Task<LatticeStagedCrdtWrite> StageIncrementAsync(string replicaId, long amount = 1, CancellationToken = default)` |
| `PnCounterAccessor` | `Task<LatticeStagedCrdtWrite> StageDecrementAsync(string replicaId, long amount = 1, CancellationToken = default)` |
| `GCounterAccessor` | `Task<LatticeStagedCrdtWrite> StageIncrementAsync(string replicaId, long amount = 1, CancellationToken = default)` |
| `GSetAccessor` | `Task<LatticeStagedCrdtWrite> StageAddAsync(byte[] element, CancellationToken = default)` |
| `VersionVectorAccessor` | `Task<LatticeStagedCrdtWrite> StageTickAsync(string replicaId, CancellationToken = default)` |
| `MvRegisterAccessor<T>` | `Task<LatticeStagedCrdtWrite> StageSetAsync(T value, string replicaId, CancellationToken = default)` |
| `RgaAccessor<T>` | `Task<LatticeStagedCrdtWrite> StageInsertAtAsync(int index, string replicaId, T value, CancellationToken = default)` |
| `RgaAccessor<T>` | `Task<LatticeStagedCrdtWrite> StageInsertAfterAsync(OrSetDot parentDot, string replicaId, T value, CancellationToken = default)` |
| `RgaAccessor<T>` | `Task<LatticeStagedCrdtWrite> StageRemoveAtAsync(int index, CancellationToken = default)` |
| `RgaAccessor<T>` | `Task<LatticeStagedCrdtWrite> StageRemoveAsync(OrSetDot dot, CancellationToken = default)` |
| `OrFlagAccessor` | `Task<LatticeStagedCrdtWrite> StageEnableAsync(string replicaId, CancellationToken = default)` |
| `OrFlagAccessor` | `Task<LatticeStagedCrdtWrite> StageDisableAsync(CancellationToken = default)` |
| `RwFlagAccessor` | `Task<LatticeStagedCrdtWrite> StageEnableAsync(string replicaId, CancellationToken = default)` |
| `RwFlagAccessor` | `Task<LatticeStagedCrdtWrite> StageDisableAsync(string replicaId, CancellationToken = default)` |

`LatticeStagedCrdtWrite` is a client-side staging token (`Key`,
`Value`, `Delta`) consumed synchronously by the atomic-write builder. It
never crosses the wire and is not an Orleans-serializable type.

No accessor runs a compare-and-swap retry loop. Every write - `MergeAsync`
included - reads the current state at most once to mint its typed delta and
then applies it with a single `ApplyCrdtDeltaAsync` call; the owning leaf is
the single writer for the key and folds the delta. The trailing
`int maxAttempts` parameter every mutator accepts (default
`DefaultMaxAttempts = 16`) is kept for binary compatibility: it is validated
(a value below 1 throws `ArgumentOutOfRangeException`) but no longer drives a
retry. Every accessor also exposes the `Lattice` and `Key` it is bound to,
plus `Serializer` on `MvRegisterAccessor<T>`, `RgaAccessor<T>`, and the
bounded registers, which also expose `OrderKeySelector`. Values are
JSON-serialized via `JsonLatticeSerializer<T>` unless a serializer is
supplied, so the bytes are inspectable through `ILattice.GetAsync`.

Previous: [Cross-tree atomic writes](cross-tree-atomic-writes.md). Next: [LatticeOptions](latticeoptions.md). Contents: [Lattice Public API Reference](../api.md).
