Reference-architecture host projects

This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at README.md, and llms.txt lists every page.

Three minimal, production-shaped reference host projects for the active-active, cross-region Orleans.Lattice estate described in the root reference-architecture.md. Each project references the published Orleans.Lattice NuGet packages (from nuget.org): the Silo and MCP heads pin the 9.8.x line (each package at its newest 9.8.x release, so the exact version differs by package), and the Explorer head pins the 9.4.x line of the Orleans.Lattice.Explorer.* libraries - including Orleans.Lattice.Explorer.Entra.Web for hosted-web OIDC sign-in - plus Orleans.Lattice.Caching.AzureBlob 9.8.1 for its distributed token cache. Each .csproj holds the exact pins. They are not project references into src/, so each head consumes the released library exactly as a real deployment would.

All three heads also reference Common/Orleans.Lattice.ReferenceArchitecture.Hosting.csproj, a shared hosting library that is not a published package (it references only the ASP.NET Core shared framework). It supplies the Front Door origin lock described below, the guard that confines the silo's /metrics and /lattice/scale endpoints to its internal HTTP port (404 on any other port), and the filter that drops informational request logs for successful requests on high-frequency probe paths. Its tests live in Common.Tests/.

Host Project Role
Silo Silo/Orleans.Lattice.ReferenceArchitecture.Silo.csproj The always-on Orleans silo: Azure Table clustering + durable Azure Table WAL, cross-region replication (shipper + receiver), the Azure Blob backup sink (primary/standby), the read-only State API, the read-write Data API, and the auth-admin, backup, schema, and tree-administration control planes over gRPC (plus the replication control plane when enabled), the lattice.scaling compute-axis signal, OpenTelemetry /metrics, and Entra auth.
Mcp Mcp/Orleans.Lattice.ReferenceArchitecture.Mcp.csproj A stateless remote MCP server (AddLatticeMcpRemote over gRPC) fronting the silo, with the telemetry tool module and Entra auth.
Explorer Explorer/Orleans.Lattice.ReferenceArchitecture.Explorer.csproj A standalone Explorer web console (Blazor Server) that connects, as a gRPC/gRPC-web client, to the silo's State + Auth gRPC endpoint, with a hosted-web Entra (OpenID Connect) sign-in and a distributed token cache over the region storage account.

All external inputs (connection targets, tenant / client ids, the replication key, the peer list, merge modes, the backup-primary flag) come from environment variables / IConfiguration. No secret is hardcoded: the only secret, the per-cluster replication key, is read from the environment (LATTICE_REPLICATION_SECRET, injected from Key Vault at deploy time). Managed identity is the first-class Azure storage auth mode (DefaultAzureCredential + service URIs); a connection string is accepted only as the local / emulator fallback.

For a genuinely runnable local stack (Azurite + all three hosts + Prometheus + Grafana, no Azure), see ../local/.

Configuration surface

Configuration uses the standard .NET IConfiguration binding: a key like Silo:HttpPort is set by the environment variable Silo__HttpPort (double underscore separator, case-insensitive).

Silo

