Skip to content
bitzorcas
EN

Reference

Multitenancy module

Multi-tenancy system — 8-level ITenantResolver chain, TenantStatusGuard, PlatformTenantEntity, tenant lifecycle management, and cross-tenant query patterns.

Last updated

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 → TenantId
Priority 4: UserClaim → JWT claim "tenant_id"
Priority 5: HostSubdomain → *.tenant.example.com subdomain
Priority 6: Header → X-Tenant-Id header
Priority 7: Path → /api/tenants/{tenantId}/... path segment
Priority 8: SingleTenantDefault → Fallback for single-tenant mode

Each 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);
}
StatusBehavior
ActiveAll operations allowed
SuspendedRead-only, mutations blocked
ExpiredAll operations blocked
Not Found404 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 automatically
db.QueryFilter.AddTableFilter<TenantEntityBase>(
e => e.TenantId == currentTenantId);

Cross-tenant queries

For admin/superuser cross-tenant operations:

// Bypass tenant filter with explicit query
var 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)
  • SystemJobTenantContext singleton provides tenant identity for background jobs
  • ICurrentUserAccessor.BeginScope() enables run-as scoping

Configuration

{
"Tenancy": {
"Mode": "MultiTenant", // or "SingleTenant"
"DefaultTenantId": "0"
}
}

See also