---
title: "MV-Register (Multi-Value Register)"
url: "https://nsta1.github.io/Orleans.Lattice/docs/crdt/mvregister.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/docs/crdt/mvregister.md"
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/crdt/llms-full.txt"
---
# MV-Register (Multi-Value Register)

Part of the [CRDTs documentation](readme.md).

`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

Figure: 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.

```mermaid
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

```csharp verify
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](orset.md) when you want to *keep* many values rather than
resolve to one, and the [CRDT overview](readme.md).
