---
title: "Lattice Public API Reference"
url: "https://nsta1.github.io/Orleans.Lattice/docs/lattice/api.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/docs/lattice/api.md"
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"
---
# Lattice Public API Reference

Part of the [Orleans.Lattice documentation](architecture.md).

This document is the **contract** for what each public type and method
on `Orleans.Lattice` does. It states behaviour in caller-visible terms
only - signature, return value, exceptions, and observable effect. It
does not describe how the library delivers any of those guarantees.

For the consistency classification (linearizable / strongly consistent /
snapshot / eventually consistent) of every operation, see
[Consistency](consistency.md). For implementation details, follow the
topic cross-references in each section.

> **Compression** - `ILatticeCompressor`, `LatticeCompression`, `ZstdLatticeCompressor`, and `LatticeCompressionServiceCollectionExtensions.AddLatticeCompressor` are part of the public API surface, along with the shared-dictionary compression types `ILatticeCompressionDictionaryProvider` (with its optional companion interfaces `ILatticeCompressionDictionaryCatalog`, `ILatticeActiveCompressionDictionary`, `ILatticeCompressionDictionarySink`, and `ILatticeCompressionDictionarySampler`), `OperatorSuppliedCompressionDictionaryProvider`, `ILatticeDictionaryCompressor`, `ZstdDictionaryLatticeCompressor`, the auto-trained-dictionary types `CompressionDictionaryTrainingOptions` and `AutoTrainingCompressionDictionaryProvider`, and the `AddLatticeCompressionDictionaries` / `AddLatticeCompressionDictionaryProvider` / `AddLatticeZstdDictionaryCompressor` / `AddLatticeAutoTrainingCompressionDictionary` registration helpers. They are documented in [`compression.md`](compression.md), which is the source of truth for registration, the tag-space partitioning, the shared-dictionary opt-in, runtime auto-training, and the worked example for plugging in a custom algorithm.

> **Cluster-internal queues** - `ILatticeQueue<T>`, `LatticeQueueEntry<T>`, and the `IGrainFactory.GetLatticeQueue<T>` resolver (`LatticeQueueExtensions`) are part of the public API surface. They are documented in [`queues.md`](queues.md), which is the source of truth for resolving a named queue, the bounded-FIFO eviction knob (`LatticeOptions.QueueCapacity`), and the single-coordinator throughput model.

## Contents

- [Setup](api/setup.md): Install the NuGet package.
- [`ILattice`](api/ilattice.md): Obtain an `ILattice` grain from the grain factory using the tree's logical name as the string key.
  - [Cancellation to Stateful cursors](api/ilattice-1.md)
  - [Maintenance operations](api/ilattice-2.md)
