---
title: "Materialised views - Lattice Public API Reference"
url: "https://nsta1.github.io/Orleans.Lattice/docs/lattice/api/materialised-views.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/docs/lattice/api.md?plain=1#L2816-L2981"
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"
---
# Materialised views

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

A **materialised view** is an asynchronous, eventually-consistent projection of a
source tree, maintained by tailing that tree's WAL. It needs a WAL-backed lattice
(`AddLattice`) and `AddLatticeViews(...)` (which folds in `AddWalCursorRegistry()`
so the view can pin the source WAL); it does **not** require the replication
package's `AddLatticeReplication` (that is only for the cross-cluster `ShipView`
mode). See [Materialised views](../materialised-views.md) for the full guide and
[configuration](../configuration/materialised-view-options.md) for the per-view
options. Runtime-created stateful views use a host-registered
`LatticeRuntimeViewProjectionDescriptor`; its opaque payload is capped at 64 KiB,
and activation fails closed unless the provider reconstructs the exact persisted
projection shape and version. Filter-only predicate views are captured
automatically. See the durability section of the full guide for provider examples
and legacy-registration migration.

```csharp verify
using Microsoft.Extensions.DependencyInjection;

// Delegate-backed runtime shapes use stable, host-registered provider keys.
siloBuilder.AddLatticeViews(views =>
{
    views.AddRuntimeProjectionProvider(
        "app.age-sum.v1",
        (_, context) => new LatticeViewDefinition(
            context.ViewName,
            AggregationLatticeViewProjection.Create<User>(
                AggregationKind.Sum,
                groupKeySelector: u => u.Name,
                selectorVersion: "sum-age-v1",
                valueSelector: u => u.Age)));
    views.AddRuntimeProjectionProvider(
        "app.name-trail.v1",
        (_, context) => new LatticeViewDefinition(
            context.ViewName,
            LatticeFoldProjection.Create<User, string>(
                groupKeySelector: u => u.Age.ToString(),
                initial: () => string.Empty,
                apply: (trail, key, u, hlc) => trail.Length == 0 ? u.Name : trail + "," + u.Name,
                foldVersion: "name-trail-v1")));
});

// ILatticeViewFactory is registered by AddLatticeViews; here it is resolved from
// the service provider for illustration. Prefer constructor injection in practice.
var viewFactory = client.ServiceProvider.GetRequiredService<ILatticeViewFactory>();
var people = grainFactory.GetGrain<ILattice>("people");

// Filter / re-project view.
ILatticeView adults = await viewFactory.CreateAsync(
    people,
    "adults",
    new LatticeViewDefinition("adults", new PredicateLatticeViewProjection(
        LatticePredicateTranslator.Translate<User>(u => u.Age >= 18))));

// Typed read: deserialize the view value to T (defaults to JsonLatticeSerializer<T>).
User? alice = await adults.GetAsync<User>("alice", cancellationToken);
long lag = await adults.GetLagAsync(cancellationToken);

// Read-your-write barrier, then a content digest.
await adults.WaitForSourceHeadAsync(TimeSpan.FromSeconds(5), cancellationToken);
ViewDigest digest = await adults.ComputeDigestAsync(cancellationToken);

// Aggregation view: one reduced value per group.
ILatticeView ageByName = await viewFactory.CreateAsync(
    people,
    "age-sum-by-name",
    new LatticeRuntimeViewProjectionDescriptor("app.age-sum.v1", []));

// Typed aggregate read: null means the group has no live members.
double total = await ageByName.GetAggregateDoubleAsync("Alice", cancellationToken) ?? 0;

// Folded (custom-reducer) view: a user-defined, HLC-ordered fold per group.
ILatticeView nameTrail = await viewFactory.CreateAsync(
    people,
    "name-trail-by-age",
    new LatticeRuntimeViewProjectionDescriptor("app.name-trail.v1", []));

// The accumulator is stored under the bare group key; read it with GetAsync<T>.
string? names = await nameTrail.GetAsync<string>("30", cancellationToken);
```

## Surface

