Tenant scopes
Register tenant propagation explicitly, before building your service provider:
services.AddRelayerTenancy(contextPoolCapacity: 64);
services.AddRelayer(builder =>
{
builder.AddMessageTypesFromThisAssembly();
// Configure native transports, routes and single-message consumers here.
});
TenantContext is a scoped service. In an application scope, set its ID after authorizing the tenant:
await using var scope = provider.CreateAsyncScope();
var tenant = scope.ServiceProvider.GetRequiredService<TenantContext>();
tenant.Id = new WireString(authorizedTenantId);
await scope.ServiceProvider.GetRequiredService<ISender>().SendAsync(message);
Every scoped send carries relayer.tenant_id in the existing native headers. This includes
SendNowAsync, outbox payloads, requests and replies. A send snapshots the tenant before invoking
send middleware. While a scoped tenant is set, SendOptions.Headers cannot override that header;
use a separate authorized scope to send for another tenant. An empty ID means no scoped tenant.
Explicit headers continue to work when no scoped tenant is set.
For incoming messages, Relayer restores the header before resolving the scoped sender, handler
constructor dependencies, or EF Core transaction middleware.
Inject TenantContext into a scoped handler or database factory to read the ID. Each in-process retry
gets a fresh scope restored from the original header. Missing or empty headers produce an empty ID.
Consumers in a provider without AddRelayerTenancy leave tenant headers unread.
Authorize tenant IDs before using them to select resources. Incoming headers are untrusted routing metadata, not authentication. Reject missing or unauthorized tenants according to your application's policy before opening a database connection or performing business work. Relayer does not validate membership or choose a default tenant.
IDs use WireString, preserving native UTF-8 storage without decoding or copying. Calling ToString()
on UTF-8 allocates; use Text, Utf8 or TryFormat where possible. Borrowed values must remain valid
until processing and sends complete. Do not retain TenantContext beyond its DI scope or dispose it
yourself. Relayer clears its ID and returns it to a provider-owned pool during scope disposal.
Size contextPoolCapacity for all concurrent application and consumer scopes. Above that capacity,
scopes still work but overflow contexts are allocated and only the configured number is retained.
Batch consumers share one scope, so composition rejects them when tenancy is registered. Use single-message consumers and their native concurrency controls. Tenant propagation does not change native ordering, settlement, retries, or transaction semantics.
Tenant databases and dispatch
Automatic IOutboxStoreResolver selection and ITenantSource dispatch enumeration remain planned
under #38. There is no shared tenant connection budget
yet. Existing PostgreSQL and EF Core
integrations continue to use explicitly selected connections and contexts. EF Core consumer composition
validates a context in a temporary startup scope whose tenant is empty; database factories must also
support that validation. The wire header is preserved
in outbox payloads; automatic population of the separate tenant_id column remains planned.