Skip to content

Middleware Ordering

By default, middleware executes in the order it was registered with dependency injection. While this works for simple cases, as your middleware pipeline grows you may need explicit control over execution order.

The [MiddlewareOrder] attribute lets you define a numeric order on your middleware classes. Lower values run first (outermost in the pipeline), and higher values run closer to the handler.

Apply [MiddlewareOrder(int)] to your middleware class:

[MiddlewareOrder(-100)]
[MediatorSingleton]
public class ValidationMiddleware<TRequest, TResult> : IRequestMiddleware<TRequest, TResult>
where TRequest : IRequest<TResult>
{
public async Task<TResult> Process(
IMediatorContext context,
RequestHandlerDelegate<TResult> next,
CancellationToken cancellationToken)
{
// Validation runs first (outermost) — before logging, caching, etc.
Validate(context.Message);
return await next();
}
}
[MiddlewareOrder(0)]
[MediatorSingleton]
public class LoggingMiddleware<TRequest, TResult> : IRequestMiddleware<TRequest, TResult>
where TRequest : IRequest<TResult>
{
public async Task<TResult> Process(
IMediatorContext context,
RequestHandlerDelegate<TResult> next,
CancellationToken cancellationToken)
{
// Logging wraps the inner middleware and handler
Log.Debug("Before {Request}", typeof(TRequest).Name);
var result = await next();
Log.Debug("After {Request}", typeof(TRequest).Name);
return result;
}
}
[MiddlewareOrder(100)]
[MediatorSingleton]
public class CachingMiddleware<TRequest, TResult> : IRequestMiddleware<TRequest, TResult>
where TRequest : IRequest<TResult>
{
public async Task<TResult> Process(
IMediatorContext context,
RequestHandlerDelegate<TResult> next,
CancellationToken cancellationToken)
{
// Caching runs last (closest to handler) — cache the final result
return await next();
}
}

With the configuration above, the execution flows as:

Validation → Logging → Caching → Handler → Caching → Logging → Validation

RuleDescription
Lower = firstA middleware with order -100 runs before order 0 which runs before order 100
Default is 0Middleware without [MiddlewareOrder] defaults to order 0
Stable sortMiddleware with the same order value preserves DI registration order
All typesWorks for request, command, event, and stream middleware
Open genericsWorks with both open generic and closed generic middleware

If you don’t use [MiddlewareOrder] at all, everything works exactly as before — middleware executes in DI registration order. The attribute is completely opt-in.

Choose a consistent ordering convention for your project. A common pattern is to use negative values for cross-cutting concerns that should run first (validation, auth) and positive values for middleware that should be close to the handler (caching, offline).