| Type | Role |
|------|------|
| `AddLatticeViews(configure?)` | Silo-builder registration for the view catalog, factory, and hosted maintainer. Part of the core `Orleans.Lattice` package. Declares startup views through `LatticeViewRegistrationBuilder`: `AddView` / `AddAggregationView` / `AddFoldedView`, each with an instance or a `Func<IServiceProvider, ...>` factory overload, and `AddRuntimeProjectionProvider(providerKey, provider)` for the providers runtime-created stateful views are rebuilt from. |
| `ConfigureLatticeView(viewName, configure)` | Configures the named `LatticeViewOptions` instance for one view. `viewName` is required - a `null` or empty name throws, and there is no no-name overload. |
| `ILatticeViewFactory` | Injected entry point: `CreateAsync(source, viewName, definition, ct?)` persists a durable runtime registration before returning an `ILatticeView` handle; the synchronous `Create(...)` compatibility path persists and activates in the background. `CreateAsync(source, viewName, runtimeProjection, ct?)` (and its synchronous `Create` counterpart, whose default interface implementation throws `NotSupportedException`) creates a view from a `LatticeRuntimeViewProjectionDescriptor` alone; `GetAsync(viewName, ct?)` opens an existing view by name without re-supplying its source or projection; `DeleteAsync(viewName, ct?)` tears a runtime view down completely and is idempotent. Registered as a singleton by `AddLatticeViews`. |
| `ILatticeView` | The view handle: `ViewName`, `GetAsync`, `CountAsync`, `KeysAsync`, `EntriesAsync`, `GetLagAsync`, `RebuildAsync`, `ReconcileAsync`, `ComputeDigestAsync`, `WaitForSourceHlcAsync`, `WaitForSourceHeadAsync`. The raw `KeysAsync` / `EntriesAsync` streams omit reconnect handling; use `LatticeViewExtensions.ScanKeysAsync` / `ScanEntriesAsync` for long-running scans. |
| `LatticeViewExtensions` | Resilient forward view-scan wrappers over `ILatticeView`: `ScanKeysAsync(string? startInclusive, string? endExclusive, int? maxAttempts)` and `ScanEntriesAsync(...)` transparently recover from `EnumerationAbortedException` (remote enumerator reclaimed mid-walk by grain deactivation, idle expiry, or a rebuild's shadow-swap) by resuming from the successor of the last-yielded key - no gaps, no duplicates, ordering preserved. They pass `startInclusive` through unchanged so the view's reserved-floor semantics still apply, and share the `ILattice` wrappers' bounded reconnect budget (`maxAttempts`, default `LatticeExtensions.DefaultScanReconnectAttempts = 8`). Recommended surface for long-running view scans. |
| `TypedLatticeViewExtensions` | Typed read helpers over `ILatticeView`: `GetAsync<T>` / `EntriesAsync<T>` (deserialize via `ILatticeSerializer<T>`, default `JsonLatticeSerializer<T>`), the resilient `ScanEntriesAsync<T>` (typed layer over `LatticeViewExtensions.ScanEntriesAsync`), and `GetAggregateDoubleAsync` / `GetAggregateInt64Async` (decode aggregate values via `LatticeAggregationValue`). |
| `LatticeViewDefinition` | Pairs a view name with either an `ILatticeViewProjection` (filter / re-project) or an `ILatticeAggregationProjection` (aggregation), optionally marked `Accumulative` (append-only) and carrying the `RuntimeProjection` descriptor it is persisted by; read back through `ViewName`, `Projection`, `AggregationProjection`, `Accumulative`, and `RuntimeProjection`. |
| `ILatticeViewProjection` / `PredicateLatticeViewProjection` | Filter / re-project projection: a predicate, optional value transform, and optional injective key re-map. `ProjectionVersion` is a structural hash that drives rebuild-on-change. `Create<T>(...)` builds one whose value transform runs against a deserialized `T` (defaulting to `JsonLatticeSerializer<T>`). |
| `ILatticeAggregationProjection` / `AggregationLatticeViewProjection` | Aggregation projection: an `AggregationKind`, group-key selector, selector-version tag, and the value / member selector the kind needs. `Create<T>(...)` builds one whose selectors run against a deserialized `T` (defaulting to `JsonLatticeSerializer<T>`). |
| `ILatticeFoldProjection` / `LatticeFoldProjection` | Custom-reducer aggregation projection (`AggregationKind.Fold`): a group-key selector plus a fold seed (`Initial`) and step (`Apply(accumulator, sourceKey, value, hlc)`) applied over a group's surviving members in ascending source-HLC order, re-folded on every change. `Create<TValue, TAccumulator>(...)` builds one whose delegates run against a deserialized `TValue` / `TAccumulator` (defaulting to `JsonLatticeSerializer<T>`). A `foldVersion` tag drives rebuild-on-change. |
| `AggregationKind` | `Count`, `Sum`, `Min`, `Max`, `SetUnion`, `Fold`. |
| `ViewWrite` / `ViewWriteKind` | The SPI value a projection's `Project(...)` yields against the view tree: `Upsert`, `Delete`, `RangeDelete` (a key-preserving projection's slice of an unconstrained source range delete), `RangeReconcile` (re-derive a re-keyed range from source state), or the reserved `CrdtDelta`, which no maintainer emits or applies yet. Built with `ViewWrite.Upsert` / `Delete` / `RangeDelete` / `RangeReconcile`; members `Kind`, `Key`, `EndKey`, `Value`, `ExpiresAtTicks`, `Timestamp`, and `SourceKey`. Authored only when writing a custom projection. |
| `LatticeAggregationValue` | Codec for materialised aggregate bytes: `DecodeDouble` / `EncodeDouble` (`Sum` / `Min` / `Max`) and `DecodeInt64` / `EncodeInt64` (`Count` / `SetUnion`). |
| `ViewDigest` | Order-independent content fingerprint over the materialised `(key, value)` pairs: a `Hash` and an `EntryCount`, compared with `ContentEquals`. |
| `AggregationContribution` / `AggregationContributionKind` | The SPI value an aggregation projection's `Project(...)` yields: a `Contribute` (built with `OfNumeric`, `Membership`, `SetMember`, or `Fold`) naming how a source key participates in a group, a `Retract` of a source key's prior contribution, or a `RangeReconcile`. Members: `Kind`, `GroupKey`, `SourceKey`, `Numeric`, `Member`, `Value`, `Timestamp`, and `EndKey`. Authored only when writing a custom aggregation projection. |
| `LatticeRuntimeViewProjectionDescriptor` / `LatticeRuntimeViewProjectionContext` | A runtime view's host-registered provider key plus an opaque payload capped at `MaxPayloadBytes` (64 KiB), and the `ViewName` / `SourceTreeId` / `Payload` context a provider registered through `AddRuntimeProjectionProvider` receives when it reconstructs the view after a restart. |
| `LatticeHistoryView` / `HistoryLatticeViewProjection` / `HistoryRow` / `HistoryRowKind` | The durable per-key history view: `LatticeHistoryView.Definition(viewName, services)` builds the definition to pass to `CreateAsync` (a history view must be runtime-created so it can be deleted again); its projection appends one `HistoryRow` (`Timestamp`, `Kind` - `Set`, `Delete`, `CrdtDelta`, or `RangeTombstone` - `SourceKey`, `OriginClusterId`, `Value`, `Delta`, `ValueHash`, `ValueLength`, `Mode`, `RetentionShape`, `EndKey`) per source mutation at `{sourceKey}/{encodedHlc}`. See [History views](../history-views.md). |
| `ViewWriteCoalescer` / `ViewKeyCollisionDetector` | The maintainer's batch helpers: `Coalesce` keeps the highest-timestamp write per view key, and `Detect` returns the view keys a batch re-keys from two or more distinct source keys. |
| `LatticeViewOptions` | Per-view options resolved via `IOptionsMonitor<LatticeViewOptions>.Get(viewName)`. See [configuration](../configuration/materialised-view-options.md). |
| `LatticeViewReplicationMode` | `DeriveLocally` (default, single-cluster / full-replication) or `ShipView` (replicate the view tree to thin consumer clusters; requires the replication package). |

