Skip to content

Singleton

The Singleton pattern ensures a class has only one instance and provides a global access point to it. In Granit, the Singleton takes two forms: DI singletons (managed by the container) and AsyncLocal singletons (thread-safe state per async flow).

classDiagram
    class NullTenantContext {
        -Instance : NullTenantContext
        +IsAvailable : bool = false
        +Id : Guid? = null
    }

    class CurrentTenant {
        -_current : AsyncLocal of TenantInfo
        +Id : Guid?
        +IsAvailable : bool
        +Change(tenantId) IDisposable
    }

    class DataFilter {
        -_state : AsyncLocal of ImmutableDictionary
        +IsEnabled of T() bool
        +Disable of T() IDisposable
    }
SingletonFileTypeScope
NullTenantContext.Instancesrc/Granit.Core/MultiTenancy/NullTenantContext.csstatic readonlyGlobal
CurrentTenant._currentsrc/Granit.MultiTenancy/CurrentTenant.csAsyncLocal<TenantInfo?>Per async flow
DataFilter._statesrc/Granit.Core/DataFiltering/DataFilter.csAsyncLocal<ImmutableDictionary>Per async flow
DI servicesAll modulesAddSingleton<T>()DI container

Custom variant — AsyncLocal Singleton: a static readonly AsyncLocal<T> field provides singleton state per async/await flow, thread-safe without locks. Each Task inherits state from its parent, but modifications are isolated per flow thanks to copy-on-write (ImmutableDictionary).

NullTenantContext eliminates null checks throughout the framework when multi-tenancy is not installed. AsyncLocal singletons allow propagating context (tenant, filters) across async/await boundaries without relying on HttpContext.

// NullTenantContext -- always available, never null
ICurrentTenant tenant = serviceProvider.GetRequiredService<ICurrentTenant>();
// If Granit.MultiTenancy is not installed: tenant.IsAvailable == false
// No NullReferenceException, no if (tenant != null) check
// AsyncLocal -- state isolated per async flow
using (currentTenant.Change(newTenantId))
{
// This async flow sees newTenantId
await Task.Run(async () =>
{
// This child flow inherits newTenantId
Guid? id = currentTenant.Id; // == newTenantId
});
}
// Here, the previous tenant is restored