---
title: "Orleans.Lattice.Api.Data.Grpc"
url: "https://nsta1.github.io/Orleans.Lattice/docs/lattice.api.data.grpc/README.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/docs/lattice.api.data.grpc/README.md"
package: "Orleans.Lattice.Api.Data.Grpc"
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.api.data.grpc/llms-full.txt"
---
# Orleans.Lattice.Api.Data.Grpc

Part of the [documentation map](../index.md).

Code-first gRPC binding for [Orleans.Lattice.Api.Data](../lattice.api.data/README.md) - projects the write-capable data-API facade onto a gRPC service and a public typed client, using Orleans-serialized request / response records that wrap the facade DTOs, with no hand-written `.proto`.

## What is it?

`Orleans.Lattice.Api.Data.Grpc` is the remote transport for the cluster data API. Hosts reference it when a client, a CLI tool, or the `Orleans.Lattice.Api.Mcp` MCP server in its remote topology needs to perform key/value reads and opt-in writes over the network rather than in-process.

It provides:

- **A code-first gRPC service.** Ten unary RPCs - point read, range read, set, delete, range delete, non-atomic bulk upsert, the two atomic multi-key writes (single-tree and cross-tree), typed CRDT write, and typed CRDT read - bound from C# definitions rather than a `.proto`. The service is exposed under the fully-qualified gRPC service name `orleans.lattice.api.data`, so each method's full path is `/orleans.lattice.api.data/<Rpc>`.
- **A public typed client.** `LatticeDataApiGrpcClient` exposes one method per RPC over a caller-supplied gRPC channel.
- **Shared Orleans marshalling.** Every wire message is a `[GenerateSerializer]` record serialized with the Orleans binary serializer, so client and server stay in lock-step by construction. Most RPCs use this package's own request / response records, wrapping facade DTOs where needed; `Get` answers with the facade's `DataReadResult`, and `ReadRange` and `DeleteRange` carry the facade's `DataRangeRequest` / `DataRangePage` and `DataRangeDeleteRequest` / `DataRangeDeleteResult` unchanged.
- **Fail-closed authorization.** A per-call `ILatticeDataApiAuthorizer` seam gates every RPC; the default denies all traffic until a host configures one.

The package has no external broker and no `.proto` file to maintain.

## Core Properties

- **Write-capable, opt-in.** The binding exposes reads plus mutating verbs (set, delete, range delete, non-atomic bulk upsert, atomic single-tree and cross-tree writes, and typed CRDT writes); every mutation runs through the same core access gate as a read, which fails closed once `Orleans.Lattice.Auth` is registered.
- **Public client, internal service.** Callers consume `LatticeDataApiGrpcClient`; the service, marshallers, and method definitions are internal.
- **No transport policy in the client.** Address, TLS, retries, deadlines, and credentials live on the caller's `GrpcChannel` / `CallInvoker`.
- **Fail-closed.** Unconfigured, the binding denies every call: the default `DenyAllDataApiAuthorizer` is registered via `TryAdd`, so a host must register a real `ILatticeDataApiAuthorizer` (or turn enforcement off) before any call succeeds.

## Features

| Feature | What it gives you | Docs |
|---|---|---|
| **Code-first service** | Unary RPCs bound from C# - no `.proto` to author or keep in sync. | [`Orleans.Lattice.Api.Data`](../lattice.api.data/README.md) |
| **Public typed client** | `LatticeDataApiGrpcClient` over a caller-supplied channel, one method per RPC. | [`Orleans.Lattice.Api.Data`](../lattice.api.data/README.md) |
| **Fail-closed authorization** | Per-call `ILatticeDataApiAuthorizer` seam, default-deny. | [`Orleans.Lattice.Api.Data`](../lattice.api.data/README.md) |

## Quick Start

Register the binding on a silo that already has the data-API facade, then map its routes. The binding fails closed, so register an `ILatticeDataApiAuthorizer` implementation before serving traffic:

```csharp verify
var builder = WebApplication.CreateBuilder();
builder.Services.AddLatticeDataApiGrpc(o => o.RequireAuthorization = true);

var app = builder.Build();
app.MapLatticeDataApiGrpc();
```

The public client wraps a caller-supplied channel and exposes one method per RPC - `GetAsync`, `ReadRangeAsync`, `SetAsync`, `DeleteAsync`, `DeleteRangeAsync`, `SetManyAsync`, `SetManyAtomicAsync`, `SetManyAtomicCrossTreeAsync`, `CrdtWriteAsync`, and `CrdtReadAsync`. A mutating call the caller is not permitted to make surfaces as a `PermissionDenied` `RpcException` rather than an unhandled error.

