---
title: "Extension seams and supporting types - Lattice Public API Reference"
url: "https://nsta1.github.io/Orleans.Lattice/docs/lattice/api/extension-seams-and-supporting-types.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/docs/lattice/api.md?plain=1#L2275-L2495"
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"
---
# Extension seams and supporting types

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

The sections above document the call surface an application drives. The
tables below cover the rest of the public surface of the core package:
the registration helpers, the host-replaceable seams, and the value types
those seams exchange, each with a pointer to the page that documents it in
depth. `AddLattice` registers the core default of each replaceable seam
below - the access gate, write interceptor, value decoder, envelope codec,
merge observer, membership and tenancy resolvers, tree placement resolver,
tree ownership guard, merge-mode and origin-cluster resolvers, replication
context, WAL record
encoder, WAL provider catalog, cursor registry, and baseline WAL provider -
with `TryAdd`, so an implementation registered before `AddLattice` is
kept; an add-on package that owns a seam swaps its own implementation in.

## Registration helpers

| Helper | Signature | Purpose |
|--------|-----------|---------|
| `AddLattice` | `ISiloBuilder AddLattice(this ISiloBuilder builder, Action<ISiloBuilder, string> configureStorage)` | Registers Lattice and every core seam default; the callback registers the grain-storage provider under the supplied provider name, and that provider must enforce ETags on write. See [Setup](setup.md) and [The grain storage provider must enforce ETags](../configuration.md#the-grain-storage-provider-must-enforce-etags). |
| `ConfigureLattice` | `ISiloBuilder ConfigureLattice(this ISiloBuilder builder, Action<LatticeOptions> configure)` and `ConfigureLattice(this ISiloBuilder builder, string treeName, Action<LatticeOptions> configure)` | Options for every tree, or overrides for one tree. See [Configuration](../configuration.md). |
| `ConfigureLatticeGrainStorageFencing` | `ISiloBuilder ConfigureLatticeGrainStorageFencing(this ISiloBuilder builder, Action<LatticeGrainStorageFencingOptions> configure)` | Configures the start-up probe that checks the grain storage provider enforces ETags: `Mode` (`LatticeGrainStorageFencingMode.Warn` by default, `Reject`, or `Disabled`) and `ProbeTimeout` (30 seconds). See [The grain storage provider must enforce ETags](../configuration.md#the-grain-storage-provider-must-enforce-etags). |
| `ConfigureLatticeTagIndexReconciliation` | `(this ISiloBuilder builder, Action<LatticeTagIndexReconciliationOptions> configure)` and `(this ISiloBuilder builder, string indexName, Action<LatticeTagIndexReconciliationOptions> configure)` | Reconciliation options for every tag index, or overrides for one index. See [Background reconciliation](tag-indexes.md#background-reconciliation). |
| `AddWalStorage` | `ISiloBuilder AddWalStorage(this ISiloBuilder builder, Func<IServiceProvider, IWalStorageProvider>? factory = null)` | Registers the baseline `IWalStorageProvider`. The no-factory form installs `InMemoryWalStorageProvider` only when no provider is registered yet; a factory replaces whatever is registered, so the host's choice is order-independent with respect to `AddLattice`. See [WAL Storage Providers](../wal-storage-providers.md#registering-a-provider). |
| `AddLatticeWalStorageProvider` | `ISiloBuilder AddLatticeWalStorageProvider(this ISiloBuilder builder, string key, Func<IServiceProvider, IWalStorageProvider> factory)` | Registers a named provider in the silo's `IWalStorageProviderCatalog` so WAL partitions can be pinned to it. The reserved `default` key is rejected, every silo must register the same key set, and re-registering a key is last-call-wins. See [Multi-account fan-out](../wal-storage-providers.md#multi-account-fan-out-named-providers-and-pinned-placement). |
| `AddWalCursorRegistry` | `ISiloBuilder AddWalCursorRegistry(this ISiloBuilder builder, Func<IServiceProvider, IWalCursorRegistry>? factory = null)` | See [WAL consumer cursors](wal-consumer-cursors-iwalcursorregistry.md). |
| `AddLatticeWalGc` | `ISiloBuilder AddLatticeWalGc(this ISiloBuilder builder, Func<IServiceProvider, ILatticeWalGc>? factory = null)` | Registers the WAL garbage collector (default `LatticeWalGc`) and the per-silo scheduler that runs it every `LatticeOptions.WalGcInterval`. Idempotent. |
| `AddLatticeRetryPolicy` | `ISiloBuilder AddLatticeRetryPolicy(this ISiloBuilder builder, Action<BoundedExponentialRetryPolicyOptions>? configure = null)` | See [Idempotency keys and retry policy](../api.md#idempotency-keys-and-retry-policy). |
| `AddOrMapShape<TKey, TValue>` | `ISiloBuilder AddOrMapShape<TKey, TValue>(this ISiloBuilder builder, string treeName) where TKey : notnull where TValue : ICrdt<TValue>, new()` | Registers the `(TKey, TValue)` shape an OR-Map tree's accessor and receiver-side applier resolve. Registering a different pair for the same tree is a configuration error. |
| `AddLatticeGrainCallObservation` | `ISiloBuilder AddLatticeGrainCallObservation(this ISiloBuilder builder)` | Opt-in silo-wide filter that records outstanding depth and duration for every outgoing grain call, tagged by target grain type. Idempotent. See [Metrics](../metrics.md). |
| `AddLatticeViews` / `ConfigureLatticeView` | `LatticeViewsServiceCollectionExtensions` | See [Materialised views](materialised-views.md). |
| `AddLatticeAtomicAction` | `ISiloBuilder AddLatticeAtomicAction(this ISiloBuilder builder, Action<AtomicActionRegistrationBuilder>? configure = null)` (`LatticeAtomicActionServiceCollectionExtensions`) | Enables the generic atomic-action coordinator and allow-lists its custom handlers. See [Atomic actions](../atomic-action.md). |

## WAL storage seam (`IWalStorageProvider`)

Implement `IWalStorageProvider` to host the write-ahead log on a custom
backend; [WAL Storage Providers - Contract](../wal-storage-providers.md#contract)
is the full contract. Members with a default implementation are optional
overrides; every member takes a trailing `CancellationToken`.

| Member | Default implementation | Purpose |
|--------|------------------------|---------|
| `Task AppendBatchAsync(string treeId, int shardIndex, IReadOnlyList<WalEntry> entries, CancellationToken)` | None (required) | All-or-nothing append of dense, ascending, caller-assigned offsets. |
| `Task AppendEncodedBatchAsync(string treeId, int shardIndex, ReadOnlyMemory<ArraySegment<byte>> encodedEntries, ReadOnlyMemory<long> offsets, IWalRecordEncoder encoder, CancellationToken)` | Decodes the segments and delegates to `AppendBatchAsync` | Zero-copy append of pre-encoded payloads, with the same atomicity and offset rules. |
| `IAsyncEnumerable<WalEntry> ReadAsync(string treeId, int shardIndex, long fromOffsetExclusive, int maxEntries, CancellationToken)` | None (required) | Entries above `fromOffsetExclusive` in ascending offset order, at most `maxEntries`. |
| `Task<WalShardEncodedPage> ReadEncodedAsync(string treeId, int shardIndex, long fromOffsetExclusive, int maxEntries, IWalRecordEncoder encoder, CancellationToken)` | Drains `ReadAsync` and re-encodes each entry | The same entries as pre-encoded byte segments. |
| `IAsyncEnumerable<WalEntry> ReadFilteredAsync(string treeId, int shardIndex, long fromOffsetExclusive, long toOffsetInclusive, int maxEntries, WalKeyFilter filter, CancellationToken)` | Drains `ReadAsync` over the window and applies the filter to the decoded entries - the same result, at the full decode cost | Filtered replay read for a reader that owns `filter`. It examines at most `maxEntries` entries of the window `(fromOffsetExclusive, toOffsetInclusive]` and yields every entry the filter does not exclude, in full; when the last entry examined is excluded it is yielded routing-only (offset, `Kind`, and `Key` exact, every other field default) so the reader still advances past everything that was dropped. `maxEntries` below 1 throws `ArgumentOutOfRangeException`. Lets a provider drop foreign records before materialising their payloads. See [Filtered replay read](../wal-storage-providers.md#filtered-replay-read-readfilteredasync). |
| `Task<long> GetHighestOffsetAsync(string treeId, int shardIndex, CancellationToken)` | None (required) | The monotonic high-water mark of every offset ever assigned (`-1` for a never-written shard). A trim must never lower it. |
| `Task<long> GetLowestOffsetAsync(string treeId, int shardIndex, CancellationToken)` | None (required) | The lowest still-stored offset, or `-1` when the shard holds no entries. |
| `Task TrimAsync(string treeId, int shardIndex, long throughOffsetInclusive, CancellationToken)` | None (required) | Idempotently removes every entry at or below the offset; called by the WAL GC. |
| `Task EvaluateCompactionAsync(string treeId, int shardIndex, CancellationToken)` | No-op | Lets a log-structured backend reclaim dead bytes on a pass that trimmed nothing; must not change the shard's logical contents. |
| `Task ReconcileAsync(string treeId, int shardIndex, CancellationToken)` | No-op | Activation-time recovery hook for a backend with a multi-phase commit. |
| `Task<long> GetRetainedByteSizeAsync(string treeId, int shardIndex, CancellationToken)` | Returns `-1` (unsupported) | Retained logical payload bytes, for storage-usage accounting and the byte-pressure retention policy. |
| `Task<long> GetPhysicalByteSizeAsync(string treeId, int shardIndex, CancellationToken)` | Returns `-1` (unsupported) | Bytes physically occupied, including framing and trimmed-but-unreclaimed space. |

| Type | Role |
|------|------|
| `WalEntry` | `readonly record struct` (`Offset`, `Mutation`): the provider-boundary entry, a `LatticeMutation` tagged with its dense per-shard offset. |
| `WalShardEncodedPage` | `readonly record struct` (`EncodedEntries`, `Offsets`, `HighestOffsetInclusive`) returned by `ReadEncodedAsync`. Transient and not an Orleans wire type. |
| `WalKeyFilter` | `readonly record struct`: the keys a WAL reader owns - a half-open ordinal key range (`LowKeyInclusive`, `HighKeyExclusive`; `null` means unbounded on that side) intersected, optionally, with the virtual slots a `ShardMap` routes to one physical shard (`VirtualShardCount`, `OwnedSlots`). Built with `WalKeyFilter(lowKeyInclusive, highKeyExclusive)` or `WalKeyFilter(lowKeyInclusive, highKeyExclusive, shardMap, shardIndex)`; a shard that owns every slot carries no shard constraint. `Owns(key)` tests ownership; `Excludes(kind, key)` is `true` only for a `Set`, `Delete`, or `Tombstone` record whose key it does not own (range deletes and saga terminals are never excluded); `IsUnbounded` and `HasShardConstraint` describe the shape, and the `default` filter owns every key. |
| `IWalRecordEncoder` / `OrleansBinaryWalRecordEncoder` | The single-pass codec for WAL payload bytes (`Encode(in WalRecord, IBufferWriter<byte>)` plus three `Decode` overloads) and its default Orleans-binary implementation. See [WAL](../wal.md). |
| `InMemoryWalStorageProvider` | The default provider `AddLattice` registers: process-local, lost on restart, with native `ReadFilteredAsync` and byte accounting. |
| `IWalStorageProviderCatalog` | The silo's named directory of providers: `TryGet(key, out provider)`, `Keys`, and the reserved `DefaultProviderKey` (`default`) naming the baseline provider. |

## WAL garbage collection

| Type | Role |
|------|------|
| `ILatticeWalGc` / `LatticeWalGc` | `Task<LatticeWalGcReport> RunOnceAsync(string treeName, CancellationToken)` and its default implementation, which trims each WAL partition through the largest prefix every retention bound allows. See [WAL - Trim and GC](../wal.md#trim-and-gc). |
| `LatticeWalGcReport` | Diagnostic result of one pass over `TreeName`: the bounds it evaluated (`MinCursor`, `TtlCeilingHlc`, `CausalStable`, `BlockedFloor`), `ShardsScanned` (the WAL partitions whose provider resolved and were visited; a partition whose pinned provider key does not resolve on this silo is skipped and not counted, so `0` means none could be visited rather than an empty WAL), `EntriesTrimmed`, the byte-pressure fields (`ByteCeiling`, `RetainedBytesBefore` / `RetainedBytesAfter`, `LogicalRetainedBytes`, `BytePressureTriggered`, `BytePressureOverThreshold`, `CeilingUnsatisfiable`), and why the cursor floor held (`CursorFloorState`, `BlockingConsumerId`, `BlockingConsumerIds`, `RetainedBacklog`). |
| `WalGcCursorFloorState` | Whether the consumer-cursor trim branch was usable: `Available`, `NoCursorReported`, or `BlockedByUnusablePin`. |
| `WalGcBlockingPinState` | The durable-pin state of a consumer the GC singled out as blocking: `CheckpointedUncovered`, `NeverCheckpointed`, `NoDurableState`, `Unreadable`, `Orphaned`, or `CheckpointedCoverageUnknown`. See [Metrics](../metrics.md). |
| `TreeWalUsageReport` | The cheap WAL-only storage report (`TreeId`, `WalRetainedBytes`, `WalPhysicalBytes`, `Partial`, `SampledAt`) behind `ILatticeAdmin.PollWalUsageAsync`. |

## Access gate and write interception

| Type | Role |
|------|------|
| `ILatticeAccessGate` | `ValueTask<LatticeAccessDecision> AuthorizeAsync(in LatticeAccessRequest request, CancellationToken cancellationToken = default)`, consulted at the data-plane choke point before a read, write, delete, range, CRDT, or lifecycle operation. The core default allows everything. See [Access-gate operation flags](caller-credential-propagation-latticecredentialcontext.md#access-gate-operation-flags). |
| `LatticeAccessRequest` | `TreeId`, `Operation` (a `LatticeOperation`), `Subject` (a `LatticeSubject`), and the optional `Key`, `RangeStart`, and `RangeEnd`. |
| `LatticeAccessDecision` | `Allow()`, `Deny(reason)`, or `Filtered(predicate, reason?)` - an allow with a per-key `KeyFilter` the enforcement point applies to prune keys the caller may not observe. Exposes `Allowed`, `Reason`, and `KeyFilter`. |
| `LatticeSubject` | The resolved caller: `SubjectId`, the transitively expanded `GroupIds`, and optional `Claims`; `Anonymous` and `System`, whose ids are the `AnonymousSubjectId` (`anonymous`) and `SystemSubjectId` (`system`) constants, and `IsAnonymous`. |
| `ILatticeMembershipContext` | Resolves the ambient `LatticeCredential` into a `LatticeSubject` (`ResolveCurrentAsync`, `TryResolveCurrent`). The core default is an anonymous fallback; the [Membership](../../lattice.membership/README.md) package contributes the real implementation. |
| `ILatticeWriteInterceptor` | `ValueTask<LatticeWriteDecision> OnWriteAsync(in LatticeWriteRequest request, CancellationToken cancellationToken = default)`, consulted after the access gate authorizes a write and before the value is appended to the WAL. System-origin writes (replication apply, saga legs, view maintenance) bypass it unless `InterceptsSystemOrigin` returns `true`. The core default accepts everything. |
| `LatticeWriteRequest` | `TreeId`, `Key`, `Value`, `Operation`, and the optional `Ttl`. |
| `LatticeWriteDecision` / `LatticeWriteDecisionKind` | `Accept()`, `AcceptTransformed(newValue)`, `Reject(reason)` (surfaced as `LatticeWriteRejectedException`), or `DeadLetter(reason)`; read back through `Kind`, `TransformedValue`, and `Reason`. |
| `LatticeKeyRange` | `PrefixUpperBound(prefix)`: the exclusive upper bound of a prefix scan under ordinal comparison, or `null` when none exists (an empty prefix or one made only of `U+FFFF`). |

## Value envelopes and merge observation

| Type | Role |
|------|------|
| `ILatticeValueDecoder` | Read-path seam that strips or upcasts a per-value envelope just before stored bytes are returned to a client (`IsActive(treeId)`, `DecodeAsync`). The core default is inactive, so the read path is unchanged. |
| `ILatticeEnvelopeCodec` | The merge-path complement: reports a stored value's schema-version tag (`ReadVersion`) and strips the version envelope from CRDT fold input (`StripForFold`) before it is deserialized, and never upcasts (`IsActive(treeId)`). The core default is inactive. |
| `ILatticeMergeObserver` | Post-merge hook after a per-key CRDT or LWW merge completes (`OnMergedAsync(in LatticeMergeContext ctx, CancellationToken ct)`), returning a `LatticeMergeOutcome`. The core default always accepts. |
| `LatticeMergeContext` | `Key`, `TreeId`, `Mode`, `LocalValue`, `IncomingValue`, `MergedValue`, `LocalVersion`, and `IncomingVersion`. |
| `LatticeMergeOutcome` / `MergeOutcomeKind` | `Accept()`, `AcceptTransformed(mergedValue)` (LWW only), or `AcceptWithEvent(reason)`, read back through `Kind`, `TransformedValue`, and `EventReason`. There is deliberately no reject: the merge has already been applied. See [Schema](../../lattice.schema/README.md). |

## Tenancy seams

The core package defines the tenancy seams so tenant-aware choke points
need no dependency on the add-on. The core defaults are no-ops - the
context resolver resolves the reserved default tenant and the placement
resolver the default placement - so a cluster without
[`Orleans.Lattice.Tenancy`](../../lattice.tenancy/README.md) behaves as
though tenancy did not exist.

| Type | Role |
|------|------|
| `ITenantContextResolver` | Resolves the caller's active `TenantId` (`ResolveCurrentAsync`, `TryResolveCurrent`). |
| `ITenantAdmissionController` | Admits or refuses a tenant-scoped operation or tree creation (`IsActive`, `IsAdmittedAsync`, `IsReadAdmitted`, `IsTreeCreateAdmittedAsync`). |
| `ITenantEnumerationFilter` | Prunes a tree-id enumeration to the trees a tenant may observe (`IsActive`, `Filter`). |
| `ITenantRegionVisibilityResolver` | Resolves a tenant's per-region standing as a `TenantRegionVisibilityMap` (`IsActive`, `ResolveAsync`). |
| `ITreePlacementResolver` | Resolves a tree's `TreePhysicalPlacement` when it is first registered (`TryResolveForRegistration`, `ResolveForRegistrationAsync`). |
| `TenantId` | A DNS-label-like tenant token (`Value`, `MaxLength` 63, `Default` / `DefaultId` `default`, `IsDefault`) with `Parse` and `TryParse`. |
| `TreeOwnership` | A tree's ownership, always re-derived from its id and never stored (`IsTenantOwned`, `IsPlatformOwned`, `Tenant`; built with the static `Platform` or `ForTenant(tenant)`). |
| `TreePhysicalPlacement` | `WalProviderKey` and the optional `PlacementFilter` seeded into a new tree's WAL placement; `Default`. |
| `TenantRegionVisibility` / `TenantRegionVisibilityMap` / `TenantRegionResidencyStatus` | One tenant's standing in a region (`IsAllowed`, `Status`, and the derived `IsResident` and `IsVisible`), the immutable per-region map of it (`Create`, `TryGet`, `Count`, `IsResolved`, `Empty`, `Unresolved`), and the residency lifecycle (`None`, `Provisioning`, `Backfilling`, `Online`, `Draining`, `Offline`, `Removed`). |
| `LatticeTenantTrees` | The reserved `t/` structural namespace (`SegmentPrefix`): `Compose`, `ComposePrefix`, `IsTenantScoped`, `TryGetTenant`, `LocalName`, and `GetOwner`. |
| `LatticeTenantExtensions` | `ResolveEffectiveTreeIdAsync` (an extension on `ITenantContextResolver`) and the two `GetLatticeAsync` overloads (on `IServiceProvider`, and on `IGrainFactory` with an explicit resolver) that resolve an `ILattice` from a tenant-local tree name. With the core no-op resolver - or with the tenancy add-on registered but no active tenant asserted, which resolves the default tenant - the bare name is returned unchanged, so behaviour is byte-for-byte as before. Under an asserted non-default tenant an unqualified name is scoped into that tenant's `t/{tenant}/{name}` namespace, and an already-qualified, well-formed `t/` id or a `_lattice_` system-tree name passes through unchanged (a well-formed foreign `t/{other}/{name}` is left to the tenancy access gate to adjudicate). The call fails closed with `LatticeTenantAccessDeniedException` when the asserted tenant fails validation against the caller's own membership, or when, under an asserted tenant and outside system origin, it names a `sys-` tree or a malformed `t/` id that belongs to no tenant. |
| `LatticeActiveTenantContext` / `LatticeActiveTenantAssertion` | The ambient active-tenant scope (`Current`, `IsActive`, `With`), and the helper that lifts a caller-asserted tenant off a transport header (default `lattice-active-tenant`) onto it (`Stamp`, `Resolve`, `DefaultHeaderName`). |
| `LatticeTenantAdminScope` / `LatticeTenantAdminAuthorizer` | The platform-wide or delegated per-tenant administration scope (`Platform`, `ForTenant`, `IsPlatformWide`, `Tenant`, and the `TreeScope` built from the `PlatformScopeId` / `TenantScopePrefix` constants, with `ToAdminRequest`), and the authorizer that evaluates it as a whole-scope `Admin` request against the access gate, refusing a key-filtered grant (`IsAuthorizedAsync`, `AuthorizeAsync`). |
| `LatticeTenantLabel` | The single source of the derived `tenant` metric dimension (`TagTenant`, `ForTree`, `ForTenant`, `Resolve`, `PlatformMeasurement`, and the pre-built `Platform` / `Default` tags; `PlatformTenant` `_platform_` for platform-owned series and `DefaultTenant` `default`). |

## Tree ownership guard

The tree registry puts every alias assignment to this seam before it writes
anything - a resize's swap, a shadow-cutover restore's cutover or its revert to
a previous alias, a schema remediation's cutover, and an administrative
set-alias, system-origin maintenance included; removing an alias does not
consult it. The core default allows every alias, and the
[installable apps package](../../lattice.apps/README.md) replaces it with a guard
over its tree ownership ledger. See
[Ownership-bounded aliasing](../tree-registry.md#ownership-bounded-aliasing) for
the full contract.

| Type | Role |
|------|------|
| `ITreeOwnershipGuard` | `ValueTask<TreeOwnershipDecision> AuthorizeAliasAsync(string logicalTreeId, string physicalTreeId, string? derivedFrom, CancellationToken cancellationToken = default)`. `derivedFrom` is the logical tree the target was created for, read from the registry rather than supplied by the caller (`null` for an independent tree). An in-process service, not a grain; a guard that throws fails the alias change without writing it. |
| `TreeOwnershipDecision` | In-process `readonly struct`: `Allow()`, or `Deny(reason)` with a non-blank, caller-safe reason, read back through `Allowed` and `Reason`. The `default` value denies. A denial surfaces as `LatticeTreeOwnershipDeniedException`. |

## Replication-facing seams and ambient contexts

| Type | Role |
|------|------|
| `ILatticeMergeModeResolver` | Resolves a tree's declared `LatticeMergeMode` at commit time (`Resolve(treeId)`); `null` means not replicated. See [Replication modes](../../lattice.replication/replication-modes.md). |
| `ILatticeOriginClusterIdResolver` | Supplies the local cluster id stamped on a WAL record when the mutation carries no origin (`Resolve(treeId)`). See [WAL - Origin cluster id stamping](../wal.md#origin-cluster-id-stamping). |
| `LatticeVectorClockContext` / `LatticeHlcOverrideContext` | Ambient scopes (`Current`, `With`) that stamp a `VersionVector` frontier, or a source HLC verbatim, onto the mutations the current logical call authors. |
| `LatticeReplayAdmissionContext` / `LatticeReplayAdmissionClass` | Declares a call chain `Bulk` (`BeginBulkScope()`) rather than the default `Interactive` when it must queue for a WAL replay permit, so the per-silo replay queue can bound and prioritise bulk walks. |
| `CrdtShapeRegistry` | The typed CRDT shapes a tree's writes and receiver-side applies resolve (`Register`, `TryGet`), with a global fallback for every closed-shape mode; only OR-Map needs per-tree registration (`AddOrMapShape`). |

## CRDT support types

| Type | Role |
|------|------|
| Typed delta records (`LwwRegisterDelta`, `OrSetDelta` / `OrSetDeltaDot`, `PnCounterDelta`, `GCounterDelta`, `GSetDelta`, `VersionVectorDelta`, `MvRegisterDelta`, `OrMapDelta<TKey, TValue>` / `OrMapDeltaEntry` / `OrMapDeltaTombstone`, `RgaDelta` / `RgaDeltaNode`, `OrFlagDelta`, `RwFlagDelta`, `RwSetDelta`, `BoundedRegisterDelta`) | The System.Text.Json-encoded delta payloads a CRDT write carries - the `deltaBytes` of `ApplyCrdtDeltaAsync`, and the author delta on a mutation's `Delta` slot. See [Typed CRDT delta records](../../lattice.replication/deltas.md#records). |
| `MvRegisterEntry`, `OrMapEntry<TValue>`, `RgaNode` | The dot-tagged entries inside `MvRegister`, `OrMap<TKey, TValue>`, and `Rga`. See [State Primitives](../state-primitives.md). |
| `ICrdtProvenanceDecoder` / `CrdtProvenanceDecoderRegistry` | Turns a CRDT's stored state or author deltas into ordered element-level `CrdtMemberChange` events (`DecodeDeltas`, `DecodeState`) and its live members into `CrdtMemberValue`s (`DecodeCurrentValue`); the registry resolves the decoder by `LatticeMergeMode` or shape tag (`TryGet`), and `CrdtProvenanceDecoderRegistry.Default` carries one built-in decoder per typed CRDT mode - `OrSetProvenanceDecoder`, `PnCounterProvenanceDecoder`, `VersionVectorProvenanceDecoder`, `MvRegisterProvenanceDecoder`, `OrMapProvenanceDecoder`, `SequenceProvenanceDecoder`, `OrFlagProvenanceDecoder`, `RwFlagProvenanceDecoder`, `GCounterProvenanceDecoder`, `GSetProvenanceDecoder`, `RwSetProvenanceDecoder`, `MaxRegisterProvenanceDecoder`, and `MinRegisterProvenanceDecoder`, each exposing a static `Instance`. See [State API surfaces](../../lattice.api.state/surfaces.md). |
| `CrdtMemberChange` / `CrdtMemberChangeKind` / `CrdtMemberValue` / `CrdtProvenanceDelta` | A decoded `Added` or `Removed` event (`Element`, `Kind`, `ReplicaId`, the causal `Ordinal`, optional `WallClock`), a live member (`Element`, `ReplicaId`, `Ordinal`), and the in-process `(Delta, WallClock)` input pair a decoder consumes. |

## Change history, events, and diagnostics

| Type | Role |
|------|------|
| `EntryRevision` / `EntryHistorySource` | One revision of a key's timeline in an `EntryHistoryPage` (`Hlc`, `Kind`, `SourceKey`, `OriginClusterId`, `ValuePreview`, `ValueLength`, `ValueTruncated`, `ValueHash`, `Delta`, `Mode`, `RetentionShape`, `EndKey`, `VectorClock`), and which substrate served the page (`None`, `View`, `WalWindow`). See [Change history](../change-history.md). |
| `LatticeTreeEventKind` / `LatticeEventConstants` | The kinds a `LatticeTreeEvent` carries (`Set`, `Delete`, `DeleteRange`, `SplitCommitted`, `CompactionTriggered`, `CompactionCompleted`, `TreeDeleted`, `TreeRecovered`, `TreePurged`, `SnapshotCompleted`, `ResizeCompleted`, `ReshardCompleted`, `AtomicWriteCompleted`), and the `StreamNamespace` (`orleans.lattice.events`) events are published on. See [Events](../events.md). |
| `ShardDiagnosticReport` / `RecentSplit` | The per-shard entries of a `TreeDiagnosticReport` (`ShardIndex`, `Depth`, `RootIsLeaf`, `LiveKeys`, `Tombstones`, `TombstoneRatio`, `OpsPerSecond`, `Reads`, `Writes`, `HotnessWindow`, `SplitInProgress`, `BulkOperationPending`, `SampleFailed`), and one recently committed adaptive split (`ShardIndex`, `AtUtc`). The report itself carries `TreeId`, `ShardCount`, `VirtualShardCount`, `TotalLiveKeys`, `TotalTombstones`, `Shards`, `RecentSplits`, `SampledAt`, and `Deep`. See [Diagnostics](../diagnostics.md). |
| `LatticeStorageUsageMetrics` | The process-wide sink behind the storage-usage observable gauges (`Publish`, `PublishWal`, `PublishOverThreshold`, `StalenessHorizon`). `AddLattice` registers it. |
| `LatticeMetrics.WalReplayPermitWaitScope` / `LatticeMetrics.LeafSplitCompletionScope` | Allocation-free disposable scopes returned by `LatticeMetrics.EnterWalReplayPermitWait` and `EnterLeafSplitCompletion`, which feed the in-flight observable gauges. See [Metrics](../metrics.md). |

## Grain storage

| Type | Role |
|------|------|
| `ILatticeBinaryPersistedState` / `LatticeGrainStorageSerializer` | The marker for a persisted state type Lattice writes through the Orleans binary serializer instead of the JSON grain-storage serializer, and the serializer `AddLattice` installs to do it (`WritesBinary(stateType)`, `Fallback`). Every other state type is delegated, unchanged, to the serializer registered before it, because the JSON path cannot write a large opaque payload without first materialising it as one contiguous string. |

## Distributed lock and atomic actions

| Type | Role |
|------|------|
| `ILatticeLockGrain`, `LockAcquireRequest`, `LockLease`, `LockToken`, `LockStatus` | The FIFO-fair distributed lock (`AcquireAsync`, `TryAcquireAsync`, `RenewAsync`, `ReleaseAsync`, `GetStatusAsync`) and its request (`LeaseDuration`, `MaxWait`), lease (`Token`, `ExpiresAt`, `LeaseDuration`), fencing token (`FencingToken`), and status (`IsHeld`, `CurrentFencingToken`, `LeaseExpiresAt`, `QueueDepth`) values. See [Distributed lock](../distributed-lock.md). |
| `IAtomicActionGrain`, `AtomicActionPlanBuilder`, `AtomicActionTreeWriteBuilder`, `AtomicActionPlan`, `AtomicActionStep`, `AtomicActionStepKind`, `AtomicActionEntry`, `AtomicActionOutcome`, `AtomicActionStatus` | The saga coordinator (`ExecuteAsync`, `TryGetOutcomeAsync`), the fluent plan builder (`Step`, `TreeWrite` with `Upsert` / `Delete`, `Build`), the plan and step shapes (`Custom` or `TreeWrite` steps), and the terminal outcome (`Committed`, `Compensated`, or `CompensationFailed`, with `FailedStepIndex` and `FailureMessage`). See [Atomic actions](../atomic-action.md). |
| `IAtomicActionHandler`, `IAtomicActionContext`, `AtomicActionRegistrationBuilder` | A named, versioned forward / compensate pair (`HandlerId`, `VersionTag`, `ForwardAsync`, `CompensateAsync`), the context each effect receives (`OperationId`, `Args`, `GrainFactory`, `CancellationToken`), and the `AddHandler` registration surface `AddLatticeAtomicAction` supplies. |

## Exception reference

Every public exception type the core package defines. Those deriving from a BCL
exception subclass implement [`ILatticeDomainFault`](../api.md#domain-faults---ilatticedomainfault);
`LeafProjectionStaleException` also implements `ILatticeLeafUnavailable`,
the marker for "this leaf cannot be activated right now".

| Exception | Base | Carries | Raised when |
|-----------|------|---------|-------------|
| `LatticeAuthorizationDeniedException` | `UnauthorizedAccessException` | `TreeId`, `Operation`, `SubjectId`, `Reason` | The registered `ILatticeAccessGate` denies a write, delete, CRDT, atomic, range-delete, bulk-load, lifecycle, or whole-tree read call, or the backup / restore authorization seam. `LatticeTenantAdminAuthorizer.AuthorizeAsync` raises it when the gate denies a tenant-administration scope or allows only a key-filtered subset of it, and add-on facades (for example telemetry, tenant administration, and app installation) raise it for their own capability denials. With the auth add-on's internal-origin enforcement registered, a direct external-client call to one of a tree's internal shard or leaf grains is refused with it. Nothing is persisted. A denied point or range read reports absence or an empty result instead (see [Reading an empty range read under a gate](caller-credential-propagation-latticecredentialcontext.md#reading-an-empty-range-read-under-a-gate)). |
| `LatticeReservedTreeNamespaceException` | `InvalidOperationException` | `TreeId` | Any `ILattice` call is addressed to an internal system tree (the `_lattice_` prefix). Outside system origin it is also raised when a data-mutation call (write, delete, CRDT apply, bulk load) names a tree in the `sys-` namespace, a `t/` tenant id the caller's active tenant does not own, or the all-trees authorization sentinel `*`, and when a `SnapshotAsync` destination names a `_lattice_`, `sys-`, or another tenant's `t/` tree. Reads of `sys-` and `t/` trees are not refused. App trees (`a/{app}/{tree}`) are ordinary trees and are not reserved; the app registry's `sys-app-` trees fall under `sys-`. |
| `LatticeTreeNotRegisteredException` | `KeyNotFoundException` | `TreeId` | A tree-registry verb that changes an existing tree's entry - a shard-map change, a split's shard-index allocation, a per-tree configuration override, the projection-digest latch, or a WAL placement change - names a tree with no registry entry (never created, or purged). Nothing is created. `SetHistoryRetentionAsync` and `SetPublishEventsEnabledAsync` raise it for a purged tree; a tree that was never created is registered by them instead. The API facades report it as not found. See [Tree Registry](../tree-registry.md#changing-a-trees-entry-does-not-create-it). |
| `LatticeTenantAccessDeniedException` | `Exception` | - | Resolving a tree name through `LatticeTenantExtensions` fails closed: the caller's asserted active tenant fails validation against the caller's own membership (an anonymous caller can never act as a tenant), or, under an asserted tenant and outside system origin, the name is a `sys-` tree or a malformed `t/` id that belongs to no tenant. The `ILattice` read and write paths also raise it (the snapshot cursor per page) when an active `ITenantAdmissionController` returns `false` for the tenant's read or write; the tenancy add-on's own controller never returns `false`, and signals a breach with `LatticeQuotaExceededException` instead. Never raised by the core defaults: the no-op resolver always resolves the default tenant and the no-op admission controller is inactive. |
| `LatticeTreeOwnershipDeniedException` | `Exception` | `Reason` | The registered [`ITreeOwnershipGuard`](#tree-ownership-guard) refuses an alias assignment; the alias is not written and no alias-change notification fires. Every assignment consults the guard, system-origin maintenance included - a resize's swap, a shadow-cutover restore's cutover or its revert to a previous alias, a schema remediation's cutover, and an administrative set-alias - so each can meet it, except that a resize swaps its alias after `ResizeAsync` has returned, so a refused swap is retried and leaves the resize in progress rather than reaching that caller. The core default guard never refuses. Deleting through an alias to a copy this tree does not own raises `InvalidOperationException`, not this exception (see [`DeleteTreeAsync`](ilattice-2.md#tree-lifecycle)). `Reason` is the guard's caller-safe reason. |
| `LatticeReplicationModeMismatchException` | `InvalidOperationException` | `TreeId`, `DeclaredMode`, `AttemptedMode` | A write would break the single-shape rule of a tree declared for cross-cluster replication. |
| `LatticeCrdtShapeNotRegisteredException` | `InvalidOperationException` | `TreeId` | An OR-Map write targets a tree with no registered `(TKey, TValue)` shape. Also raised, at any CRDT mode and with an empty `TreeId`, when a CRDT write reaches a leaf its shard has not yet attached to a tree - a routing or lifecycle race to retry, not a missing registration. |
| `LatticeIdempotencyKeyMismatchException` | `InvalidOperationException` | `OperationId` | An `operationId` is re-submitted with a different key set (cross-tree: tree set or key set). |
| `LatticeStateWriteFailedException` | `Exception` | `GrainType`, `GrainKey`, `FaultType`, `Conflict` | An atomic-write saga, a cross-tree atomic-write coordinator, or a WAL materialiser pin shard fails to persist its own durable state. The storage provider's exception is summarised in `FaultType` and the message rather than carried as the inner exception, so a client that does not reference the provider can still load it; a fault that is not a conflict is translated only when its exception type comes from such a provider assembly, and a BCL or Lattice exception propagates unchanged. `Conflict` is `true` when the write lost an optimistic-concurrency (ETag) check - typically a storage SDK retry of a write that had already landed: the grain deactivates so the next call reloads its durable state, and retrying the same operation with the same operation id is safe and neither loses nor double-applies a commit or abort decision. Every `ILattice` `SetManyAtomicAsync` / `SetManyAtomicWhereAsync` overload, and the cross-tree `SetManyAtomicAsync` / `LatticeAtomicWriteBuilder.CommitAsync`, re-attaches up to three attempts (after 1 s and 2 s) before surfacing a persistent conflict. |
| `LatticeWriteRejectedException` | `InvalidOperationException` | `TreeId`, `Operation`, `Key`, `Reason` | A registered `ILatticeWriteInterceptor` rejects the value before commit. A dead-letter decision on a single-key write does not raise it (the value is diverted and the call completes normally), but in an atomic batch a dead-letter aborts the whole batch and surfaces here too. |
| `LatticeQuotaExceededException` | `InvalidOperationException` | `TreeId`, `Dimension`, `Current`, `Limit`, `TenantId` | See [Admission back-pressure](admission-back-pressure-latticequotaexceededexception.md). |
| `LatticeSaturatedException` | `InvalidOperationException` | `TreeId`, `SaturationSource` | See [Saturation back-pressure](saturation-back-pressure-latticesaturatedexception.md). |
| `LatticeShuttingDownException` | `InvalidOperationException` | - | See [Shutdown back-pressure](shutdown-back-pressure-latticeshuttingdownexception.md). |
| `LatticeWriteFencedException` | `InvalidOperationException` | `TreeId`, `SagaId` | The tree is write-fenced for a cross-cluster saga such as a restore cutover. Transient. |
| `LatticeWalQuiescingException` | `InvalidOperationException` | - | A WAL partition is quiesced for an administrative placement move. Transient. |
| `LatticeWalProviderMissingException` | `InvalidOperationException` | `TreeId`, `Partition`, `ProviderKey` | A WAL partition's pinned provider key does not resolve on this silo; the partition fails closed rather than re-routing to the baseline provider. The `ILatticeAdmin` WAL move verbs also raise it, before touching any log, when a move target key (or, for `ReclaimMovedWalSourceAsync`, the source key) does not resolve on the serving silo. |
| `LatticeCursorRegistryPinExhaustedException` | `InvalidOperationException` | - | Opening a point-in-time cursor would push a saga decision registry shard the snapshot touches past `LatticeOptions.MaxPinnedSagaDecisions`; the cursor is not opened, though a snapshot spanning several shards can leave its pin on the shards that accepted it (see [Point-in-time cursors](ilattice-1.md#point-in-time-cursors)). |
| `LatticeCursorSnapshotExpiredException` | `InvalidOperationException` | - | A point-in-time cursor's registry pin expired between steps; open a fresh cursor. |
| `LatticeSnapshotReplayBudgetExceededException` | `InvalidOperationException` | - | Opening a snapshot cursor would materialise more than `LatticeOptions.MaxSnapshotReplayEntries` baseline rows on its deepest shard. |
| `LatticeSnapshotExpiredException` | `InvalidOperationException` | - | A snapshot cursor's frozen baseline can no longer be loaded; open a fresh cursor. |
| `LeafProjectionStaleException` | `InvalidOperationException` | - | A leaf cannot rebuild its projection at activation: the WAL has been trimmed past its persisted projection checkpoint and no snapshot covers the gap. Every `ProjectionRebuildPolicy` value surfaces it (no automatic recovery path is integrated), and the cost triggers - a replay gap over `MaxLeafReplayEntries` or a checkpoint older than `LeafProjectionRetention` - never raise it. Resolved by an explicit `RebuildLeafProjectionAsync`, never by a retry. See [Projection Rebuild](../projection-rebuild.md). |
| `ScanPageStalledException` | `TimeoutException` | `TreeId`, `ShardIndex`, `Operation`, `Phase`, `LeavesVisited`, `TimeoutSeconds`, `LeafInFlight`, `ConsecutiveZeroProgressStalls`, `LeafStranded`, `StrandedRecoveryApplications` | One shard range-scan page fill exceeded `LatticeOptions.MaxScanPageStallDuration` with nothing it could bank: before any leaf contributed, or during a snapshot baseline capture or a bounded range delete, which never bank. A fill that had already accumulated rows returns them as a short page instead, and an aggregate walk (counts, diagnostics, storage usage) banks its partial page. The resilient read scans resume it within their stall budget; the resilient range-delete drain propagates it (see [Enumeration](ilattice-1.md#enumeration)). |
| `ShardActivationTimeoutException` | `TimeoutException` | `TreeId`, `ShardIndex`, `TimeoutSeconds` | A shard root's activation-readiness seed exceeded `LatticeOptions.ActivationReadyTimeout`; entry points that wrap the seed retry up to three attempts before surfacing it. |
| `LatticeTransactionOutcomeUnavailableException` | `TimeoutException` | `TreeId`, `Key`, `KeyCount`, `TransactionIds` | A read's result depends on an atomic-write saga whose outcome the transaction registry could not be reached to establish. A point read raises it for its key; a multi-key read (`GetManyAsync`, the counts, key and entry enumeration) raises it, with a `null` `Key`, only when a key it read carried a prepared mutation and only after its own `MaxScanRetries` retry is exhausted. A registry that answers that the outcome is no longer known makes the key read as absent instead, on every read path. Retryable; never a guessed value. See [When the registry cannot be reached](../atomic-writes.md#when-the-registry-cannot-be-reached-latticetransactionoutcomeunavailableexception). |
| `LatticeLockConflictException` | `Exception` | `LockName` | `ILatticeLockGrain.RenewAsync` is presented a token that no longer holds the lock. |
| `CompensationFailedException` | `Exception` | `StepIndex` | An atomic action's compensating effect faulted after its retry budget. See [Atomic actions](../atomic-action.md#when-compensation-itself-fails). |
| `AtomicActionHandlerNotRegisteredException` | `Exception` | `HandlerId` | An atomic-action plan names a custom handler id the silo never registered; handler resolution fails closed. |

Previous: [LatticeOptions](latticeoptions.md). Next: [Serializable types](serializable-types.md). Contents: [Lattice Public API Reference](../api.md).
