Table of Contents

Orleans.Lattice.Api.Data.Grpc

This page documents Orleans.Lattice.Api.Data.Grpc 9.9.0, in 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 README.md, and llms.txt lists every page.

Code-first gRPC binding for Orleans.Lattice.Api.Data - 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
Public typed client LatticeDataApiGrpcClient over a caller-supplied channel, one method per RPC. Orleans.Lattice.Api.Data
Fail-closed authorization Per-call ILatticeDataApiAuthorizer seam, default-deny. Orleans.Lattice.Api.Data

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:

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:

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 below). Set the option to an empty string to disable header-based tenant selection entirely.

See the Orleans.Lattice.Api.Data overview 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 or LatticeOptions.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).
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 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.

Public surface

Type Role
LatticeDataApiGrpcClient Public typed client; one method per RPC over a caller-supplied CallInvoker.
LatticeDataApiGrpcOptions Server-side options (see 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