Skip to content

Null Object

The Null Object pattern replaces null checks with an object that implements the expected interface with neutral (no-op) behavior. Calling code treats the Null Object like any other implementation, eliminating conditional branches.

classDiagram
    class ICurrentTenant {
        +Id : Guid?
        +IsAvailable : bool
        +Change(id) IDisposable
    }

    class CurrentTenant {
        +Id : Guid?
        +IsAvailable : bool = true
        +Change(id) IDisposable
    }

    class NullTenantContext {
        +Id : Guid? = null
        +IsAvailable : bool = false
        +Change(id) IDisposable = no-op
        +Instance : NullTenantContext
    }

    ICurrentTenant <|.. CurrentTenant
    ICurrentTenant <|.. NullTenantContext
Null ObjectFileInterfaceBehavior
NullTenantContextsrc/Granit.Core/MultiTenancy/NullTenantContext.csICurrentTenantIsAvailable = false, Id = null, Change() is a no-op
NullCacheValueEncryptorsrc/Granit.Caching/NullCacheValueEncryptor.csICacheValueEncryptorPasses bytes through without encryption (dev)
NullWebhookDeliveryStoresrc/Granit.Webhooks/Internal/NullWebhookDeliveryStore.csIWebhookDeliveryStoreNo-op operations

NullTenantContext is registered by default in the DI container. It is replaced by CurrentTenant only when Granit.MultiTenancy is installed. This is the soft dependency: all modules can inject ICurrentTenant without depending on Granit.MultiTenancy.

Without the Null Object, every module would have to check whether ICurrentTenant is null or whether multi-tenancy is installed. With NullTenantContext, code simply checks IsAvailable — never null.

// Code works identically with or without multi-tenancy
public sealed class FeatureChecker(IServiceProvider sp)
{
public async Task<string?> GetValueAsync(string featureName, CancellationToken cancellationToken)
{
ICurrentTenant? currentTenant = sp.GetService<ICurrentTenant>();
// NullTenantContext: IsAvailable = false -> tenantId = null
// CurrentTenant: IsAvailable = true -> tenantId = Guid
Guid? tenantId = currentTenant?.IsAvailable == true
? currentTenant.Id
: null;
// The rest of the code is identical in both cases
return await ResolveFeatureValueAsync(featureName, tenantId, ct);
}
}