Middleware
Middleware in Foundatio Mediator provides a powerful pipeline for implementing cross-cutting concerns like validation, logging, authorization, and error handling. Middleware can run before, after, and finally around handler execution.
Basic Middleware
Create middleware by following naming conventions:
public class LoggingMiddleware
{
private readonly ILogger<LoggingMiddleware> _logger;
public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
{
_logger = logger;
}
public void Before(object message)
{
_logger.LogInformation("Handling {MessageType}", message.GetType().Name);
}
public void After(object message)
{
_logger.LogInformation("Handled {MessageType}", message.GetType().Name);
}
}Middleware Conventions
Class Names
Middleware classes must end with Middleware
Method Names
Valid middleware method names:
Before/BeforeAsyncAfter/AfterAsyncFinally/FinallyAsync
Method Parameters
- First parameter: The message (can be
object, interface, or concrete type) - Remaining parameters: Injected via DI (including
CancellationToken)
Lifecycle Methods
Before
Runs before the handler. Can return values that are passed to After and Finally:
public class TimingMiddleware
{
public Stopwatch Before(object message)
{
return Stopwatch.StartNew();
}
public void Finally(object message, Stopwatch stopwatch)
{
stopwatch.Stop();
Console.WriteLine($"Handled {message.GetType().Name} in {stopwatch.ElapsedMilliseconds}ms");
}
}After
Runs after successful handler completion. Only called if the handler succeeds:
public class AuditMiddleware
{
public void After(object message, IUserContext userContext)
{
_auditLog.Record($"User {userContext.UserId} executed {message.GetType().Name}");
}
}Finally
Always runs, regardless of success or failure. Receives exception if handler failed:
public class ErrorHandlingMiddleware
{
public void Finally(object message, Exception? exception)
{
if (exception != null)
{
_errorLog.Record(message, exception);
}
}
}Short-Circuiting with HandlerResult
Middleware can short-circuit handler execution by returning a HandlerResult from the Before method:
Real-World Validation Example
Let's look at the validation middleware from the sample:
public class ValidationMiddleware
{
public HandlerResult Before(object message)
{
if (!IsValid(message))
return Result.Invalid("Validation failed"); // Short-circuit
return HandlerResult.Continue();
}
}Short-Circuit Usage
public class AuthorizationMiddleware
{
public HandlerResult Before(object message, IUserContext userContext)
{
if (!IsAuthorized(userContext, message))
{
// Short-circuit with forbidden result
return HandlerResult.ShortCircuit(Result.Forbidden("Access denied"));
}
// Continue to handler
return HandlerResult.Continue();
}
private bool IsAuthorized(IUserContext user, object message)
{
// Authorization logic
return true;
}
}State Passing Between Lifecycle Methods
Values returned from Before are automatically injected into After and Finally by type:
public class TransactionMiddleware
{
public IDbTransaction Before(object message, IDbContext context)
{
return context.BeginTransaction();
}
public async Task After(object message, IDbTransaction transaction)
{
await transaction.CommitAsync();
}
public async Task Finally(object message, IDbTransaction transaction, Exception? ex)
{
if (ex != null)
await transaction.RollbackAsync();
await transaction.DisposeAsync();
}
}Tuple State Returns
You can return multiple values using tuples:
public class ComplexMiddleware
{
public (Stopwatch timer, string correlationId) Before(object message)
{
return (Stopwatch.StartNew(), Guid.NewGuid().ToString());
}
public void Finally(object message, Stopwatch timer, string correlationId, Exception? ex)
{
timer.Stop();
_logger.LogInformation("Correlation {CorrelationId} completed in {Ms}ms",
correlationId, timer.ElapsedMilliseconds);
}
}Middleware Ordering
Use the [Middleware] attribute to control execution order:
[Middleware(10)]
public class ValidationMiddleware
{
// Runs early in Before, late in After/Finally
}
[Middleware(50)]
public class LoggingMiddleware
{
// Runs later in Before, earlier in After/Finally
}Default ordering (without explicit order):
- Message-specific middleware
- Interface-based middleware
- Object-based middleware
Execution flow:
Before: Lower order values run firstAfter/Finally: Higher order values run first (reverse order for proper nesting)
Message-Specific Middleware
Target specific message types or interfaces:
// Only runs for ICommand messages
public class CommandMiddleware
{
public void Before(ICommand command)
{
_commandLogger.Log($"Executing command: {command.GetType().Name}");
}
}
// Only runs for CreateOrder messages
public class OrderCreationMiddleware
{
public HandlerResult Before(CreateOrder command)
{
if (_orderService.IsDuplicate(command))
return HandlerResult.ShortCircuit(Result.Conflict("Duplicate order"));
return HandlerResult.Continue();
}
}Real-World Examples
Comprehensive Logging Middleware
Here's the logging middleware from the sample project:
public class LoggingMiddleware
{
private readonly ILogger<LoggingMiddleware> _logger;
public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
{
_logger = logger;
}
public static void Before(object message, ILogger<LoggingMiddleware> logger)
{
logger.LogInformation("Handling {MessageType}", message.GetType().Name);
}
}Caching Middleware
public class CachingMiddleware
{
private readonly IMemoryCache _cache;
public CachingMiddleware(IMemoryCache cache) => _cache = cache;
public HandlerResult Before(IQuery query)
{
var cacheKey = $"{query.GetType().Name}:{GetQueryKey(query)}";
if (_cache.TryGetValue(cacheKey, out var cachedResult))
{
return HandlerResult.ShortCircuit(cachedResult);
}
return HandlerResult.Continue();
}
public void After(IQuery query, object result)
{
var cacheKey = $"{query.GetType().Name}:{GetQueryKey(query)}";
_cache.Set(cacheKey, result, TimeSpan.FromMinutes(5));
}
}Async Middleware
All lifecycle methods support async versions:
public class AsyncMiddleware
{
public async Task<Stopwatch> BeforeAsync(object message, CancellationToken ct)
{
await SomeAsyncSetup(ct);
return Stopwatch.StartNew();
}
public async Task AfterAsync(object message, Stopwatch sw, CancellationToken ct)
{
await SomeAsyncCleanup(ct);
}
public async Task FinallyAsync(object message, Stopwatch sw, Exception? ex, CancellationToken ct)
{
sw.Stop();
await LogTiming(message, sw.ElapsedMilliseconds, ex, ct);
}
}Middleware Registration
Middleware are discovered automatically, but you can control their lifetime by registering them in DI:
// Singleton (default behavior)
services.AddSingleton<ValidationMiddleware>();
// Scoped (new instance per request)
services.AddScoped<DatabaseTransactionMiddleware>();
// Transient (new instance per use)
services.AddTransient<DisposableMiddleware>();Middleware Discovery
Middleware is automatically discovered by the Foundatio.Mediator source generator. To share middleware across projects:
- Create a middleware project with Foundatio.Mediator package referenced
- Reference that project from your handler projects
- Ensure the handler project also references Foundatio.Mediator
The source generator will discover middleware in referenced assemblies that have the Foundatio.Mediator source generator.
Discovery Rules
Middleware classes are found using:
- Naming Convention: Classes ending with
Middleware(e.g.,LoggingMiddleware,ValidationMiddleware) - Attribute: Classes marked with
[Middleware]attribute
Example: Cross-Assembly Middleware
Solution/
├── Common.Middleware/ # Shared middleware project
│ ├── Common.Middleware.csproj # References Foundatio.Mediator
│ └── LoggingMiddleware.cs # Discovered by convention
└── Orders.Handlers/ # Handler project
├── Orders.Handlers.csproj # References Common.Middleware AND Foundatio.Mediator
└── OrderHandler.cs # Uses LoggingMiddleware automaticallyCommon.Middleware.csproj:
<ItemGroup>
<PackageReference Include="Foundatio.Mediator" />
</ItemGroup>Orders.Handlers.csproj:
<ItemGroup>
<ProjectReference Include="..\Common.Middleware\Common.Middleware.csproj" />
<PackageReference Include="Foundatio.Mediator" />
</ItemGroup>Common.Middleware/LoggingMiddleware.cs:
namespace Common.Middleware;
// Discovered by naming convention (ends with Middleware)
public class LoggingMiddleware
{
public void Before(object message, ILogger logger)
{
logger.LogInformation("Handling {MessageType}", message.GetType().Name);
}
}The middleware will automatically be applied to all handlers in Orders.Handlers project.
💡 Complete Example: See the Modular Monolith Sample for a working demonstration of cross-assembly middleware in a multi-module application with shared middleware in
Common.Modulebeing used byProducts.ModuleandOrders.Module.
Setting Middleware Order
Control execution order using the [Middleware(Order = n)] attribute:
[Middleware(Order = 1)] // Runs first in Before, last in After/Finally
public class LoggingMiddleware { }
[Middleware(Order = 10)] // Runs later in Before, earlier in After/Finally
public class PerformanceMiddleware { }Execution flow:
Before: Lower order values run firstAfter/Finally: Higher order values run first (reverse order for proper nesting)
Ignoring Middleware
Use [FoundatioIgnore] to exclude middleware classes or methods:
[FoundatioIgnore] // Entire class ignored
public class DisabledMiddleware
{
public void Before(object message) { }
}
public class PartialMiddleware
{
public void Before(object message) { }
[FoundatioIgnore] // Only this method ignored
public void After(object message) { }
}Best Practices
1. Keep Middleware Focused
Each middleware should handle one concern:
// ✅ Good - single responsibility
public class ValidationMiddleware { }
public class LoggingMiddleware { }
public class AuthorizationMiddleware { }
// ❌ Avoid - multiple responsibilities
public class EverythingMiddleware { }2. Use Appropriate Lifecycle Methods
// ✅ Validation in Before (can short-circuit)
public HandlerResult Before(object message) => ValidateMessage(message);
// ✅ Logging in Finally (always runs)
public void Finally(object message, Exception? ex) => LogResult(message, ex);
// ❌ Don't validate in After (handler already ran)3. Handle Exceptions Gracefully
public void Finally(object message, Exception? exception)
{
if (exception != null)
{
// Log, notify, cleanup, etc.
_logger.LogError(exception, "Handler failed for {MessageType}", message.GetType().Name);
}
}4. Use Strongly-Typed Message Parameters
// ✅ Specific to commands
public void Before(ICommand command) { }
// ✅ Specific to queries
public void Before(IQuery query) { }
// ⚠️ Generic (runs for everything)
public void Before(object message) { }Next Steps
- Modular Monolith Sample - Complete working example of cross-assembly middleware
- Handler Conventions - Learn handler discovery rules