The CommandQuery.Framing framework now supports before and after pipelines for domain events using the abes.GenericPipeline library. This allows you to add cross-cutting concerns like logging, validation, authorization, and more around your domain event handlers.
- Middleware Pattern: Apply middleware before and after domain event execution
- Pipeline Composition: Chain multiple middleware in a specific order
- DI Integration: Middleware can use dependency injection
- Short-Circuit Support: Stop pipeline execution based on conditions
- Context Sharing: Share data between middleware via the context
The abes.GenericPipeline package is already included in CommandQuery.Framing.
Create middleware by implementing IPipelineMiddleware<DomainEventContext<TMessage>>:
using CommandQuery.Framing;
using GenericPipeline;
public class LoggingMiddleware<TMessage> : IPipelineMiddleware<DomainEventContext<TMessage>>
{
private readonly ILogger _logger;
public LoggingMiddleware(ILogger<LoggingMiddleware<TMessage>> logger)
{
_logger = logger;
}
public async ValueTask InvokeAsync(
DomainEventContext<TMessage> context,
PipelineDelegate<DomainEventContext<TMessage>> next)
{
_logger.LogInformation("Before: {MessageType}", typeof(TMessage).Name);
await next(context); // Call next middleware or handler
_logger.LogInformation("After: {MessageType}, Success: {Success}",
typeof(TMessage).Name,
context.Success);
}
}Configure the pipeline for specific message types:
public void ConfigureServices(IServiceCollection services)
{
services.AddCommandQuery(typeof(Startup).Assembly);
// Register middleware
services
.AddDomainEventMiddleware<LoggingMiddleware<WidgetCreated>>()
.AddDomainEventMiddleware<ValidationMiddleware<WidgetCreated>>();
// Configure pipeline for WidgetCreated events
services.AddDomainEventPipeline<WidgetCreated>(builder =>
{
builder.Use<ValidationMiddleware<WidgetCreated>>();
builder.Use<LoggingMiddleware<WidgetCreated>>();
});
}Domain events will now flow through the pipeline:
public class CreateWidget : IAsyncHandler<CreateWidgetMessage, CommandResponse<string>>
{
private readonly IDomainEventPublisher _publisher;
public CreateWidget(IDomainEventPublisher publisher)
{
_publisher = publisher;
}
public async Task<CommandResponse<string>> Execute(
CreateWidgetMessage message,
CancellationToken cancellationToken = default)
{
var widgetId = Guid.NewGuid().ToString();
// This will execute through the configured pipeline
await _publisher.Publish(
new WidgetCreated { Id = widgetId, Name = message.Name },
cancellationToken);
return Response.Ok(widgetId);
}
}The pipeline context provides:
public class DomainEventContext<TMessage> : PipelineContext
{
// The message being published
public TMessage Message { get; set; }
// Control flow - set to false to short-circuit
public bool ShouldContinue { get; set; } = true;
// Result information
public bool Success { get; set; } = true;
public string ErrorMessage { get; set; }
public Exception Exception { get; set; }
// From PipelineContext:
public CancellationToken CancellationToken { get; init; }
public IDictionary<string, object> Items { get; } // For sharing data
}public class ValidationMiddleware<TMessage> : IPipelineMiddleware<DomainEventContext<TMessage>>
{
public async ValueTask InvokeAsync(
DomainEventContext<TMessage> context,
PipelineDelegate<DomainEventContext<TMessage>> next)
{
if (context.Message == null)
{
context.Success = false;
context.ErrorMessage = "Message cannot be null";
context.ShouldContinue = false; // Stop the pipeline
return;
}
await next(context);
}
}public class AuthorizationMiddleware<TMessage> : IPipelineMiddleware<DomainEventContext<TMessage>>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AuthorizationMiddleware(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async ValueTask InvokeAsync(
DomainEventContext<TMessage> context,
PipelineDelegate<DomainEventContext<TMessage>> next)
{
var user = _httpContextAccessor.HttpContext?.User;
if (user == null || !user.Identity?.IsAuthenticated == true)
{
context.Success = false;
context.ErrorMessage = "Unauthorized";
context.ShouldContinue = false;
return;
}
await next(context);
}
}public class TimingMiddleware<TMessage> : IPipelineMiddleware<DomainEventContext<TMessage>>
{
private readonly ILogger _logger;
public TimingMiddleware(ILogger<TimingMiddleware<TMessage>> logger)
{
_logger = logger;
}
public async ValueTask InvokeAsync(
DomainEventContext<TMessage> context,
PipelineDelegate<DomainEventContext<TMessage>> next)
{
var sw = Stopwatch.StartNew();
try
{
await next(context);
}
finally
{
sw.Stop();
_logger.LogInformation(
"Domain event {MessageType} processed in {ElapsedMs}ms",
typeof(TMessage).Name,
sw.ElapsedMilliseconds);
}
}
}You can control middleware execution order:
// Using IOrderedMiddleware for coarse ordering
public class EarlyMiddleware : IPipelineMiddleware<MyContext>, IOrderedMiddleware
{
public int Order => -10; // Lower runs first
public async ValueTask InvokeAsync(MyContext context, PipelineDelegate<MyContext> next)
{
await next(context);
}
}
// Using IRunBefore<T> constraint
public class ValidationMiddleware :
IPipelineMiddleware<MyContext>,
IRunBefore<LoggingMiddleware> // Runs before logging
{
public async ValueTask InvokeAsync(MyContext context, PipelineDelegate<MyContext> next)
{
await next(context);
}
}
// Using IRunAfter<T> constraint
public class CleanupMiddleware :
IPipelineMiddleware<MyContext>,
IRunAfter<LoggingMiddleware> // Runs after logging
{
public async ValueTask InvokeAsync(MyContext context, PipelineDelegate<MyContext> next)
{
await next(context);
}
}Stop the pipeline early by setting ShouldContinue = false:
public async ValueTask InvokeAsync(
DomainEventContext<TMessage> context,
PipelineDelegate<TMessage> next)
{
if (SomeCondition())
{
context.ShouldContinue = false;
context.ErrorMessage = "Condition not met";
return; // Don't call next()
}
await next(context);
}✅ Separation of Concerns: Keep cross-cutting logic separate from business logic
✅ Reusability: Write middleware once, use across multiple event types
✅ Testability: Test middleware independently
✅ Flexibility: Enable/disable middleware via configuration
✅ Performance Monitoring: Add timing and metrics easily
✅ Error Handling: Centralized exception handling