- [`ILatticeAdmin`](api/ilatticeadmin.md): `ILatticeAdmin` is the cluster-wide administrative surface.
- [Mutation observers](api/mutation-observers.md): `IMutationObserver` is a grain-side extensibility hook invoked synchronously after a mutation is durably committed, before the grain method returns to the caller.
- [Tree alias observers](#tree-alias-observers): `ITreeAliasObserver` is a control-plane extensibility hook invoked once per logical-tree **physical-identity alias change**, fired from inside the tree registry's single alias-mutation choke point (where an alias is set or removed) after...
- [Caller-credential propagation (`LatticeCredentialContext`)](api/caller-credential-propagation-latticecredentialcontext.md): `LatticeCredentialContext` is a transport-only ambient seam that carries an opaque caller credential from the client edge down to the silo on the Orleans `RequestContext`, following the same marker idiom as `LatticeOriginContext`...
- [WAL saturation back-pressure](api/wal-saturation-back-pressure.md): Lattice publishes a per-tree, three-state saturation signal so callers driving offered load into `ILattice` can throttle their own input *before* the saturation regime's failure tail surfaces to them as a `TimeoutException` from `SetAsync`...
- [WAL consumer cursors - `IWalCursorRegistry`](api/wal-consumer-cursors-iwalcursorregistry.md): Every consumer of a tree's write-ahead log.
- [Domain faults - `ILatticeDomainFault`](#domain-faults---ilatticedomainfault): Marker interface implemented by every public Lattice exception that derives from a BCL exception subclass (`InvalidOperationException`, `TimeoutException`, `UnauthorizedAccessException`) rather than directly from `Exception`.
- [Shutdown back-pressure - `LatticeShuttingDownException`](api/shutdown-back-pressure-latticeshuttingdownexception.md): Public typed exception thrown by any `ILattice` operator (and by the internal saga coordinator on its caller-facing throw path) when the operation cannot complete because the owning silo's write-ahead-log writer is draining as part of host...
- [Saturation back-pressure - `LatticeSaturatedException`](api/saturation-back-pressure-latticesaturatedexception.md): Public typed exception thrown when an operation is refused because the tree's storage layer is back-pressured - most commonly by the WAL writer admission gate and the atomic-write saga coordinator, when the per-tree `IWalSaturationSignal`...
- [Admission back-pressure - `LatticeQuotaExceededException`](api/admission-back-pressure-latticequotaexceededexception.md): Public typed exception thrown when a locally-authored write call is refused because the target tree has reached a configured per-tree admission-control cap - either `LatticeOptions.MaxLiveKeys` (the `Dimension` property is `keys`)...
- [Leaf-projection digest](#leaf-projection-digest): `LeafProjectionDigest { byte[] Hash; long EntryCount; long CheckpointOffset; int Version; }` (alias `ol.lpd`).
- [Operator tooling: projection rebuild and materialiser lag](#operator-tooling-projection-rebuild-and-materialiser-lag): Two `ILattice` methods expose operator-driven projection recovery and steady-state materialiser-lag observation.
- [Operator tooling: orphaned-leaf repair](api/operator-tooling-orphaned-leaf-repair.md): `VerifiedKeyCount` always means the verified **prefix length** before the first missing key or routing contradiction.
- [Metrics](#metrics): Orleans.Lattice publishes `System.Diagnostics.Metrics` instruments on the static meter `orleans.lattice`, exposed via `Orleans.Lattice.LatticeMetrics` (meter name `LatticeMetrics.MeterName`).
- [`SnapshotMode`](#snapshotmode): Controls source-tree availability during a snapshot operation.
- [`LatticeExtensions`](#latticeextensions): The resilient scans (`ScanKeysAsync`, `ScanEntriesAsync`) and the resilient `DeleteRangeAsync` drain are documented under Enumeration, the `Open*CursorScopeAsync` family under Scoped cursor extensions, and the `DefaultScanReconnectAttempts`...
- [`TypedLatticeExtensions`](#typedlatticeextensions): Extension methods that serialize and deserialize via an `ILatticeSerializer<T>`, eliminating per-caller `byte[]` boilerplate.
- [Predicate operations](#predicate-operations): Typed overloads that take an `Expression<Func<T, bool>>` and evaluate it **server-side** against a JSON document view of each value, so only matching keys (or values) cross the wire.
- [Cross-tree atomic writes](api/cross-tree-atomic-writes.md): `IGrainFactory` / cluster-client extension methods (`LatticeCrossTreeAtomicWriteExtensions`) that commit a batch spanning two or more distinct `ILattice` trees all-or-nothing, with the same atomic-visibility guarantee `SetManyAtomicAsync`...
- [CRDT value-surface accessors](api/crdt-value-surface-accessors.md): The CRDT value-surface methods on `ILattice` return lightweight, allocation-free accessors that read and write a single key through the primitive's natural mutation API instead of forcing callers to hand-roll byte arrays or CRDT deltas.
- [`ILatticeSerializer<T>`](#ilatticeserializert): Implement this interface to provide a custom serialization strategy.
- [`LatticeOptions`](api/latticeoptions.md): See [Configuration](configuration.md) for detailed guidance, mutability constraints, and per-tree overrides via the [tree registry](tree-registry.md).
- [Idempotency keys and retry policy](#idempotency-keys-and-retry-policy): Opt-in surface for retrying transient storage faults under a caller-supplied identity.
- [Extension seams and supporting types](api/extension-seams-and-supporting-types.md): The sections above document the call surface an application drives.
- [Serializable types](api/serializable-types.md): All serializable types - and every grain interface, including the public `ILattice` - carry stable `[Alias]` attributes (prefixed `ol.`) to ensure wire-format and grain-manifest compatibility across versions.
- [Public but not usable](#public-but-not-usable): A handful of types are necessarily `public` so that Orleans can serialize them on the `ILattice` wire surface, but they are **not intended as direct caller dependencies**. Their shape, members, and return values can change in any release...
- [Tag indexes](api/tag-indexes.md): A **tag index** associates string tags with the keys of a tree and lets you query keys back by tag.
- [Materialised views](api/materialised-views.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...

## Tree alias observers

`ITreeAliasObserver` is a control-plane extensibility hook invoked once
per logical-tree **physical-identity alias change**, fired from inside
the tree registry's single alias-mutation choke point (where an alias is
set or removed) after the new alias is durably persisted and **only**
when the effective physical id actually changed. It is the seam a consumer
that binds to a logical tree's physical WAL uses to rebind when a
shadow-cutover restore (or its revert), a resize (or its undo), a schema
remediation, or an administrative alias change swaps that binding
underneath it (a reshard changes the shard map, not the alias, so it
raises no notification) - most notably the cross-cluster replication shipper, which uses it to
rebind reactively instead of re-reading the registry on every pump tick
(see [Source-identity rebind](../lattice.replication/replication-drivers.md#source-identity-rebind)).

The hook receives a `TreeAliasChange` carrying the logical `TreeId` and
both the old and new **effective** physical ids (an unaliased tree resolves
to its own logical id, so a removed alias reports the logical id as the new
physical), so a consumer can rebind directly without re-reading the registry.

```csharp verify
public sealed class MyRebindObserver : ITreeAliasObserver
{
    public Task OnTreeAliasChangedAsync(TreeAliasChange change, CancellationToken ct)
    {
        // change.TreeId is the logical tree; change.OldPhysicalTreeId and
        // change.NewPhysicalTreeId are the effective physical ids before and
        // after the swap. Dispatch a rebind and return - do not block the
        // registry grain with slow synchronous I/O.
        return Task.CompletedTask;
    }
}
```

```csharp verify
siloBuilder.ConfigureServices(services =>
    services.AddSingleton<ITreeAliasObserver, MyRebindObserver>());
```

Observers are resolved as `IEnumerable<ITreeAliasObserver>`, so multiple can
coexist; the hook is zero-cost when none is registered. `OnTreeAliasChangedAsync`
runs on the registry grain's single-threaded scheduler and is awaited inline
before the alias-mutation grain method returns, so an implementation should
dispatch its work and return rather than issue slow synchronous I/O. Exceptions
thrown by one observer are logged as a warning and suppressed (the alias is
already persisted and cannot be rolled back), and do not short-circuit the
others; a consumer that misses a notification is expected to heal out-of-band
(the replication shipper's coarse backstop re-resolve covers exactly this case).
The registry raises the hook only on a genuine effective-physical-id change, so
a no-op re-set of the current alias never produces a spurious rebind.

## Domain faults - `ILatticeDomainFault`

Marker interface implemented by every public Lattice exception that derives from a BCL exception subclass (`InvalidOperationException`, `TimeoutException`, `UnauthorizedAccessException`) rather than directly from `Exception`. It declares no members and adds no state.

Those base types were chosen for backwards compatibility, but the inheritance is a hazard rather than a convenience: a broad `catch (InvalidOperationException)` written for some unrelated condition silently absorbs a Lattice domain fault and applies remediation calibrated for a different problem. `ILatticeDomainFault` gives such a handler a way to decline it:

```csharp verify
var entries = new List<KeyValuePair<string, byte[]>>
{
    new("k1", new byte[] { 0x01 }),
};

try
{
    await lattice.SetManyAsync(entries);
}
catch (InvalidOperationException ex) when (ex is not ILatticeDomainFault)
{
    // Genuine API misuse. A Lattice domain fault (saturation, quota,
    // shutdown, fencing, ...) is declined here and falls through to a
    // handler that understands it, instead of being absorbed.
    Console.WriteLine(ex.Message);
}
```

The marker does **not** mean "never handle this". Catching a domain fault *by its own type* remains the correct way to handle it and is unaffected - `catch (LatticeSaturatedException)` behaves exactly as documented below, and an internal retry loop that catches `ShardActivationTimeoutException` by name still absorbs and retries it. The marker constrains only a broad catch of the **base** type.

A reflection gate in the core test suite enumerates the assembly at run time and fails if a public exception with a foreign base does not implement the marker, so the set cannot drift as exceptions are added.

## Leaf-projection digest

`LeafProjectionDigest { byte[] Hash; long EntryCount; long CheckpointOffset; int Version; }` (alias `ol.lpd`). `LeafProjectionDigest.CurrentVersion` (`0`) is the contribution-function shape a current silo stamps; digests with different `Version` values must not be byte-compared.

```csharp verify
LeafProjectionDigest digest = await tree.GetLeafProjectionDigestAsync(
    shardIndex: 0,
    cancellationToken);
```

Throws `ArgumentOutOfRangeException` when `shardIndex` is not a
physical shard of the per-tree map, `InvalidOperationException` for
activations on reserved system-tree prefixes or when
`LatticeOptions.MaintainProjectionDigest = false` (the per-tree
opt-out makes the digest API unavailable),
`LatticeAuthorizationDeniedException` when the caller is not authorized
to read the whole shard (a key-filtered grant is refused rather than
narrowed, because a digest cannot be narrowed per key), and
`OperationCanceledException` if the token was already cancelled. See
[Projection Rebuild](projection-rebuild.md) for the determinism
contract, the cost model, and the related `ProjectionRebuildPolicy`
setting (every value currently fails closed on genuine loss).

`GetLeafProjectionDigestForRangeAsync(int shardIndex, string? startKeyInclusive, string? endKeyExclusive, CancellationToken cancellationToken = default)`
folds the same digest over only the half-open key range of one physical
shard; a `null` bound is unbounded on that side, so `(null, null)` is
byte-identical to the whole-shard digest. It is the primitive behind the
cross-cluster anti-entropy Merkle-walk localisation, which narrows a
divergent shard to a leaf or key range. It throws the same exceptions as
`GetLeafProjectionDigestAsync`.

## Operator tooling: projection rebuild and materialiser lag

Two `ILattice` methods expose operator-driven projection recovery
and steady-state materialiser-lag observation. Both surfaces are
narrow: they do not freeze the tree, take no consistency lock, and
are safe to call against a live shard under load.

| Method | Description |
|--------|-------------|
| `RebuildLeafProjectionAsync(int shardIndex, CancellationToken)` | Clears the projection state of every leaf in the specified physical shard - the per-activation in-memory entry cache (per-key data is not persisted on the leaf row; on activation it is rehydrated from the leaf's snapshot where one exists, then from the WAL after it), the persisted projection-digest fold, the persisted projection checkpoint (reset to "nothing scanned"), and the in-memory pending-saga and recently-terminal dedup buffers - then deactivates each leaf so its next activation re-materialises the projection through the standard activation-time path (which, under every `ProjectionRebuildPolicy` value, first rehydrates from the leaf's captured snapshot where one exists and then replays the WAL after it). Topology-bearing state (tree id, shard index, key-range bounds, sibling pointers, parent pointer, split state) is preserved. Requires whole-tree `Admin` authorization (`LatticeAuthorizationDeniedException` otherwise). Throws `ArgumentOutOfRangeException` when `shardIndex` is not a physical shard of the per-tree map, `LatticeReservedTreeNamespaceException` (an `InvalidOperationException`) for a reserved `_lattice_` system tree, and `OperationCanceledException` if the token is cancelled. |
| `GetMaterialiserLagAsync(CancellationToken)` | Returns the largest per-shard lag, in WAL entries: for each physical shard, how far its WAL partition heads run ahead of the lowest leaf-projection checkpoint across that shard's leaves, summed over its partitions (each partition's gap clamped at zero). The figure is an estimate: each WAL head is the next offset to be assigned while each checkpoint is the last offset a leaf applied, and every partition's head is measured against the leaf's partition-0 checkpoint, so a caught-up shard with a non-empty WAL still reports a small positive value rather than `0`; a steady value means caught up, and a steadily growing value indicates the materialiser is falling behind WAL ingestion. Requires whole-tree `Read` authorization (`LatticeAuthorizationDeniedException` otherwise). Throws `InvalidOperationException` for system-tree activations and `OperationCanceledException` if the token is cancelled. |

```csharp verify
// Operator-driven recovery: rebuild one shard's projection from
// the WAL after detecting drift via the digest surface.
await tree.RebuildLeafProjectionAsync(shardIndex: 0, cancellationToken);

// Steady-state lag observation: poll periodically and alert
// when lag crosses an SLO threshold.
long lag = await tree.GetMaterialiserLagAsync(cancellationToken);
// Emit (treeId, lag) to telemetry.
```

See [Projection Rebuild](projection-rebuild.md#operator-tooling-rebuild-and-lag)
for the rebuild semantics, the topology-preservation guarantees, and
the recommended monitoring shape for lag.

## Metrics

Orleans.Lattice publishes `System.Diagnostics.Metrics` instruments on
the static meter `orleans.lattice`, exposed via
`Orleans.Lattice.LatticeMetrics` (meter name `LatticeMetrics.MeterName`).
The catalog groups instruments by the subsystem that records them - the
shard and leaf data paths, snapshot cursors, the WAL append pipeline and
garbage collector, admission control, the read cache, sagas and lifecycle
coordinators, events, materialised views, and more. Subscribe with `.AddMeter("orleans.lattice")` on your
OpenTelemetry `MeterProviderBuilder`. See [Metrics](metrics.md) for
the full catalog and tag conventions.

## `SnapshotMode`

Controls source-tree availability during a snapshot operation.

| Value | Description |
|-------|-------------|
| `Offline` | Every source shard the copy reads is locked before any is copied and stays locked until it has been copied; reads and writes to it throw `InvalidOperationException` meanwhile. The destination is a point-in-time image of the shards copied. |
| `Online` | Source tree remains available throughout. Writes it accepts while the copy runs are shadow-forwarded to the destination, so the destination is a mirror maintained during the copy rather than a point-in-time image; typed CRDT delta applies and bulk appends are not forwarded. |

Both modes copy every physical shard the source's shard map routes to, a shard an adaptive split added included; see [`SnapshotAsync`](api/ilattice-2.md#snapshots).

## `LatticeExtensions`

The resilient scans (`ScanKeysAsync`, `ScanEntriesAsync`) and the resilient
`DeleteRangeAsync` drain are documented under [Enumeration](api/ilattice-1.md#enumeration), the
`Open*CursorScopeAsync` family under
[Scoped cursor extensions](api/ilattice-1.md#scoped-cursor-extensions), and the
`DefaultScanReconnectAttempts` / `DefaultScanStallResumeAttempts` /
`DefaultScanStallResumeCeiling` budget constants with the scans that use them.
The remaining members are:

| Method | Description |
|--------|-------------|
| `BulkLoadAsync(this ILattice, IAsyncEnumerable<KeyValuePair<string, byte[]>> sortedEntries, IGrainFactory grainFactory, int chunkSize = 10_000, CancellationToken cancellationToken = default)` | Streaming bulk load for large datasets. Input **must** be pre-sorted ascending by key. See [Bulk Loading](bulk-loading.md). |
| `SubscribeToEventsAsync(this ILattice, IClusterClient, Func<LatticeTreeEvent, Task>, string providerName = "Default", CancellationToken)` | Subscribes to the per-tree `LatticeTreeEvent` stream on the cluster client. Returns a `StreamSubscriptionHandle<LatticeTreeEvent>`; call `UnsubscribeAsync()` to stop. Throws `InvalidOperationException` when `providerName` is not registered on the client. See [Events](events.md). |

## `TypedLatticeExtensions`

Extension methods that serialize and deserialize via an
`ILatticeSerializer<T>`, eliminating per-caller `byte[]`
boilerplate. Each method has two overloads: one accepting an
explicit serializer and one that defaults to
`JsonLatticeSerializer<T>` (System.Text.Json with UTF-8 encoding).

```csharp verify
// Default (System.Text.Json):
await tree.SetAsync("user:1", new User("Alice", 30));
var user = await tree.GetAsync<User>("user:1");

// Custom serializer:
var serializer = new JsonLatticeSerializer<User>(new JsonSerializerOptions { WriteIndented = false });
await tree.SetAsync("user:1", new User("Alice", 30), serializer);

// Compare-and-swap (CAS):
var versioned = await tree.GetWithVersionAsync<User>("user:1");
var updated = versioned.Value! with { Age = 31 };
bool success = await tree.SetIfVersionAsync("user:1", updated, versioned.Version);
```

The serializer-less overload of every method uses
`JsonLatticeSerializer<T>.Default`. The members are `GetAsync<T>`,
`GetWithVersionAsync<T>`, `GetOrSetAsync<T>`, `SetAsync<T>` (plus a
`TimeSpan ttl` overload), `SetIfVersionAsync<T>`, `GetManyAsync<T>`,
`SetManyAsync<T>`, `SetManyAtomicAsync<T>` (plus an `operationId` overload),
`BulkLoadAsync<T>`, and the resilient `ScanEntriesAsync<T>`; the predicate
overloads are listed under [Predicate operations](#predicate-operations), and
the raw typed streams under [Public but not usable](#public-but-not-usable).

## Predicate operations

Typed overloads that take an `Expression<Func<T, bool>>` and evaluate it
**server-side** against a JSON document view of each value, so only matching
keys (or values) cross the wire. Each method has an explicit-serializer overload
and a `JsonLatticeSerializer<T>`-default overload (shown collapsed below). The
serializer must implement `ILatticePredicateSerializer` or the call throws
`NotSupportedException` before any RPC.

This table lists signatures only. For semantics, supported expressions, the
error surface, and runnable samples see
[Predicate Operations](predicated-operations.md).

| Method | Signature |
|--------|-----------|
| `GetManyAsync` | `Task<Dictionary<string, T>> GetManyAsync<T>(this ILattice, List<string> keys, Expression<Func<T, bool>> predicate, ILatticeSerializer<T> serializer, CancellationToken = default)` |
| `SetManyAsync` | `Task<IReadOnlyList<string>> SetManyAsync<T>(this ILattice, List<KeyValuePair<string, T>> entries, Expression<Func<T, bool>> predicate, ILatticeSerializer<T> serializer, CancellationToken = default)` |
| `SetManyAtomicAsync` | `Task<AtomicWriteOutcome> SetManyAtomicAsync<T>(this ILattice, List<KeyValuePair<string, T>> entries, Expression<Func<T, bool>> predicate, ILatticeSerializer<T> serializer, CancellationToken = default)` |
| `SetManyAtomicAsync` (idempotent) | `Task<AtomicWriteOutcome> SetManyAtomicAsync<T>(this ILattice, List<KeyValuePair<string, T>> entries, Expression<Func<T, bool>> predicate, string operationId, ILatticeSerializer<T> serializer, CancellationToken = default)` |
| `ScanKeysAsync` | `IAsyncEnumerable<string> ScanKeysAsync<T>(this ILattice, Expression<Func<T, bool>> predicate, ILatticeSerializer<T> serializer, string? startInclusive = null, string? endExclusive = null, bool reverse = false, bool? prefetch = null, int? maxAttempts = null, CancellationToken = default)` |
| `ScanEntriesAsync` | `IAsyncEnumerable<KeyValuePair<string, T>> ScanEntriesAsync<T>(this ILattice, Expression<Func<T, bool>> predicate, ILatticeSerializer<T> serializer, string? startInclusive = null, string? endExclusive = null, bool reverse = false, bool? prefetch = null, int? maxAttempts = null, CancellationToken = default)` |
| `ScanValuesAsync` | `IAsyncEnumerable<T> ScanValuesAsync<T>(this ILattice, ILatticeSerializer<T> serializer, Expression<Func<T, bool>>? predicate = null, string? startInclusive = null, string? endExclusive = null, bool reverse = false, bool? prefetch = null, int? maxAttempts = null, CancellationToken = default)` |
| `OpenKeyCursorAsync` | `Task<string> OpenKeyCursorAsync<T>(this ILattice, Expression<Func<T, bool>> predicate, ILatticeSerializer<T> serializer, string? startInclusive = null, string? endExclusive = null, bool reverse = false, bool pointInTime = false, CancellationToken = default)` |
| `OpenEntryCursorAsync` | `Task<string> OpenEntryCursorAsync<T>(this ILattice, Expression<Func<T, bool>> predicate, ILatticeSerializer<T> serializer, string? startInclusive = null, string? endExclusive = null, bool reverse = false, bool pointInTime = false, CancellationToken = default)` |
| `OpenSnapshotKeyCursorAsync` | `Task<string> OpenSnapshotKeyCursorAsync<T>(this ILattice, Expression<Func<T, bool>> predicate, ILatticeSerializer<T> serializer, string? startInclusive = null, string? endExclusive = null, bool reverse = false, CancellationToken = default)` |
| `OpenSnapshotEntryCursorAsync` | `Task<string> OpenSnapshotEntryCursorAsync<T>(this ILattice, Expression<Func<T, bool>> predicate, ILatticeSerializer<T> serializer, string? startInclusive = null, string? endExclusive = null, bool reverse = false, CancellationToken = default)` |
| `DeleteRangeAsync` | `Task<int> DeleteRangeAsync<T>(this ILattice, Expression<Func<T, bool>> predicate, string startInclusive, string endExclusive, ILatticeSerializer<T> serializer, CancellationToken = default)` |
| `OpenDeleteRangeCursorAsync` | `Task<string> OpenDeleteRangeCursorAsync<T>(this ILattice, Expression<Func<T, bool>> predicate, string startInclusive, string endExclusive, ILatticeSerializer<T> serializer, CancellationToken = default)` |

Supporting public types: `LatticePredicateTranslator`,
`LatticePredicateNode`, `LatticePredicateNodeKind`, `LatticeValueKind` (the kinds a
[structural `TypeOf` test](predicated-operations.md#structural-predicate-kinds)
checks for), `LatticeConstant`,
`LatticeConstantKind`, `LatticeComparisonOperator`, `LatticeBooleanOperator`,
`LatticeStringMethod`, `LatticePredicateContext`,
`ILatticePredicateSerializer`, the `AtomicWriteOutcome` enum
(`Committed`, `PreconditionFailed`), and `LatticePredicateEvaluation`,
whose `Matches(byte[]? value, in LatticePredicateNode predicate)` evaluates
the predicate IR against a value's UTF-8 JSON document with the same
semantics as server-side push-down (ordinal, case-insensitive property
matching; a `null`, empty, or non-JSON payload evaluates `false`), so an
enforcement add-on need not reimplement it.

## `ILatticeSerializer<T>`

Implement this interface to provide a custom serialization strategy.
`JsonLatticeSerializer<T>` ships as the default.

| Member | Signature | Description |
|--------|-----------|-------------|
| `Serialize` | `byte[] Serialize(T value)` | Converts a value to bytes for storage. |
| `Deserialize` | `T Deserialize(byte[] bytes)` | Converts bytes back to a value. |

## Idempotency keys and retry policy

Opt-in surface for retrying transient storage faults under a
caller-supplied identity. Default behaviour is throw-and-revert; the
library never installs a policy itself. See
[Retry Policy](retry-policy.md) for the full contract, operational
guidance, and code examples.

| Type | Member | Description |
|------|--------|-------------|
| `LatticeIdempotencyKey` | `HybridLogicalClock Timestamp { get; init; }` | Pins the logical write time. Re-stamped verbatim on every retry under the same key so LWW resolution treats retries as ties. |
| `LatticeIdempotencyKey` | `static LatticeIdempotencyKey Fresh()` | Convenience factory minting a fresh HLC tick. Consecutive calls produce distinct keys. |
| `LatticeIdempotencyContext` | `static bool IsActive { get; }` | `true` when an ambient idempotency key is set on the current logical execution context. |
| `LatticeIdempotencyContext` | `static LatticeIdempotencyKey? Current { get; set; }` | Reads or sets the ambient key on the current logical execution context. Flows across `await` points. |
| `LatticeIdempotencyContext` | `static IDisposable With(LatticeIdempotencyKey? key)` | Opens a scope that restores the previous ambient value on dispose. Idempotent on repeated dispose. |
| `LatticeIdempotencyContext` | `static IDisposable NewScope()` | Shorthand for `With(LatticeIdempotencyKey.Fresh())`. |
| `ILatticeRetryPolicy` | `Task ExecuteAsync(Func<CancellationToken, Task> operation, CancellationToken cancellationToken)` | Re-invokes `operation` on transient failure under the same ambient scope; rethrows on exhaustion. |
| `ILatticeRetryPolicy` | `Task<T> ExecuteAsync<T>(Func<CancellationToken, Task<T>> operation, CancellationToken cancellationToken)` | Typed overload. |
| `BoundedExponentialRetryPolicy` | `ctor(int maxAttempts = 4, TimeSpan? initialDelay = null, TimeSpan? maxDelay = null, Func<Exception, bool>? retryableExceptionClassifier = null)` | Shipped default. Delay between attempts: `min(MaxDelay, InitialDelay * 2^(attempt-1))`. No jitter. |
| `BoundedExponentialRetryPolicy` | `ctor(BoundedExponentialRetryPolicyOptions options)` | Options-based constructor used by the DI extension. |
| `BoundedExponentialRetryPolicyOptions` | `int MaxAttempts { get; set; }` (default 4) | Total attempts, including the first. |
| `BoundedExponentialRetryPolicyOptions` | `TimeSpan InitialDelay { get; set; }` (default 50 ms) | First retry backoff. |
| `BoundedExponentialRetryPolicyOptions` | `TimeSpan MaxDelay { get; set; }` (default 2 s) | Per-attempt backoff cap. |
| `BoundedExponentialRetryPolicyOptions` | `Func<Exception, bool>? RetryableExceptionClassifier { get; set; }` (default `null`) | When non-null, only exceptions accepted by the classifier are retried. |
| `LatticeServiceCollectionExtensions` | `static ISiloBuilder AddLatticeRetryPolicy(this ISiloBuilder builder, Action<BoundedExponentialRetryPolicyOptions>? configure = null)` | DI helper that installs `BoundedExponentialRetryPolicy` as `LatticeOptions.RetryPolicy` for every tree. |

## Public but not usable

A handful of types are necessarily `public` so that Orleans can
serialize them on the `ILattice` wire surface, but they are **not
intended as direct caller dependencies**. Their shape, members, and
return values can change in any release without notice; treat them
as wire-only.

To make this contract visible in tooling, every type and member in the
table below carries `[EditorBrowsable(EditorBrowsableState.Never)]`:
the wire-only types `HybridLogicalClock`, `VersionedValue`,
`RoutingInfo`, `LatticeIdempotencyKey`, `GatedMultiReadResult`,
`RangeDeleteResult`, and `LeafKeyRange`, and the raw `ILattice` /
`TypedLatticeExtensions` members listed. IDE IntelliSense therefore
hides them from completion lists by default (the standard IntelliSense
behaviour for `EditorBrowsable(Never)`), so callers do not surface them
by accident when typing against `ILattice` or its extensions. The
attribute only hides a symbol from completion - it still compiles - so
the row here remains the contract that it is not caller surface.

The types and members in this category are:

| Symbol | Why public |
|--------|------------|
| `HybridLogicalClock` | Embedded in the wire types below; appears as the `Version` slot of `Versioned<T>` / `VersionedValue`. |
| `VersionedValue` | Return shape of `ILattice.GetWithVersionAsync` (the raw `byte[]` overload). |
| `RoutingInfo` | Return shape of `ILattice.GetRoutingAsync`, consumed by infrastructure helpers such as the streaming bulk loader. |
| `LatticeIdempotencyKey` | Token stamped via `LatticeIdempotencyContext.With(...)` to deduplicate retries; callers usually let the ambient retry policy mint and propagate it. |
| `RangeDeleteResult` | Result of the internal leaf-level range-delete call (tombstoned count, past-range flag, matched keys); public only so Orleans can serialize it. |
| `ILattice.KeysAsync` / `EntriesAsync` | Raw streaming overloads that omit the resume/reconnect handshake. Use `LatticeExtensions.ScanKeysAsync` / `ScanEntriesAsync` instead. |
| `TypedLatticeExtensions.KeysAsync<T>` / `EntriesAsync<T>` / `ValuesAsync<T>` | The typed raw streams, likewise without reconnect handling. Use `ScanKeysAsync<T>` / `ScanEntriesAsync<T>` / `ScanValuesAsync<T>` instead. |
| `ILattice.GetRoutingAsync` (both overloads) | Direct shard-addressing primitive used by infrastructure helpers. |
| `ILattice.KeysWherePredicateAsync` / `EntriesWherePredicateAsync`, the `Open*CursorWherePredicateAsync` family, `SetManyWherePredicateAsync`, `SetManyAtomicWhereAsync`, `DeleteRangeWherePredicateAsync`, and the ranged `CountAsync` | Raw primitives behind the typed predicate and aggregation surfaces; call the typed [predicate operations](#predicate-operations) instead. |
| `GatedMultiReadResult` | Return shape of `ILattice.GetManyWithGateAccountingAsync`. |
| `LeafKeyRange` | Leaf key-range bounds returned by an internal leaf-grain call; public only so Orleans can serialize it. |

`ShardMap` is also `public` (it is reachable through `RoutingInfo`)
but is not marked hidden because it has no useful caller-facing
members beyond what `RoutingInfo` already exposes; treat it as
wire-only as well.

Apart from `ILattice`, `ILatticeAdmin`, `ILatticeLockGrain` (see
[Distributed lock](distributed-lock.md)), and `IAtomicActionGrain`
(see [Atomic actions](atomic-action.md)), every other grain interface in
the assembly - for example the shard root, leaf, internal node, registry,
cursor, atomic-write saga, cross-tree transaction, compaction, snapshot,
resize, reshard, shard split and consolidation, replication apply, WAL
shard, hot-shard monitor, tree deletion / merge, leaf-cache, leaf-replay
coordinator, queue, view, stats, storage-usage, and tx-registry grains -
is declared `internal` and is not visible from
consumer assemblies at all. The C# type system enforces that boundary
at compile time.
