Skip to main content

Middleware

Middleware components process messages before and after handlers, providing cross-cutting concerns like logging, validation, and authorization.

Why Should I Care?

Without middleware, every handler ends up with copy-pasted try/catch logging, manual validation checks, and authorization guards. Middleware lets you write cross-cutting logic once and apply it to every message automatically -- just like ASP.NET Core middleware but for your dispatch pipeline. Add logging, validation, retry, and auth without touching any handler code.

Before You Start

  • .NET 8.0+ (or .NET 9/10 for latest features)
  • Install the required package:
    dotnet add package Excalibur.Dispatch
  • Familiarity with handlers and pipeline concepts

Overview

flowchart LR
subgraph Pipeline["Middleware Pipeline"]
direction LR
M1[Logging] --> M2[Validation]
M2 --> M3[Authorization]
M3 --> M4[Custom...]
M4 --> H[Handler]
H --> M4
M4 --> M3
M3 --> M2
M2 --> M1
end

A[Request] --> M1
M1 --> R[Response]

Quick Start

Register Middleware

builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);

// Fluent pipeline extensions (recommended)
dispatch.UseExceptionMapping()
.UseAuthentication()
.UseAuthorization()
.UseValidation()
.UseThrottling()
.UseRetry()
.UseCircuitBreaker();
});
Pipeline.Use vs UseX()

Both approaches register the same middleware. The Use{Feature}() extensions are shorthand for the explicit pipeline API:

// Shorthand (recommended)
dispatch.UseValidation();

// Equivalent explicit registration
dispatch.ConfigurePipeline("Default", pipeline =>
{
pipeline.Use<ValidationMiddleware>();
});

Create Custom Middleware

public class TimingMiddleware : IDispatchMiddleware
{
private readonly ILogger<TimingMiddleware> _logger;

public TimingMiddleware(ILogger<TimingMiddleware> logger)
{
_logger = logger;
}

public DispatchMiddlewareStage? Stage => DispatchMiddlewareStage.Logging;

public async ValueTask<IMessageResult> InvokeAsync(
IDispatchMessage message,
IMessageContext context,
DispatchRequestDelegate next,
CancellationToken ct)
{
var sw = Stopwatch.StartNew();

// Call next middleware
var result = await next(message, context, ct);

sw.Stop();
_logger.LogInformation(
"{MessageType} completed in {ElapsedMs}ms",
message.GetType().Name,
sw.ElapsedMilliseconds);

return result;
}
}

Middleware Stages

Middleware executes in defined stages for consistent ordering:

StageOrderPurpose
Start0Pipeline starting point
RateLimiting50Throttle requests
PreProcessing100Early processing, enrichment
Instrumentation150Performance metrics, telemetry
Authentication175Verify identity
Logging190Structured logging
Validation200Input validation
Serialization250Message serialization
Authorization300Permission checks
Cache400Response caching
Optimization450Batching, bulk operations
Routing500Message routing
Deduplication599Deduplication and idempotency checks
Processing600Handler execution
PostProcessing700Result processing
ErrorHandling800Error handling and compensation
End1000Pipeline final stage

Stage Assignment

public class MyMiddleware : IDispatchMiddleware
{
// Explicit stage
public DispatchMiddlewareStage? Stage => DispatchMiddlewareStage.Validation;

// Or null for default ordering
// public DispatchMiddlewareStage? Stage => null;
}

Custom Stage Ordering

builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);

dispatch.ConfigurePipeline("Default", pipeline =>
{
// Add at specific stage
pipeline.UseAt<CustomMiddleware>(DispatchMiddlewareStage.PreProcessing);

// Middleware is ordered by stage - set the Stage property
// on your middleware class to control execution order
pipeline.Use<CustomMiddleware>();
});
});

Built-in Middleware

MiddlewareStagePackageDescription
LoggingMiddlewareLoggingExcalibur.DispatchStructured request/response logging
ValidationMiddlewareValidationExcalibur.DispatchFluentValidation / DataAnnotations
AspNetCoreAuthorizationMiddlewareAuthorizationExcalibur.Dispatch.Hosting.AspNetCoreASP.NET Core [Authorize] policy bridge
AuthorizationMiddleware (A3)AuthorizationExcalibur.A3Activity-based [RequirePermission] authorization
ExceptionMiddlewareErrorHandlingExcalibur.DispatchException to result conversion
MetricsMiddlewareLoggingExcalibur.DispatchOpenTelemetry metrics
TracingMiddlewarePreProcessingExcalibur.DispatchDistributed tracing

Message Context

Access and modify the message context:

public async ValueTask<IMessageResult> InvokeAsync(
IDispatchMessage message,
IMessageContext context,
DispatchRequestDelegate next,
CancellationToken ct)
{
// Read direct properties (hot-path, preferred)
var userId = context.UserId;
var correlationId = context.CorrelationId;

// Read/write custom items dictionary
var customValue = context.GetItem<string>("CustomKey");
context.SetItem("ProcessedAt", DateTime.UtcNow);

// Access scoped services
var db = context.RequestServices.GetRequiredService<IDbConnection>();

return await next(message, context, ct);
}

Short-Circuiting

Return early without calling the next middleware:

public async ValueTask<IMessageResult> InvokeAsync(
IDispatchMessage message,
IMessageContext context,
DispatchRequestDelegate next,
CancellationToken ct)
{
// Check cache
if (TryGetCached(message, out var cached))
{
return cached; // Short-circuit - don't call next
}

// Continue pipeline
var result = await next(message, context, ct);

// Cache result
Cache(message, result);

return result;
}

Conditional Middleware

Apply middleware based on conditions:

builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);

dispatch.ConfigurePipeline("Default", pipeline =>
{
// Only in development
pipeline.UseWhen<DebugMiddleware>(
sp => sp.GetRequiredService<IHostEnvironment>().IsDevelopment());

// Only for specific message types
pipeline.UseWhen<AuditMiddleware>(
message => message is IDispatchAction);

// Based on configuration
pipeline.UseWhen<FeatureMiddleware>(
sp => sp.GetRequiredService<IConfiguration>()
.GetValue<bool>("Features:NewFeature"));
});
});
  1. Built-in Middleware -- Start here. See what's available out of the box.
  2. Custom Middleware -- Create your own middleware for application-specific concerns.
  3. Validation -- Automatic input validation with FluentValidation.
  4. Authorization -- Permission and policy checks.
  5. Serialization Providers -- Message serialization options.

Next Steps

  • Pipeline -- Understand pipeline stages and execution order
  • Configuration -- Register middleware via the dispatch builder

See Also

  • Handlers -- Action and event handlers that middleware wraps
  • Transports -- Transport-level middleware integration
  • Observability -- Tracing and metrics for middleware execution