Granit.MultiTenancy
Granit.MultiTenancy adds HTTP-based tenant resolution to the framework. A pipeline of
pluggable resolvers extracts the tenant from headers or JWT claims, sets an AsyncLocal
context for the entire request, and EF Core query filters handle the rest. Three
isolation strategies available — from shared database to database-per-tenant.
Package structure
Section titled “Package structure”Single package — isolation strategies are configured in Granit.Persistence.
| Package | Role | Depends on |
|---|---|---|
Granit.MultiTenancy | Tenant resolution, CurrentTenant, middleware | Granit.Core |
[DependsOn(typeof(GranitMultiTenancyModule))]public class AppModule : GranitModule { }{ "MultiTenancy": { "IsEnabled": true, "TenantIdClaimType": "tenant_id", "TenantIdHeaderName": "X-Tenant-Id" }}Pipeline order is critical:
app.UseAuthentication();app.UseGranitMultiTenancy(); // After auth, before authorizationapp.UseAuthorization();Tenant resolution pipeline
Section titled “Tenant resolution pipeline”Resolvers execute in Order (ascending). First non-null result wins.
flowchart LR
R[Request] --> H{Header?}
H -->|X-Tenant-Id| T[TenantInfo]
H -->|missing| J{JWT claim?}
J -->|tenant_id| T
J -->|missing| N[No tenant]
T --> M[Middleware sets AsyncLocal]
M --> A[Request continues]
| Resolver | Order | Source |
|---|---|---|
HeaderTenantResolver | 100 | X-Tenant-Id HTTP header |
JwtClaimTenantResolver | 200 | tenant_id JWT claim |
Custom resolver
Section titled “Custom resolver”public sealed class SubdomainTenantResolver( ITenantStore tenantStore) : ITenantResolver{ public int Order => 50; // Before header resolver
public async Task<TenantInfo?> ResolveAsync( HttpContext context, CancellationToken cancellationToken = default) { string host = context.Request.Host.Host; string subdomain = host.Split('.')[0];
var tenant = await tenantStore .FindBySubdomainAsync(subdomain, cancellationToken) .ConfigureAwait(false);
return tenant is not null ? new TenantInfo(tenant.Id, tenant.Name) : null; }}Register it in DI — the pipeline auto-discovers all ITenantResolver implementations.
CurrentTenant
Section titled “CurrentTenant”The real implementation uses AsyncLocal<TenantInfo?> for thread-safe, per-async-flow
isolation:
// In any service — constructor injectionpublic class PatientService(ICurrentTenant currentTenant, AppDbContext db){ public async Task<List<Patient>> GetAllAsync(CancellationToken cancellationToken) { // EF Core query filter automatically applies WHERE TenantId = @tenantId return await db.Patients .ToListAsync(cancellationToken) .ConfigureAwait(false); }}Temporary tenant override
Section titled “Temporary tenant override”Switch tenant context for background jobs or cross-tenant admin operations:
public class TenantMigrationService(ICurrentTenant currentTenant){ public async Task MigrateDataAsync( Guid sourceTenantId, Guid targetTenantId, CancellationToken cancellationToken) { using (currentTenant.Change(sourceTenantId)) { var data = await LoadDataAsync(cancellationToken).ConfigureAwait(false);
using (currentTenant.Change(targetTenantId)) { await SaveDataAsync(data, cancellationToken).ConfigureAwait(false); } // Back to source tenant } // Back to original tenant (or no tenant) }}AllowAnonymousTenant
Section titled “AllowAnonymousTenant”Mark endpoints that don’t require a tenant context:
[AllowAnonymousTenant]app.MapGet("/health", () => Results.Ok());Granit.ApiDocumentation automatically excludes the X-Tenant-Id header from
OpenAPI docs for these endpoints.
Isolation strategies
Section titled “Isolation strategies”Three strategies configured in Granit.Persistence:
| Strategy | Isolation | Query filter | Connection |
|---|---|---|---|
SharedDatabase | WHERE TenantId = @id | Automatic via ApplyGranitConventions | Single connection string |
SchemaPerTenant | PostgreSQL SET search_path | Per-schema tables | Single connection, per-request schema |
DatabasePerTenant | Separate database | N/A (physical isolation) | Per-tenant connection from Vault |
Default strategy. All tenants share one database, isolated by EF Core query filters
on IMultiTenant entities.
{ "TenantIsolation": { "Strategy": "SharedDatabase" }}No additional setup — ApplyGranitConventions handles the TenantId filter.
Each tenant gets a dedicated PostgreSQL schema. TenantSchemaConnectionInterceptor
executes SET search_path on every connection open.
context.Services.AddTenantPerSchemaDbContext<AppDbContext>( options => options.UseNpgsql( context.Configuration.GetConnectionString("Default")));{ "TenantIsolation": { "Strategy": "SchemaPerTenant" }, "TenantSchema": { "NamingConvention": "TenantId", "Prefix": "tenant_" }}Maximum isolation — each tenant has its own database. Connection strings resolved
from Vault via ITenantConnectionStringProvider.
context.Services.AddSingleton<ITenantConnectionStringProvider, VaultTenantConnectionStringProvider>();
context.Services.AddTenantPerDatabaseDbContext<AppDbContext>( static (options, connectionString) => options.UseNpgsql(connectionString));{ "TenantIsolation": { "Strategy": "DatabasePerTenant" }}Wolverine context propagation
Section titled “Wolverine context propagation”When Wolverine dispatches messages, the OutgoingContextMiddleware injects X-Tenant-Id
into the message envelope. The TenantContextBehavior restores it on the receiving side:
HTTP Request (Tenant A) → Wolverine handler → background job X-Tenant-Id: A ───────────────────→ ICurrentTenant.Id = AThis ensures tenant isolation across async message processing without manual propagation.
Configuration reference
Section titled “Configuration reference”| Property | Default | Description |
|---|---|---|
IsEnabled | true | Enable/disable tenant resolution |
TenantIdClaimType | "tenant_id" | JWT claim name for tenant ID |
TenantIdHeaderName | "X-Tenant-Id" | HTTP header name for tenant ID |
Public API summary
Section titled “Public API summary”| Category | Key types | Package |
|---|---|---|
| Module | GranitMultiTenancyModule | Granit.MultiTenancy |
| Core (in Granit.Core) | ICurrentTenant, AllowAnonymousTenantAttribute | Granit.Core |
| Implementation | CurrentTenant, TenantInfo, ITenantInfo | Granit.MultiTenancy |
| Resolution | ITenantResolver, HeaderTenantResolver, JwtClaimTenantResolver, TenantResolverPipeline | Granit.MultiTenancy |
| Middleware | TenantResolutionMiddleware | Granit.MultiTenancy |
| Options | MultiTenancyOptions | Granit.MultiTenancy |
| Extensions | AddGranitMultiTenancy(), UseGranitMultiTenancy() | Granit.MultiTenancy |
See also
Section titled “See also”- Core module —
IMultiTenantinterface, data filter - Persistence module —
ApplyGranitConventions, isolation strategies - Wolverine module — Tenant context propagation in messages
- API Reference (auto-generated from XML docs)