Key Default Meaning
Cluster:Id / Cluster:ServiceId lattice / Cluster:Id Orleans cluster / service id.
Silo:HttpPort / Silo:GrpcPort 8080 / 8081 HTTP port (health, scaling, /metrics) and HTTP/2 gRPC port (state, auth, replication).
Silo:SiloPort / Silo:GatewayPort 11111 / 30000 Orleans silo-to-silo and gateway ports.
Silo:AdvertisedIp (auto) Advertised IP for Orleans endpoints when the default NIC probe is not appropriate.
Storage:ConnectionString - Emulator / dev storage connection string (Azurite). Takes precedence over the service URIs when set; without it both Storage:TableServiceUri and Storage:BlobServiceUri are required, or the host fails to start.
Storage:TableServiceUri / Storage:BlobServiceUri - Managed-identity storage endpoints (production).
Wal:TableName / Clustering:TableName / Reminders:TableName / GrainStorage:TableName OrleansLatticeWal / ...Clustering / ...Reminders / ...Grains Azure Table names.
Replication:ClusterId Cluster:Id This region's replication cluster id.
Replication:Peers - Enrolled peers as clusterId=endpoint,clusterId=endpoint: the clusters this region ships to (and whose cross-cluster saga calls it accepts). Must be reciprocal across regions.
Replication:Trees - Per-tree wire merge mode as treeName=MergeMode,... (for example orders=LwwRegister; must match on both ends).
Replication:AllowPlaintext false Allow http:// peer endpoints (local dev only; Azure uses server TLS).
Replication:EnableRuntimeConfig false Runtime per-tree replication control plane: enrols the sys-replication-config tree and co-hosts the replication control facade and its gRPC binding, fail-closed behind an authored Replication grant. Compute binds it to the deployer's replication-control switch.
Replication:EnableDigestAntiEntropy / Replication:DigestProbeIntervalSeconds false / 0 Cross-cluster anti-entropy (digest probe, Merkle-walk drift localisation, bounded automatic repair), set symmetrically per region; a positive interval overrides the package's probe cadence.
Backup:Primary false true on the single designated backup-primary region (scheduler on); false on DR standbys (scheduler off).
Backup:ContainerName orleans-lattice-backup Blob container for the backup sink.
Backup:BlobConnectionString / Backup:BlobServiceUri - Optional dedicated storage for the backup sink (an emulator connection string, or a managed-identity blob endpoint). When neither is set the sink uses the Storage:* identity.
Backup:FullIntervalHours / Backup:IncrementalIntervalMinutes / Backup:RetentionKeepLast 24 / 60 / 7 Schedule tuning (primary only).
Scaling:MinReplicas 1 Floor for the compute-axis scaling signal.
StateApi:RequireAuthorization false When true, the silo's gRPC facades require authorization. With Entra on, their coarse transport gates are opened and the deny-by-default per-subject access gate, keyed on the caller's Entra identity, is the real enforcement; with Entra off, the state surface is gated by the turnkey env-var credential authorizer (a shared username / password). Local dev leaves it false (a documented bypass); a deployment sets it true behind the Entra front door.
DataApi:Enabled true Exposes the read-write Data API gRPC binding, co-hosted on the silo gRPC port (same origin as the State API). Enabled by default; every mutation is still subject-checked by the deny-by-default access gate. Set false to withhold the write surface entirely.
Auth:DefaultEffect Deny Deny (secure default) or Allow (fully-open local dev cluster).
Auth:BootstrapAdministrators - Comma-separated subject ids seeded as administrators.
Auth:DevAuthenticateForwardedSubject false Local dev bypass, honoured only when Entra is off: trust a forwarded bearer token as its named subject when that id is a configured bootstrap administrator.
Entra:Enabled false Enable Entra-backed authentication for the exposed facades.
Entra:TenantId / Entra:ClientId / Entra:Authority / Entra:Audiences - / - / https://login.microsoftonline.com/{TenantId}/v2.0 / ClientId and api://{ClientId} Entra authenticator configuration. TenantId and ClientId are required when Entra is on; Audiences is comma-separated.
Entra:Algorithms RS256 Comma-separated allow-list of accepted JWT signature algorithms (the header alg), pinned as defense-in-depth against algorithm-confusion attacks (CWE-347). Defaults to RS256, the algorithm Entra issues v2.0 tokens with; a token advertising any other algorithm is refused.
Entra:Graph:UseManagedIdentity false Enables the app-only Microsoft Graph group resolver via a secret-less managed identity (DefaultAzureCredential). Compute sets this true on the silo when Entra is on. Ignored when Entra:Graph:ClientSecret is supplied.
Entra:Graph:ClientSecret - Dev / back-compat override: enables the app-only Microsoft Graph group resolver with a client secret (injected from Key Vault). Takes precedence over managed identity when set.

Mcp

