MV-Register (Multi-Value Register)
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 mvregister.md, and llms.txt lists every page.tree.MvRegister<T>(key) -> MvRegisterAccessor<T>, merge mode LatticeMergeMode.MvRegister.
Semantics
An MV-Register holds a single typed value - but unlike last-writer-wins, when two replicas write concurrently it keeps both values instead of silently discarding one. Your application then reads the conflict set and resolves it however it likes (newest, merge, ask the user).
Each write stamps a causal dot and records the dots it has observed. On merge, a value survives unless the other side has already seen and replaced it; a write that causally follows (observed) an earlier value replaces it. So a value written after seeing both concurrent values collapses the register back to a single entry.
T is serialized with a JSON serializer by default; pass your own
ILatticeSerializer<T> for other formats.
Use it for: a config field, a document title, an order status - a single-valued field where you would rather surface a genuine conflict than lose a write.
Behaviour
Both clusters hold {in-progress, resolved}: a genuine conflict, surfaced rather than lost. A later write that has observed both, such as closed, collapses it back to one value.
Each write records the dots it has observed, and a merge drops only the values the other side observed and replaced.
graph TD
S0["value = 'todo'"] --> A["Cluster A writes 'in-progress'"]
S0 --> B["Cluster B writes 'resolved'"]
A -->|merge: neither observed the other| C["values = { 'in-progress', 'resolved' }"]
B -->|merge| C
C --> D["Cluster A observes both, writes 'closed'"]
D --> E["values = { 'closed' } (dominates both)"]
Example
var status = tree.MvRegister<string>("ticket:7:status");
// Agent A sets the status on this cluster.
await status.SetAsync("agent-A", "in-progress", cancellationToken);
// Meanwhile agent B set it on another cluster, before A's write had reached it.
// Replication delivers B's register; MergeAsync stands in for that here. (Two
// SetAsync calls in a row on one cluster would not conflict: the second write
// would observe the first and replace it.)
var fromB = new MvRegister();
fromB.Set("agent-B", status.Serializer.Serialize("resolved"));
await status.MergeAsync(fromB, cancellationToken);
// Neither write observed the other, so both survive as a conflict set the
// application resolves - last-writer-wins would have dropped one silently.
IReadOnlyList<string> candidates = await status.ValuesAsync(cancellationToken); // in-progress, resolved
// A later write that has observed both collapses the register to one value.
await status.SetAsync("agent-A", "closed", cancellationToken);
See also: OR-Set when you want to keep many values rather than resolve to one, and the CRDT overview.