Granit-Specific Variants
Some patterns in Granit are variants or hybrids of classic patterns, adapted to the framework’s specific constraints (GDPR/ISO 27001, multi-tenancy, Wolverine). This page catalogues the 10 main “in-house” variants.
1. AsyncLocal Singleton (thread-safe context)
Section titled “1. AsyncLocal Singleton (thread-safe context)”Classic pattern: Singleton with a single global instance.
Granit variant: static readonly AsyncLocal<T> — a singleton state
per async flow, thread-safe without locks.
private static readonly AsyncLocal<TenantInfo?> _current = new();The state is inherited by child flows (Task.Run) but modifiable independently
thanks to copy-on-write semantics.
2. ImmutableDictionary Copy-on-Write
Section titled “2. ImmutableDictionary Copy-on-Write”Classic pattern: Copy-on-Write on data structures.
Granit variant: AsyncLocal<ImmutableDictionary<Type, bool>> prevents
mutations in a child flow from propagating to the parent flow.
_state.Value = _state.Value!.SetItem(typeof(TFilter), false);// New dictionary created — the original remains intactThis specifically solves the “AsyncLocal trap” where a shared Dictionary<T>
would see child mutations affecting the parent.
3. Null Object as Soft Dependency
Section titled “3. Null Object as Soft Dependency”Classic pattern: Null Object with a single interface.
Granit variant: NullTenantContext is the DI default, replaced only when
Granit.MultiTenancy is installed. All modules access ICurrentTenant via
Granit.Core.MultiTenancy without a direct dependency on Granit.MultiTenancy.
The Null Object is architecturally a soft dependency mechanism — an optional package does not break packages that implicitly depend on it.
4. Enum-based Strategy Selection
Section titled “4. Enum-based Strategy Selection”Classic pattern: Strategy with interface + factory.
Granit variant: TenantIsolationStrategy is a simple enum. The factory uses
a switch expression to select the implementation.
TenantIsolationStrategy.SharedDatabase => new SharedDatabaseDbContextFactory(...),TenantIsolationStrategy.SchemaPerTenant => new TenantPerSchemaDbContextFactory(...),TenantIsolationStrategy.DatabasePerTenant => new TenantPerDatabaseDbContextFactory(...),Simpler than an abstract factory when the number of strategies is finite and known at compile time.
5. Property-based Proxy for EF Core
Section titled “5. Property-based Proxy for EF Core”Classic pattern: Proxy with method delegation.
Granit variant: FilterProxy exposes properties (not methods) because
EF Core can only translate property accesses in query filters.
internal sealed class FilterProxy(IDataFilter? dataFilter, ICurrentTenant? tenant){ public bool SoftDeleteEnabled => dataFilter?.IsEnabled<ISoftDeletable>() ?? true;}This works around an EF Core limitation, invisible to application developers.
6. Dual Sync/Async Template Method
Section titled “6. Dual Sync/Async Template Method”Classic pattern: Template Method with abstract hooks.
Granit variant: GranitModule exposes sync/async pairs. The async version
delegates to the sync version by default.
// A module can override one OR the other — not required to implement bothpublic virtual Task ConfigureServicesAsync(ServiceConfigurationContext context){ ConfigureServices(context); return Task.CompletedTask;}Avoids forcing async on simple modules while allowing it for modules that need it (e.g., Vault secret retrieval during startup).
7. Implicit Outbox via Handler Return Type
Section titled “7. Implicit Outbox via Handler Return Type”Classic pattern: Explicit transactional outbox.
Granit variant (native Wolverine): a handler returning IEnumerable<T>
automatically produces Outbox messages.
public static IEnumerable<object> Handle(CreatePatientCommand cmd, AppDbContext db){ db.Patients.Add(new Patient { /* ... */ }); yield return new PatientCreatedOccurred { /* ... */ }; // local queue yield return new SendWelcomeEmailCommand { /* ... */ }; // Outbox}The handler is pure and declarative. Infrastructure (Outbox, routing) is entirely managed by Wolverine.
8. Zero-allocation Streaming Hash
Section titled “8. Zero-allocation Streaming Hash”Classic pattern: SHA-256 on a complete buffer.
Granit variant: IncrementalHash.CreateHash(SHA256) +
ArrayPool<byte>.Shared in the idempotency middleware.
src/Granit.Idempotency/Internal/IdempotencyMiddleware.csThe HTTP body is hashed in streaming mode without allocating a complete buffer, critical for large requests (uploads).
9. Multi-tenant Composite Key (idempotency)
Section titled “9. Multi-tenant Composite Key (idempotency)”Classic pattern: Idempotency key = client header.
Granit variant: The Redis key includes {tenantId}:{userId}:{key},
guaranteeing multi-tenant and multi-user isolation.
Two different tenants can use the same idempotency key without collision. A single user with two clients cannot replay another client’s request.
10. Expression Tree Query Filters
Section titled “10. Expression Tree Query Filters”Classic pattern: Static EF Core query filters.
Granit variant: ApplyGranitConventions() dynamically builds a combined
expression via Expression.AndAlso() for each entity.
src/Granit.Persistence/Extensions/ModelBuilderExtensions.csThis solves the EF Core problem where multiple calls to HasQueryFilter()
overwrite previous filters. The single combined expression handles
ISoftDeletable + IActive + IMultiTenant + IProcessingRestrictable +
IPublishable together.