Skip to content

Scope / Context Manager

The Scope Manager pattern encapsulates a context change in an IDisposable object. The context is modified at scope creation and automatically restored on Dispose(), guaranteeing a return to the previous state even when an exception occurs.

sequenceDiagram
    participant A as Application
    participant CT as CurrentTenant
    participant AL as AsyncLocal

    A->>CT: Change(tenantB)
    CT->>AL: Save previous state (tenantA)
    CT->>AL: Write tenantB
    CT-->>A: IDisposable (TenantScope)

    A->>A: Execute in tenantB context

    A->>CT: Dispose()
    CT->>AL: Restore tenantA
ScopeFileManaged context
TenantScopesrc/Granit.MultiTenancy/CurrentTenant.csICurrentTenant.Change(tenantId) — restores previous tenant
FilterScopesrc/Granit.Core/DataFiltering/DataFilter.csIDataFilter.Disable<T>() — re-enables the filter
Wolverine behaviorssrc/Granit.Wolverine/Behaviors/UserContextBehavior.csIWolverineUserContextSetter.Change() — restores user

All scopes use AsyncLocal<T> for thread-safe propagation across async/await boundaries.

The C# using pattern guarantees Dispose() even when an exception occurs, eliminating the risk of context leaks (a tenant remaining active after an error).

// Temporary tenant change -- automatic restoration
using (currentTenant.Change(adminTenantId))
{
// All queries in this scope target adminTenantId
List<AuditLog> logs = await db.AuditLogs.ToListAsync(ct);
} // Previous tenant is automatically restored
// Temporarily disable the soft delete filter
using (dataFilter.Disable<ISoftDeletable>())
{
// Deleted records are visible
List<Patient> allPatients = await db.Patients.ToListAsync(ct);
} // Filter is automatically re-enabled