Table of Contents

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.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.
  • 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.