Tenancy across grain calls
A multi-tenant application resolves its tenant from the web request: a header, a claim, a host name. A grain call has no request. When a job step runs, anything it resolves that depends on “the current tenant” falls back to whatever your tenant resolution does without a request, while the job’s explicit writes go to the namespace its key names. Nothing fails. The two halves of the same job quietly use different tenants, and the problem only shows once a second tenant exists.
Choreography closes that gap with two Orleans call filters. You tell them how your application represents a tenant, and they re-establish it around every grain call that can name one.
Implement ITenancy
Section titled “Implement ITenancy”ITenancy is the one contract you own. It lives in Cratis.Choreography.Tenancy:
| Member | What you answer |
|---|---|
Current | The tenant the current logical flow definitely acts for, or null when none is established. The outgoing filter records it onto grain calls. |
TenantFor(scope, @namespace) | The tenant a job’s coordinates belong to, or null when they name no tenant. |
Establish(tenant) | Makes tenant current for the logical flow until the returned scope is disposed, then restores the previous tenant. |
Choreography treats scope and namespace as opaque. Chronicle uses the event store name as the scope and the tenant’s namespace
as the namespace; another host might use the tenant as the scope. Only you know the mapping, which is why TenantFor is yours.
using Cratis.Choreography.Tenancy;
public sealed class AmbientTenancy : ITenancy{ static readonly AsyncLocal<string?> _tenant = new();
public string? Current => _tenant.Value;
public string? TenantFor(string scope, string @namespace) => string.IsNullOrEmpty(@namespace) ? null : @namespace;
public IDisposable Establish(string tenant) { var previous = _tenant.Value; _tenant.Value = tenant; return new Restore(previous); }
sealed class Restore(string? previous) : IDisposable { public void Dispose() => _tenant.Value = previous; }}Hold the tenant in something that flows with the logical call, such as AsyncLocal<T>, and connect it to the same source your
tenant-scoped services read.
Register the filters
Section titled “Register the filters”AddTenancy<TTenancy>() extends ISiloBuilder. It registers your type as the singleton ITenancy and adds both call filters:
using Cratis.Choreography.Tenancy;
builder.Host.UseOrleans(silo => silo.AddTenancy<AmbientTenancy>());Register it on every silo. It is independent of AddCratisChoreography, so the order of the two calls does not matter.
How a call gets its tenant
Section titled “How a call gets its tenant”The incoming filter, TenantIncomingCallFilter, picks the tenant in this order:
- From the grain key. For the jobs manager, job and job step grains, the key names a scope and namespace, and the filter
asks
TenantForfor the tenant. The key survives restarts, so a job resumed after a crash still runs for the tenant it was started for, and the key wins over anything the caller sent. - From the caller. For every other grain, the tenant rides the Orleans
RequestContextentryCratis-Tenant-ID, which the outgoing filter,TenantOutgoingCallFilter, wrote fromCurrenton the way out.TenantRequestContext.Keyholds the key name. - Nothing. A call that names no tenant either way runs exactly as before.
When the filter finds a tenant it calls Establish(tenant) and disposes the returned scope when the call ends.
Tenancy and workflows
Section titled “Tenancy and workflows”The workflow engine uses the same contract. With an ITenancy on the silo, the default IWorkflowExecutionScopes treats a
workflow scope as the tenant: entering a scope calls Establish with the scope’s value, and a jobs manager key maps back to a
scope through TenantFor, falling back to the key’s namespace. Without ITenancy, nothing is established. The
Chronicle variant maps through the event store and namespace the same way. See
Host and start workflows.
Next steps
Section titled “Next steps”- Host the Choreography silo shows where
AddTenancysits next to the rest of the silo setup. - Hosting API lists every registration method.