Key Default Meaning
Mcp:StateEndpoint (required) The silo's State gRPC endpoint.
Mcp:AuthEndpoint Mcp:StateEndpoint The silo's Auth gRPC endpoint (needed for permission-scoped discovery).
Mcp:DataEndpoint / Mcp:BackupEndpoint - Data / backup gRPC endpoints. Compute sets Mcp:DataEndpoint to the silo gRPC FQDN when the Data API is enabled (the default; the write facade rides that endpoint), and Mcp:BackupEndpoint to the same FQDN when the deployer's backup-control switch is on (also the default). The silo always co-hosts the backup facade.
Mcp:ReplicationEndpoint - Replication control gRPC endpoint. Compute sets it to the silo gRPC FQDN when replication control is enabled.
Mcp:TreeAdminEndpoint Mcp:StateEndpoint Tree-administration and schema-control gRPC endpoint (co-hosted on the silo gRPC port).
Mcp:RequireAuthorization Entra:Enabled Fail-closed toggle on the MCP HTTP endpoint.
Mcp:EnableDataWrites / Mcp:EnableBackupControl / Mcp:EnableReplicationControl / Mcp:EnableAuthAdministration false Advertise the mutating tool verbs of each group. Compute binds the first three to the deployer's Data API, backup-control, and replication-control switches (all on by default); EnableAuthAdministration stays false.
Mcp:EnableTreeAdminLifecycle / Mcp:EnableTreeAdminSchemaControl true / true Advertise the mutating tree-lifecycle tools (create, alias, per-tree config, delete / recover / purge, bulk load, restore, reshard, resize, snapshot, WAL moves, view and tag-index maintenance, compaction, retention, orphaned-leaf repair) and the schema-mutation tools. The read-only tree-administration tools are advertised either way, and every call is still gated at the silo.
Mcp:Stateless true Stateless streamable-HTTP transport (no per-session server state), so a follow-up request can land on any region or replica behind Front Door.
Mcp:AdministratorToken / Mcp:AdministratorScheme - / Bearer Service credential for discovery-time permission introspection of non-administrator callers.
Mcp:Telemetry:BackendAddress - PromQL backend for the telemetry tool module (only wired when set).
Mcp:Telemetry:AuthMode None Backend auth mode. None for an unauthenticated backend (local compose Prometheus). DynamicBearer makes the head send a rotating managed-identity Entra token with each query to an Azure Monitor managed-Prometheus endpoint (no static secret), cached and refreshed shortly before it expires; the workload identity needs Monitoring Data Reader on the workspace.
Mcp:Telemetry:Scope https://prometheus.monitor.azure.com/.default Access-token scope for DynamicBearer mode; override only for a non-default Azure Monitor audience.
Mcp:RegionId / Mcp:ClusterId current / - This head's own (default) region id and cluster id, surfaced by lattice_list_regions and targeted when a tool call supplies no region. Compute sets them to the region code and Orleans cluster id.
Mcp:VerifyRegionIdentity false Probe each peer region's state facade once and reject a peer whose endpoint does not reach its advertised cluster (an anycast/Front Door misconfiguration). Compute sets it true whenever peer regions are wired.
Mcp:Regions:{n}:RegionId / :ClusterId / :StateEndpoint / :AuthEndpoint / :DataEndpoint / :BackupEndpoint / :ReplicationEndpoint / :TreeAdminEndpoint - The peer regions a caller may target via the optional per-call region selector. Each peer is dialed at its DIRECT region-pinned silo gRPC FQDN (the same endpoint replication uses), which serves every facade group. Compute populates these on pass 2 from the sibling regions' silo FQDNs.
Entra:Enabled / Entra:TenantId / Entra:Authority / Entra:Audience / Entra:ClientId - Entra JWT validation on the front door; the token is forwarded to and re-validated by the silo.
Mcp:PublicUrl / Mcp:Oauth:Scopes - With Entra on, the head's public URL enables OAuth 2.0 Protected Resource Metadata (RFC 9728) discovery at /.well-known/oauth-protected-resource, advertising the given scopes. Compute sets both from the Front Door MCP endpoint and the silo user_impersonation scope.
Mcp:DevAuthenticateAll / Mcp:DevSubjectId false / local-dev-admin Local dev bypass, forced off when Entra is on: authenticate every request as one synthetic subject.

Explorer

