Skip to content

Middleware Pipeline

The Middleware Pipeline pattern chains interceptor components around the main processing of a request or message. Each middleware can execute logic before and after the handler, short-circuit the chain, or enrich the context.

Granit implements a dual pipeline:

  1. ASP.NET Core Middleware: for HTTP requests (tenant resolution, idempotency)
  2. Wolverine Behaviors/Middleware: for asynchronous messages (tenant, user, and trace context propagation)

Both pipelines converge through OutgoingContextMiddleware, which injects context headers into outgoing Wolverine envelopes.

sequenceDiagram
    participant C as Client HTTP
    participant TRM as TenantResolutionMiddleware
    participant IDM as IdempotencyMiddleware
    participant H as HTTP Handler
    participant OCM as OutgoingContextMiddleware
    participant OB as Wolverine Outbox
    participant TCB as TenantContextBehavior
    participant UCB as UserContextBehavior
    participant TrCB as TraceContextBehavior
    participant BH as Background Handler

    C->>TRM: HTTP Request
    TRM->>TRM: Resolves X-Tenant-Id or JWT claim
    TRM->>IDM: next()
    IDM->>IDM: Checks Idempotency-Key
    IDM->>H: next()
    H->>OCM: Publishes message
    OCM->>OCM: Injects X-Tenant-Id, X-User-Id, traceparent
    OCM->>OB: Envelope + headers
    H-->>C: HTTP Response

    Note over OB,BH: Asynchronous processing

    OB->>TCB: Dispatch message
    TCB->>TCB: Restores ICurrentTenant from header
    TCB->>UCB: Before()
    UCB->>UCB: Restores ICurrentUserService via AsyncLocal
    UCB->>TrCB: Before()
    TrCB->>TrCB: Links Activity to traceparent
    TrCB->>BH: Handler.HandleAsync()
    BH-->>TrCB: Result
    TrCB-->>UCB: After()
    UCB-->>TCB: After()
MiddlewareFileRole
TenantResolutionMiddlewaresrc/Granit.MultiTenancy/Middleware/TenantResolutionMiddleware.csResolves the tenant via TenantResolverPipeline (Header > JWT)
IdempotencyMiddlewaresrc/Granit.Idempotency/Internal/IdempotencyMiddleware.csStripe-style HTTP idempotency with state machine
BehaviorFileRole
TenantContextBehaviorsrc/Granit.Wolverine/Behaviors/TenantContextBehavior.csRestores ICurrentTenant from X-Tenant-Id header
UserContextBehaviorsrc/Granit.Wolverine/Behaviors/UserContextBehavior.csRestores ICurrentUserService via IWolverineUserContextSetter
TraceContextBehaviorsrc/Granit.Wolverine/Behaviors/TraceContextBehavior.csLinks the W3C traceparent to the handler’s Activity

Wolverine pipeline — outgoing middleware

Section titled “Wolverine pipeline — outgoing middleware”
MiddlewareFileRole
OutgoingContextMiddlewaresrc/Granit.Wolverine/Middleware/OutgoingContextMiddleware.csInjects X-Tenant-Id, X-User-Id, traceparent into outgoing envelopes

All behaviors and middlewares are registered in src/Granit.Wolverine/Extensions/WolverineHostApplicationBuilderExtensions.cs via opts.Policies.AddMiddleware<T>().

ProblemSolution
The HTTP handler knows the tenant, but the background handler does notOutgoingContextMiddleware > headers > TenantContextBehavior restores the context
The EF Core audit interceptor needs ModifiedBy in backgroundUserContextBehavior restores ICurrentUserService via AsyncLocal
OpenTelemetry traces are disjointed between HTTP and asyncTraceContextBehavior links spans via traceparent (W3C Trace Context)
Cross-cutting concerns pollute handlersLogic extracted into reusable, composable middlewares
// The handler has no awareness of middlewares --
// tenant/user/trace context is already restored when it executes
public static class SendInvoiceHandler
{
// Wolverine discovers this handler automatically
public static async Task Handle(
SendInvoiceCommand command,
ICurrentTenant currentTenant, // restored by TenantContextBehavior
ICurrentUserService currentUser, // restored by UserContextBehavior
InvoiceDbContext db,
CancellationToken cancellationToken)
{
// currentTenant.Id is correct even in background
// currentUser.UserId is correct for the audit trail
Invoice invoice = await db.Invoices.FindAsync([command.InvoiceId], ct)
?? throw new EntityNotFoundException(typeof(Invoice), command.InvoiceId);
invoice.MarkAsSent();
await db.SaveChangesAsync(ct);
// AuditedEntityInterceptor records ModifiedBy = currentUser.UserId
}
}