Table of Contents

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

Two concurrent MV-Register writes, both kept at their join. An order diagram shaped like a diamond. At the bottom the register holds todo. Cluster A, having seen todo, writes in-progress. Cluster B, having also seen only todo, writes resolved. Neither write observed the other, so neither state is above the other. Merging keeps every value the other side has not observed and replaced, so both paths rise to the same top state, holding both in-progress and resolved.

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.