Key Default Meaning
Explorer:ConfigFilePath %TEMP%/lattice-explorer/config.json Writable JSON config backing store (the chiseled non-root image has no writable app-data dir).
Entra:Enabled false Enable the hosted-web Microsoft Entra (OpenID Connect, auth-code + PKCE) sign-in provider.
Entra:TenantId - Directory (tenant) the console signs operators in against.
Entra:WebClientId - The Explorer console's OWN confidential web-app registration (holds the OIDC redirect URIs); NOT the silo facade audience.
Entra:Scopes - Comma-separated downstream State API scope requested on-behalf-of the operator (for example api://{tenantId}/{base}-silo/user_impersonation). Empty resolves the scope from the advertised audience.
Entra:ClientSecret - Optional confidential-client secret. Left unset in Azure: the container authenticates secret-lessly via a federated managed-identity assertion (AZURE_CLIENT_ID).
Entra:TokenCache:BlobServiceUri - Blob endpoint of the per-region account backing the Microsoft.Identity.Web distributed token cache, so tokens are shared across warm replicas and survive restart. Empty falls back to an in-memory cache. Consumed via the AZURE_CLIENT_ID managed identity.
Entra:TokenCache:ContainerName explorer-token-cache Container (on the per-region account) that stores the token cache.
Explorer:PublicOrigin - The public origin (scheme + host) operators reach the console at - the Front Door Explorer endpoint - so OpenID Connect builds its sign-in redirect URIs against that host rather than the internal Container Apps origin. Empty leaves requests untouched (local / compose).
LATTICE_EXPLORER_ENDPOINT - The remote State/Auth gRPC endpoint the console connects to (read by the explorer's own environment bootstrap).
LATTICE_EXPLORER_INSECURE_DEV - true to allow the local h2c dev transport.
LATTICE_EXPLORER_TRANSPORT_HEADERS - Semicolon-separated Name=Value non-secret headers sent on every call to the silo. Compute sets X-Azure-FDID=<frontDoorId> so the console, which dials the silo origin directly, passes its origin lock.
LATTICE_EXPLORER_USERNAME / LATTICE_EXPLORER_PASSWORD - Inert in this host. The web Explorer honours this first-run sign-in seed only when a host opts in with LatticeExplorerWebOptions.AllowEnvironmentCredentialSeed, and this host does not, so sign-in is always interactive (Entra OIDC when enabled, otherwise the console's sign-in dialog).

Front Door origin lock (all hosts)

Key Default Meaning
LATTICE_FRONT_DOOR_ID - The Azure Front Door profile id (a GUID). When set, every host rejects (HTTP 403) any request whose X-Azure-FDID header is absent, duplicated, or does not match this id (compared case-insensitively) - so only traffic that actually traversed the estate's Front Door instance is served. When empty or unset the lock is disabled (local dev / docker-compose, and deploy pass 1 before the Front Door exists).

The lock always exempts the platform health probe path (/health), which ACA calls on the container directly, bypassing Front Door. The Silo host also exempts /metrics (the OpenTelemetry scrape) and /lattice/scale (the KEDA compute-axis signal), which are served on the internal-only HTTP port and are likewise probed directly, and the replication engine's silo-to-silo gRPC services (/orleans.lattice.replication.LatticeReplication, .LatticeRemoteSnapshot, and .LatticeSaga), which peer regions dial directly and which authenticate every call with the shared replication key. Exemptions match on whole path segments, so a lookalike such as /healthz remains locked.

Container images

Each host has a multi-stage Dockerfile:

  • Build stage mcr.microsoft.com/dotnet/sdk:10.0.
  • Final stage mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled - framework-dependent, distroless, shell-less, and non-root by default (the chiseled base runs as the app user, UID 1654).

Because the final image has no shell, health checks are HTTP/TCP only (the /health endpoint on the HTTP port); there is no shell to exec. TLS is terminated at the platform ingress, so the containers serve plain HTTP internally. NativeAOT and aggressive trimming are out of scope - Orleans (and Blazor Server, for the Explorer) do not support them.

InvariantGlobalization audit

All three images set InvariantGlobalization=true (in the .csproj and reinforced by DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 in the Dockerfile), dropping the ICU dependency to shrink the image and reduce surface. This is only safe if no correctness-relevant comparison depends on culture-aware (ICU-backed) collation or casing.

Method. Enumerate every string-comparison and case/format site across the Lattice surface these hosts consume and classify each as ordinal (ICU-independent) or culture-sensitive.

Result: PASS.

  • The core library's string comparisons are overwhelmingly ordinal / OrdinalIgnoreCase (487 sites when this audit was taken) - the default for keys, tree names, header names, and identifiers.
  • Every ToLowerInvariant() / ToUpperInvariant() site across the whole consumed surface operates on a guaranteed-ASCII input:
    • lowercased gRPC header names - the credential-header lookup in each gRPC facade's header credential bridge, and the active-tenant header lookups;
    • lowercased enum names (the view maintainer's WAL-saturation metric tag, the MCP head's tool-group name, and the Explorer UI's display and CSS-class labels);
    • a boolean-ish config token in EnvironmentExplorerBootstrap ("1" / "true" / "yes" / "on");
    • lowercased hex digest strings (the replication restore saga's deterministic ids). ASCII invariant casing is code-point based and does not consult ICU, so it behaves identically with or without ICU.
  • Every remaining culture reference pins CultureInfo.InvariantCulture explicitly for number formatting / parsing (offsets, shard indices, counter values). Invariant number formatting is available and unchanged under InvariantGlobalization=true.

There are no culture-sensitive (ICU-backed) linguistic comparisons, casings, or collations on any correctness-relevant path. InvariantGlobalization=true is therefore correctness-safe for all three hosts.

For the Explorer head specifically, the only residual culture effect is display-side formatting (numbers, dates) and UI list ordering rendering in the invariant culture rather than the operator's locale - a cosmetic change that is acceptable for an operator console, since tree keys and access decisions are compared ordinally underneath.

Decision: enable InvariantGlobalization in all three hosts.