## Notes

- **No replication required.** Materialised views are part of the core
  `Orleans.Lattice` package. A `DeriveLocally` view is fully local: it tails the
  source WAL through the core commit-log reader registered by `AddLattice`, and a
  single-cluster host never references `Orleans.Lattice.Replication` at all. Only
  the cross-cluster `ShipView` mode pulls in the replication package (its
  `AddLatticeReplication` ships the view tree to consumer clusters and registers
  the startup mode validator).
- **Reminders.** The maintainer registers a keepalive reminder, so a reminder
  provider must be configured on the silo.
- **Read a view by name.** Reading needs neither the source tree nor the
  projection: call `ILatticeViewFactory.GetAsync(viewName)` to open an existing
  view (returns `null` when the view is not registered). The returned
  `ILatticeView` re-resolves the maintainer's active generation on every read, so
  a rebuild that swaps the live view tree underneath you is handled for you.
  (Reads and writes issued against a raw `ILattice` grain bound to a fixed
  `view-{name}` id are rejected - see the next note.)
- **Views are read-only, and the backing tree is private.** The `view-{name}` tree
  is derived state owned by the maintainer; the public `ILattice` surface rejects
  **both** direct writes **and** direct content reads to any `view-*` tree with
  `InvalidOperationException` (a rebuild can swap the active generation underneath a
  raw bind, so a direct read could observe a stale or empty generation). Read
  through the `ILatticeView` handle instead, and write to the source tree to change
  a view's contents. `view-` is therefore a reserved tree-name prefix for
  directly-writable trees. The read-only state API inspection surface
  (`GetTreeStructureAsync`, `ScanEntriesAsync`, `GetEntryAsync`,
  `GetEntryHistoryAsync`) does expose a view's active generation read-only - so the
  Explorer can inspect a view's data, topology, and history - while system-tree
  (`_lattice_*`) ids stay hidden and view writes stay rejected.
