BitzOrcas implements multi-tenancy through an 8-level tenant resolution chain with a unified ITenantContext and automatic data isolation via SqlSugar global filters.
Resolution chain
Priority 1: SystemJob → JobHost tenant context (ICurrentUserAccessor.BeginScope)Priority 2: RootOverride → Superadmin override (bypasses tenant)Priority 3: ApplicationCaller → HMAC/API Key client_id → TenantIdPriority 4: UserClaim → JWT claim "tenant_id"Priority 5: HostSubdomain → *.tenant.example.com subdomainPriority 6: Header → X-Tenant-Id headerPriority 7: Path → /api/tenants/{tenantId}/... path segmentPriority 8: SingleTenantDefault → Fallback for single-tenant modeEach step implements ITenantResolutionStep:
public interface ITenantResolutionStep{ int Order { get; } Task<TenantResolution?> ResolveAsync(TenantResolutionRequest request, CancellationToken ct = default);}Tenant status guard
TenantStatusGuard enforces tenant lifecycle — inactive/expired tenants are blocked:
public class TenantStatusGuard : ITenantGuard{ public Task<TenantGuardResult> CheckAsync(TenantId tenantId, CancellationToken ct = default);}| Status | Behavior |
|---|---|
| Active | All operations allowed |
| Suspended | Read-only, mutations blocked |
| Expired | All operations blocked |
| Not Found | 404 response |
PlatformTenant entity
public class PlatformTenantEntity : EntityBase{ public string Name { get; set; } public string? Domain { get; set; } public TenantStatus Status { get; set; } public string? Configuration { get; set; } // JSON config}Data isolation
SqlSugar global filters enforce automatic tenant isolation:
// Applied to all TenantEntityBase queries automaticallydb.QueryFilter.AddTableFilter<TenantEntityBase>( e => e.TenantId == currentTenantId);Cross-tenant queries
For admin/superuser cross-tenant operations:
// Bypass tenant filter with explicit queryvar allTenants = await db.Queryable<NoteEntity>() .IgnoreFilter() // Bypass global filter .ToListAsync();Tenant context
public interface ITenantContext{ TenantId? CurrentTenantId { get; } bool IsResolved { get; } bool IsRootOverride { get; }}- Scoped per request (API) or per job execution (JobHost)
SystemJobTenantContextsingleton provides tenant identity for background jobsICurrentUserAccessor.BeginScope()enables run-as scoping
Configuration
{ "Tenancy": { "Mode": "MultiTenant", // or "SingleTenant" "DefaultTenantId": "0" }}See also
- Multitenancy deep dive — Comprehensive architecture guide
- Mediator pipeline diagram — Shows TenantGuard in pipeline