### Per-tenant selection

On a cluster running the optional tenancy add-on, the call's *active tenant* scopes both the **tree namespace** the call addresses and its **capacity governance** (write admission and quota enforcement). The binding lifts the active tenant from a single request header - `lattice-active-tenant` by default, configurable through `LatticeDataApiGrpcOptions.ActiveTenantHeaderName` - and stamps it onto the call's ambient scope, so the facade resolves each request's tree name into that tenant's `t/{tenant}/{name}` namespace and the silo-side admission controller charges the operation to it:

```csharp verify
var builder = WebApplication.CreateBuilder();
builder.Services.AddLatticeDataApiGrpc(o =>
{
    o.RequireAuthorization = true;
    o.ActiveTenantHeaderName = "lattice-active-tenant";
});
```

The header carries only an *assertion*: the tenancy add-on re-validates it against the caller's subject membership downstream, exactly as it validates the caller credential. An absent, blank, or syntactically invalid header asserts no tenant, so the call resolves the default tenant and its tree names are used unchanged. An asserted tenant the caller may not act as (any assertion by an anonymous caller included) - or, under an asserted tenant, a `sys-` tree or a malformed `t/` id that belongs to no tenant - is refused by that fail-closed resolution and surfaces as a `PermissionDenied` `RpcException`. A call that resolves cleanly but *breaches the tenant's quota* is a different failure - a capacity outcome, not an authorization one - and surfaces as a `ResourceExhausted` `RpcException` carrying the breached dimension as a trailer (see [Quota refusals](#quota-refusals) below). Set the option to an empty string to disable header-based tenant selection entirely.

See the [`Orleans.Lattice.Api.Data` overview](../lattice.api.data/README.md) for the full facade, its surfaces, and the shared authorization model.

## Quota refusals

A write refused by admission control - a per-tree ceiling ([`LatticeOptions.MaxLiveKeys`](../lattice/configuration/options-reference-2.md#maxlivekeys) or [`LatticeOptions.MaxEstimatedBytes`](../lattice/configuration/options-reference-2.md#maxestimatedbytes)), or, with the tenancy add-on registered, the active tenant's own quota or sustained request-rate budget - surfaces as a `ResourceExhausted` `RpcException`, the same canonical "refused, back off" code the binding already uses for storage saturation. The status message is the self-contained refusal text; the breached dimension and, where the dimension carries one, the observed value and the configured ceiling are attached as response trailers so a client can branch without parsing prose:

| Trailer | Value |
|---------|-------|
| `lattice-quota-dimension` | `keys`, `bytes`, `memory`, `trees`, or `ops-per-second`. |
| `lattice-quota-tree` | The tree whose admission quota was breached, as the effective tree id the call resolved to (`t/{tenant}/{name}` for a tenant-scoped tree). |
| `lattice-quota-current` | The observed value on the breached dimension. Omitted for a dimension with no numeric ceiling. |
| `lattice-quota-limit` | The configured ceiling on the breached dimension. Omitted for a dimension with no numeric ceiling. |

The dimension is what decides the client's next move: `ops-per-second` is **transient** (the tenant's rate budget refills continuously, so an immediate retry after a short backoff succeeds), while the footprint dimensions persist until usage drops or an operator raises the ceiling. No trailer of its own carries the tenant id - the caller asserted its own active tenant, so a separate server-side attribution adds nothing it did not already send - but the response does not withhold it either: a refusal of the tenant's own quota or rate budget names the tenant in its status message, and the `lattice-quota-tree` trailer of a tenant-scoped tree carries it as the tree id's `t/{tenant}` segment. As with every trailer this binding emits, a key and a value are never disclosed.

## Status mapping

The service maps every facade outcome onto an explicit gRPC status rather than letting it fall through to a generic fault:

| Exception | gRPC status | Why |
|---|---|---|
| `LatticeAuthorizationDeniedException` | `PermissionDenied` | The core gate denied the call. The `lattice-denied-tree`, `lattice-denied-operation`, `lattice-denied-subject`, and `lattice-denied-reason` trailers carry the non-sensitive fields of the denial, never a value. A refusal by the transport authorizer is also `PermissionDenied`, without trailers. |
| `LatticeTenantAccessDeniedException` | `PermissionDenied` | Fail-closed tenant resolution refused the call (see [Per-tenant selection](#per-tenant-selection)). |
| `ArgumentException` | `InvalidArgument` | A malformed request, including a `ReadRange` continuation token that names an unknown, drained, or closed cursor. |
| `LatticeReservedTreeNamespaceException` | `InvalidArgument` | The call named a tree in a reserved, internally-composed namespace (`_lattice_`, `sys-`, or `t/`). |
| `LatticeCrdtShapeNotRegisteredException` | `FailedPrecondition` | A typed OR-Map verb targeted a tree whose host never registered the map shape. |
| `LatticeIdempotencyKeyMismatchException` | `FailedPrecondition` | An atomic batch reused an `OperationId` with a different key or tree set; nothing was applied. |
| `LatticeReplicationModeMismatchException` | `FailedPrecondition` | A write used a shape that differs from the single merge mode the replicated tree is declared with. |
| `LatticeSaturatedException` | `ResourceExhausted` | The tree is WAL-saturated and shed the call; back off and retry. |
| `LatticeQuotaExceededException` | `ResourceExhausted` | An admission cap refused the write; see [Quota refusals](#quota-refusals) for its trailers. |
| `OperationCanceledException` | `Cancelled` | The caller's deadline or cancellation token fired. |
| anything else | `Internal` | Logged server-side and returned with a generic message, without echoing the exception text. |

A typed counter advance (`CounterIncrement`, `CounterDecrement` or `GCounterIncrement`) that would take a replica's component past `long.MaxValue` throws `OverflowException` before anything is written. The service has no arm for it, so it falls through to `Internal` like an unexpected fault; a negative `Amount` is an `ArgumentOutOfRangeException` and so surfaces as `InvalidArgument`.

## Options

`LatticeDataApiGrpcOptions`, bound through `AddLatticeDataApiGrpc(configure)`, has four properties: `RequireAuthorization` (`bool`, default `true`), `CredentialHeaderName` (`string`, default `"authorization"`), `CredentialScheme` (`string`, default `"Bearer"`), and `ActiveTenantHeaderName` (`string`, default `"lattice-active-tenant"`). Their full semantics are in the [data API configuration reference](../lattice.api.data/configuration.md#latticedataapigrpcoptions).

## Public surface

| Type | Role |
|------|------|
| `LatticeDataApiGrpcClient` | Public typed client; one method per RPC over a caller-supplied `CallInvoker`. |
| `LatticeDataApiGrpcOptions` | Server-side options (see [Options](#options)). |
| `ILatticeDataApiAuthorizer` | Transport meta-authorization seam: `Task<bool> IsAuthorizedAsync(LatticeDataApiAuthorizationContext authorizationContext, CancellationToken cancellationToken)`. |
| `DenyAllDataApiAuthorizer` | Default-deny authorizer (registered automatically via `TryAdd`). |
| `AllowAllDataApiAuthorizer` | Opt-in permissive authorizer for trusted-network use. |
| `LatticeDataApiAuthorizationContext` | Per-call description handed to the authorizer: `Operation`, `TargetTreeId` (`null` for the cross-tree batch, which spans several trees), and the underlying `ServerCallContext`. |
| `LatticeDataApiOperation` | The operation behind each RPC: `SetPoint`, `DeletePoint`, `SetManyAtomic`, `SetManyAtomicCrossTree`, `GetPoint`, `ReadRange`, `DeleteRange`, `SetMany`, `CrdtWrite`, `CrdtRead`, and `Unknown` for an unmapped method. |
| `ILatticeDataApiCredentialBridge` | Identity seam that lifts the inbound credential onto the ambient context; the default reads `CredentialHeaderName` and strips a case-insensitive `CredentialScheme` prefix. |
| `ILatticeDataApiActiveTenantBridge` | Active-tenant seam (`TenantId? Resolve(ServerCallContext context)`) that lifts the caller's asserted tenant onto the ambient scope; the default reads `ActiveTenantHeaderName`. |
| `Data*` / `Crdt*` request and response records | Public Orleans-serialized wire messages for the ten RPCs - including the `CrdtWriteOp` selector (the twenty typed-CRDT mutations) a `CrdtWriteRequest` carries, the `CrdtKind` selector (the thirteen CRDT types) a `CrdtReadRequest` carries, and the nested `CrdtMapField` / `CrdtVectorEntry` rows - with their stable aliases in `GrpcDataTypeAliases`; the facade DTOs that `Get`, `ReadRange`, and `DeleteRange` reuse keep their `DataApiTypeAliases` aliases from `Orleans.Lattice.Api.Abstractions`. |
| `AddLatticeDataApiGrpc` / `MapLatticeDataApiGrpc` | Registration and endpoint-routing extensions. |

## Reference

- [`Orleans.Lattice.Api.Data`](../lattice.api.data/README.md) - the write-capable data-API facade this binding projects, including the operation set and the fail-closed access gate.
- [`Orleans.Lattice.Api.Abstractions`](../lattice.api.abstractions/README.md) - the shared, versioned API contract the facade and this binding consume.
