Table of Contents

OR-Map (Observed-Remove Map)

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 ormap.md, and llms.txt lists every page.

tree.OrMap<TKey,TValue>(key) -> OrMapAccessor<TKey,TValue>, merge mode LatticeMergeMode.OrMap.

Semantics

An OR-Map is a dictionary that is a CRDT twice over. Its keys follow add-wins OR-Set semantics, and each value is itself a CRDT that is merged recursively per key. So when two replicas write the same map key concurrently, the values are not overwritten - they are folded together through the value type's own merge.

TValue must implement ICrdt<TValue> and have a parameterless constructor; use any built-in primitive or your own. Because the wire shape is generic, the map's (TKey, TValue) pair must be registered on the host via AddOrMapShape before the silo starts - once for each tree that holds OR-Maps, and every OR-Map in that tree shares that one shape.

Use it for: per-user scores, per-shard aggregates, per-item metadata - a keyed collection where each entry must converge, not just the key set.

Behaviour

Two concurrent OR-Map writes to one key, merged value by value. An order diagram shaped like a diamond. At the bottom the map is empty. Cluster A increments the PN-Counter under the key alice, so its entry holds P[A]=1. Cluster B, at the same time, increments the same key's counter, so its entry holds P[B]=1. Neither state is above the other. Merging keeps the key, add-wins, and merges the two counters as a PN-Counter, so both paths rise to the same top state: alice holds P[A]=1 and P[B]=1, value 2.

Both clusters hold alice with P[A]=1 and P[B]=1: value 2. The values were folded together through the counter's own merge, not overwritten.

Keys merge with add-wins OR-Set semantics, and each value merges recursively through its own CRDT.

graph TD
    subgraph "Cluster A map"
      A["alice -> PnCounter(1)"]
    end
    subgraph "Cluster B map"
      B["alice -> PnCounter(1)"]
    end
    A -->|"key merge: add-wins"| M["alice -> merge(values)"]
    B -->|"value merge: recursive ICrdt"| M
    M --> R["alice -> PnCounter(2)"]

Example

First, register the map's shape on the host for the tree that will hold it (once, at startup):

// One-time host wiring: OR-Maps stored in the "polls" tree map string keys to
// PN-Counter values.
siloBuilder.AddOrMapShape<string, PnCounter>("polls");

If this registration is missing, an OR-Map write to the tree raises LatticeCrdtShapeNotRegisteredException (a subclass of InvalidOperationException) rather than silently mis-dispatching; the API bindings surface it as a client-error precondition (for example gRPC FailedPrecondition). The closed-shape primitives never need this - they resolve through the global registry fallback.

Then read and write it through the typed accessor:

// A map of per-candidate vote tallies, stored under one key of the "polls" tree;
// each value is itself a PN-Counter.
var polls = grainFactory.GetGrain<ILattice>("polls");
var votes = polls.OrMap<string, PnCounter>("election-2026");

// Cluster A records a vote for "alice" by advancing a PN-Counter value.
var tallyA = new PnCounter();
tallyA.Increment("cluster-A");
await votes.SetAsync("alice", "cluster-A", tallyA, cancellationToken);

// A concurrent vote for the same candidate on another cluster does NOT
// overwrite A's write - the two PN-Counter values merge recursively per key.
// Reading back returns the merged counter for that key.
PnCounter? tally = await votes.GetValueAsync("alice", cancellationToken);
long aliceVotes = tally?.Value ?? 0;

See also: the value primitives PN-Counter and OR-Set, and the CRDT overview.