Skip to content

Chain of Responsibility

The Chain of Responsibility pattern passes a request along an ordered chain of handlers. Each handler decides whether to process the request or forward it to the next one. The first capable handler short-circuits the chain.

sequenceDiagram
    participant P as TenantResolverPipeline
    participant HR as HeaderTenantResolver (100)
    participant JR as JwtClaimTenantResolver (200)
    participant CR as CustomResolver (300)

    P->>HR: ResolveAsync(httpContext)
    alt X-Tenant-Id header present
        HR-->>P: TenantInfo (short-circuit)
    else Header absent
        HR-->>P: null
        P->>JR: ResolveAsync(httpContext)
        alt JWT claim present
            JR-->>P: TenantInfo (short-circuit)
        else Claim absent
            JR-->>P: null
            P->>CR: ResolveAsync(httpContext)
            CR-->>P: TenantInfo or null
        end
    end
ComponentFileRole
TenantResolverPipelinesrc/Granit.MultiTenancy/Pipeline/TenantResolverPipeline.csIterates ITenantResolver instances by ascending Order
HeaderTenantResolversrc/Granit.MultiTenancy/Resolvers/HeaderTenantResolver.csOrder=100, reads X-Tenant-Id
JwtClaimTenantResolversrc/Granit.MultiTenancy/Resolvers/JwtClaimTenantResolver.csOrder=200, reads the JWT claim
ComponentFileOrderRole
MagicBytesValidatorsrc/Granit.BlobStorage/Validators/MagicBytesValidator.cs10Verifies the actual MIME type
MaxSizeValidatorsrc/Granit.BlobStorage/Validators/MaxSizeValidator.cs20Verifies the file size

The blob validation pipeline is a special case: all validators are executed (no short-circuit), but the order determines error message priority.

The tenant resolution chain allows adding resolvers (query string, cookie, subdomain) without modifying existing code. The ordering via the Order property is configurable without recompilation.

// Add a custom resolver -- inserts into the chain by Order
public sealed class SubdomainTenantResolver : ITenantResolver
{
public int Order => 50; // Before HeaderTenantResolver (100)
public Task<TenantInfo?> ResolveAsync(HttpContext context, CancellationToken cancellationToken)
{
string host = context.Request.Host.Host;
// Extract tenant from subdomain...
return Task.FromResult<TenantInfo?>(tenantInfo);
}
}
// Registration
services.AddSingleton<ITenantResolver, SubdomainTenantResolver>();