Materialised views
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 materialised-views.md, and llms.txt lists every page.Part of Lattice Public API Reference.
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 for the full guide and
configuration 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.
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. |
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. |
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.Latticepackage. ADeriveLocallyview is fully local: it tails the source WAL through the core commit-log reader registered byAddLattice, and a single-cluster host never referencesOrleans.Lattice.Replicationat all. Only the cross-clusterShipViewmode pulls in the replication package (itsAddLatticeReplicationships 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 (returnsnullwhen the view is not registered). The returnedILatticeViewre-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 rawILatticegrain bound to a fixedview-{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 publicILatticesurface rejects both direct writes and direct content reads to anyview-*tree withInvalidOperationException(a rebuild can swap the active generation underneath a raw bind, so a direct read could observe a stale or empty generation). Read through theILatticeViewhandle 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:
DeleteTreeAsyncthrowsInvalidOperationExceptionnaming the dependent view(s). Tear the view(s) down first viaILatticeViewFactory.DeleteAsync, then delete the source. - No view-on-view. A view's source must be a directly-writable tree, not
another view.
Createand the startupAddView/AddAggregationView/AddFoldedViewbuilders reject aview-*source withInvalidOperationException. - Atomic visibility. A source atomic write (single-tree or cross-tree) is surfaced atomically in the derived views; see Materialised views.
- Resilient view scans.
ILatticeView.KeysAsync/EntriesAsyncare raw streams with no reconnect handling - a rebuild's shadow-swap, grain deactivation, or idle-expiry can abort the remote enumerator mid-walk withOrleans.Runtime.EnumerationAbortedException. For long-running scans use theLatticeViewExtensions.ScanKeysAsync/ScanEntriesAsyncwrappers (and the typedTypedLatticeViewExtensions.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 theILatticeScanKeysAsync/ScanEntriesAsyncwrappers and share their bounded reconnect budget (maxAttempts, default8). - Runtime-view durability.
CreateAsyncreturns 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.DeleteAsyncrejects a startup-declared view (the declaration would re-create it); see Materialised views.