Skip to main content

Middleware

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

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);

// Configure middleware pipeline
dispatch.ConfigurePipeline("Default", pipeline =>
{
pipeline.Use<LoggingMiddleware>();
pipeline.Use<ValidationMiddleware>();
pipeline.Use<AuthorizationMiddleware>();
});
});

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
Processing600Handler execution
PostProcessing700Result processing
Error800Exception handling
ErrorHandling801Exception handling (alias)
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"));
});
});

In This Section

See Also

  • Pipeline - Full pipeline architecture and stages
  • Handlers - Action and event handlers
  • Transports - Transport-level middleware integration