- **Source deletion is guarded.** A source tree that still has one or more
  materialised views cannot be deleted: `DeleteTreeAsync` throws
  `InvalidOperationException` naming the dependent view(s). Tear the view(s) down
  first via `ILatticeViewFactory.DeleteAsync`, then delete the source.
- **No view-on-view.** A view's source must be a directly-writable tree, not
  another view. `Create` and the startup `AddView` / `AddAggregationView` /
  `AddFoldedView` builders reject a `view-*` source with `InvalidOperationException`.
- **Atomic visibility.** A source atomic write (single-tree or cross-tree) is
  surfaced atomically in the derived views; see
  [Materialised views](../materialised-views.md#atomic-write-visibility).
- **Resilient view scans.** `ILatticeView.KeysAsync` / `EntriesAsync` are raw
  streams with no reconnect handling - a rebuild's shadow-swap, grain
  deactivation, or idle-expiry can abort the remote enumerator mid-walk with
  `Orleans.Runtime.EnumerationAbortedException`. For long-running scans use the
  `LatticeViewExtensions.ScanKeysAsync` / `ScanEntriesAsync` wrappers (and the
  typed `TypedLatticeViewExtensions.ScanEntriesAsync<T>`), which transparently
  reconnect - resuming from the successor of the last-yielded key so the stream
  has no gaps or duplicates and preserves ordering, the range bounds, the view's
  reserved floor, and cancellation. They mirror the `ILattice`
  `ScanKeysAsync` / `ScanEntriesAsync` wrappers and share their bounded
  reconnect budget (`maxAttempts`, default `8`).
- **Runtime-view durability.** `CreateAsync` returns after the runtime registration
  is durable. Filter-only predicates are encoded automatically; other stateful or
  delegate-backed projections require a host-registered provider descriptor, and
  activation fails closed unless it reconstructs the exact persisted shape and
  version. `DeleteAsync` rejects a startup-declared view (the declaration would
  re-create it); see
  [Materialised views](../materialised-views.md#deleting-a-view).

Previous: [Tag indexes](tag-indexes.md). Contents: [Lattice Public API Reference](../api.md).
