Pre and Post Handlers - arcanic-kit/Mediator GitHub Wiki
Pre and Post Handlers
Pre and Post Handlers in Arcanic Mediator provide a powerful way to implement cross-cutting concerns and additional processing logic that executes before and after your main handlers across Commands, Queries, and Events.
- Overview
- Execution Flow
- Pre-Handlers
- Post-Handlers
- Implementation by Pattern
- Common Use Cases
- Registration and Configuration
- Best Practices
- Testing
Pre and Post Handlers allow you to implement the Decorator Pattern around your main handlers, providing a clean way to separate concerns and add functionality without modifying existing handler logic.
- Separation of Concerns - Keep cross-cutting logic separate from business logic
- Reusability - Share common functionality across multiple handlers
- Testability - Test cross-cutting concerns independently
- Maintainability - Easy to add, remove, or modify without touching main handlers
- Single Responsibility - Each handler focuses on one specific aspect
Request → Pre-Handlers → Main Handler → Post-Handlers → Response
The execution flow varies slightly between Commands, Queries, and Events:
Command → Command Pre-Handlers → Command Handler → Command Post-Handlers → Result
Query → Query Pre-Handlers → Query Handler → Query Post-Handlers → Response Data
Event → Event Pre-Handlers → Event Handlers (Multiple) → Event Post-Handlers
Pre-handlers execute before the main handler and are commonly used for:
- Validation - Parameter validation, business rule validation
- Authorization - Security checks, permission validation
- Enrichment - Adding context data, populating missing information
- Transformation - Modifying request data before processing
- Logging - Request logging, audit trails
- Rate Limiting - Throttling, quota management
using Arcanic.Mediator.Command.Abstractions.Handler;
// Validation pre-handler
public class CreateOrderCommandValidationPreHandler : ICommandPreHandler<CreateOrderCommand>
{
private readonly IValidator<CreateOrderCommand> _validator;
public CreateOrderCommandValidationPreHandler(IValidator<CreateOrderCommand> validator)
{
_validator = validator;
}
public async Task HandleAsync(CreateOrderCommand request, CancellationToken cancellationToken = default)
{
var validationResult = await _validator.ValidateAsync(request, cancellationToken);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
}
}
// Authorization pre-handler
public class CreateOrderCommandAuthorizationPreHandler : ICommandPreHandler<CreateOrderCommand>
{
private readonly ICurrentUser _currentUser;
private readonly ICustomerService _customerService;
public CreateOrderCommandAuthorizationPreHandler(ICurrentUser currentUser, ICustomerService customerService)
{
_currentUser = currentUser;
_customerService = customerService;
}
public async Task HandleAsync(CreateOrderCommand request, CancellationToken cancellationToken = default)
{
// Check if user can create orders for this customer
if (request.CustomerId != _currentUser.CustomerId)
{
var canCreateForOthers = await _customerService.CanUserCreateOrdersForCustomerAsync(
_currentUser.UserId, request.CustomerId, cancellationToken);
if (!canCreateForOthers)
{
throw new UnauthorizedAccessException("You cannot create orders for this customer");
}
}
}
}
// Enrichment pre-handler
public class CreateOrderCommandEnrichmentPreHandler : ICommandPreHandler<CreateOrderCommand>
{
private readonly IPricingService _pricingService;
private readonly IPromotionService _promotionService;
public CreateOrderCommandEnrichmentPreHandler(IPricingService pricingService, IPromotionService promotionService)
{
_pricingService = pricingService;
_promotionService = promotionService;
}
public async Task HandleAsync(CreateOrderCommand request, CancellationToken cancellationToken = default)
{
// Calculate current pricing
await _pricingService.UpdateItemPricingAsync(request.Items, cancellationToken);
// Apply available promotions
var promotions = await _promotionService.GetApplicablePromotionsAsync(
request.CustomerId, request.Items, cancellationToken);
request.AppliedPromotions = promotions;
}
}using Arcanic.Mediator.Query.Abstractions.Handler;
// Authorization pre-handler
public class GetCustomerOrdersQueryAuthorizationPreHandler : IQueryPreHandler<GetCustomerOrdersQuery>
{
private readonly ICurrentUser _currentUser;
private readonly IAuthorizationService _authorizationService;
public GetCustomerOrdersQueryAuthorizationPreHandler(ICurrentUser currentUser, IAuthorizationService authorizationService)
{
_currentUser = currentUser;
_authorizationService = authorizationService;
}
public async Task HandleAsync(GetCustomerOrdersQuery request, CancellationToken cancellationToken = default)
{
if (request.CustomerId != _currentUser.CustomerId)
{
var authResult = await _authorizationService.AuthorizeAsync(
_currentUser.User, "CanViewCustomerOrders");
if (!authResult.Succeeded)
{
throw new UnauthorizedAccessException("You can only view your own orders");
}
}
}
}
// Parameter validation pre-handler
public class GetOrdersPagedQueryValidationPreHandler : IQueryPreHandler<GetOrdersPagedQuery>
{
public async Task HandleAsync(GetOrdersPagedQuery request, CancellationToken cancellationToken = default)
{
if (request.Page < 1)
throw new ArgumentException("Page must be greater than 0", nameof(request.Page));
if (request.PageSize < 1 || request.PageSize > 100)
throw new ArgumentException("Page size must be between 1 and 100", nameof(request.PageSize));
if (request.StartDate.HasValue && request.EndDate.HasValue && request.StartDate > request.EndDate)
throw new ArgumentException("Start date cannot be later than end date");
await Task.CompletedTask;
}
}
// Query optimization pre-handler
public class SearchProductsQueryOptimizationPreHandler : IQueryPreHandler<SearchProductsQuery>
{
private readonly ISearchQueryOptimizer _optimizer;
public SearchProductsQueryOptimizationPreHandler(ISearchQueryOptimizer optimizer)
{
_optimizer = optimizer;
}
public async Task HandleAsync(SearchProductsQuery request, CancellationToken cancellationToken = default)
{
// Optimize search terms
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
{
request.SearchTerm = await _optimizer.OptimizeSearchTermAsync(request.SearchTerm, cancellationToken);
}
// Set reasonable defaults for price range
if (request.MinPrice.HasValue && request.MaxPrice.HasValue && request.MinPrice > request.MaxPrice)
{
(request.MinPrice, request.MaxPrice) = (request.MaxPrice, request.MinPrice);
}
}
}using Arcanic.Mediator.Event.Abstractions.Handler;
// Event validation pre-handler
public class OrderPlacedEventValidationPreHandler : IEventPreHandler<OrderPlacedEvent>
{
public async Task HandleAsync(OrderPlacedEvent request, CancellationToken cancellationToken = default)
{
if (request.OrderId <= 0)
throw new ArgumentException("OrderId must be greater than 0", nameof(request.OrderId));
if (request.CustomerId <= 0)
throw new ArgumentException("CustomerId must be greater than 0", nameof(request.CustomerId));
if (request.TotalAmount <= 0)
throw new ArgumentException("TotalAmount must be greater than 0", nameof(request.TotalAmount));
await Task.CompletedTask;
}
}
// Event enrichment pre-handler
public class UserCreatedEventEnrichmentPreHandler : IEventPreHandler<UserCreatedEvent>
{
private readonly IGeoLocationService _geoLocationService;
private readonly IHttpContextAccessor _httpContextAccessor;
public UserCreatedEventEnrichmentPreHandler(IGeoLocationService geoLocationService, IHttpContextAccessor httpContextAccessor)
{
_geoLocationService = geoLocationService;
_httpContextAccessor = httpContextAccessor;
}
public async Task HandleAsync(UserCreatedEvent request, CancellationToken cancellationToken = default)
{
// Enrich with location data if available
var ipAddress = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();
if (!string.IsNullOrEmpty(ipAddress))
{
var location = await _geoLocationService.GetLocationAsync(ipAddress, cancellationToken);
request.RegistrationLocation = location;
}
// Add additional context
request.UserAgent = _httpContextAccessor.HttpContext?.Request.Headers.UserAgent.ToString();
}
}Post-handlers execute after the main handler and are commonly used for:
- Logging - Operation logging, performance metrics
- Auditing - Audit trail creation, compliance logging
- Cleanup - Resource cleanup, temporary data removal
- Notifications - Sending notifications, triggering workflows
- Analytics - Usage tracking, business intelligence
- Response Transformation - Modifying response data
using Arcanic.Mediator.Command.Abstractions.Handler;
// Audit logging post-handler
public class CreateOrderCommandAuditPostHandler : ICommandPostHandler<CreateOrderCommand>
{
private readonly IAuditService _auditService;
private readonly ICurrentUser _currentUser;
public CreateOrderCommandAuditPostHandler(IAuditService auditService, ICurrentUser currentUser)
{
_auditService = auditService;
_currentUser = currentUser;
}
public async Task HandleAsync(CreateOrderCommand request, CancellationToken cancellationToken = default)
{
await _auditService.LogCommandAsync(new CommandAuditEntry
{
CommandType = nameof(CreateOrderCommand),
UserId = _currentUser.UserId,
Timestamp = DateTime.UtcNow,
Details = new
{
CustomerId = request.CustomerId,
ItemCount = request.Items?.Count ?? 0,
TotalAmount = request.Items?.Sum(x => x.Quantity * x.UnitPrice) ?? 0
}
}, cancellationToken);
}
}
// Performance monitoring post-handler
public class CreateOrderCommandPerformancePostHandler : ICommandPostHandler<CreateOrderCommand>
{
private readonly IMetricsCollector _metricsCollector;
public CreateOrderCommandPerformancePostHandler(IMetricsCollector metricsCollector)
{
_metricsCollector = metricsCollector;
}
public async Task HandleAsync(CreateOrderCommand request, CancellationToken cancellationToken = default)
{
_metricsCollector.IncrementCommandExecuted(nameof(CreateOrderCommand));
_metricsCollector.RecordOrderMetrics(request.Items?.Count ?? 0, request.CustomerId);
await Task.CompletedTask;
}
}
// Cleanup post-handler
public class CreateOrderCommandCleanupPostHandler : ICommandPostHandler<CreateOrderCommand>
{
private readonly ITemporaryDataService _tempDataService;
public CreateOrderCommandCleanupPostHandler(ITemporaryDataService tempDataService)
{
_tempDataService = tempDataService;
}
public async Task HandleAsync(CreateOrderCommand request, CancellationToken cancellationToken = default)
{
// Clean up any temporary data created during order processing
if (!string.IsNullOrEmpty(request.SessionId))
{
await _tempDataService.CleanupSessionDataAsync(request.SessionId, cancellationToken);
}
}
}using Arcanic.Mediator.Query.Abstractions.Handler;
// Analytics tracking post-handler
public class SearchProductsQueryAnalyticsPostHandler : IQueryPostHandler<SearchProductsQuery>
{
private readonly IAnalyticsService _analyticsService;
private readonly ICurrentUser _currentUser;
public SearchProductsQueryAnalyticsPostHandler(IAnalyticsService analyticsService, ICurrentUser currentUser)
{
_analyticsService = analyticsService;
_currentUser = currentUser;
}
public async Task HandleAsync(SearchProductsQuery request, CancellationToken cancellationToken = default)
{
await _analyticsService.TrackSearchAsync(new SearchEvent
{
UserId = _currentUser.UserId,
SearchTerm = request.SearchTerm,
CategoryId = request.CategoryId,
Filters = new SearchFilters
{
MinPrice = request.MinPrice,
MaxPrice = request.MaxPrice,
InStock = request.InStock
},
Timestamp = DateTime.UtcNow
}, cancellationToken);
}
}
// Query performance logging post-handler
public class QueryPerformanceLoggingPostHandler<TQuery> : IQueryPostHandler<TQuery> where TQuery : class
{
private readonly ILogger<QueryPerformanceLoggingPostHandler<TQuery>> _logger;
private readonly IPerformanceContext _performanceContext;
public QueryPerformanceLoggingPostHandler(ILogger<QueryPerformanceLoggingPostHandler<TQuery>> logger, IPerformanceContext performanceContext)
{
_logger = logger;
_performanceContext = performanceContext;
}
public async Task HandleAsync(TQuery request, CancellationToken cancellationToken = default)
{
var queryName = typeof(TQuery).Name;
var executionTime = _performanceContext.ElapsedMilliseconds;
if (executionTime > 1000) // Log slow queries
{
_logger.LogWarning("Slow query detected: {QueryName} took {ElapsedMs}ms", queryName, executionTime);
}
else
{
_logger.LogDebug("Query {QueryName} completed in {ElapsedMs}ms", queryName, executionTime);
}
await Task.CompletedTask;
}
}using Arcanic.Mediator.Event.Abstractions.Handler;
// Event audit post-handler
public class EventAuditPostHandler<TEvent> : IEventPostHandler<TEvent> where TEvent : IEvent
{
private readonly IAuditService _auditService;
public EventAuditPostHandler(IAuditService auditService)
{
_auditService = auditService;
}
public async Task HandleAsync(TEvent request, CancellationToken cancellationToken = default)
{
await _auditService.LogEventAsync(new EventAuditEntry
{
EventType = typeof(TEvent).Name,
Timestamp = DateTime.UtcNow,
EventData = System.Text.Json.JsonSerializer.Serialize(request),
ProcessedSuccessfully = true
}, cancellationToken);
}
}
// Metrics collection post-handler
public class EventMetricsPostHandler<TEvent> : IEventPostHandler<TEvent> where TEvent : IEvent
{
private readonly IMetricsCollector _metricsCollector;
public EventMetricsPostHandler(IMetricsCollector metricsCollector)
{
_metricsCollector = metricsCollector;
}
public async Task HandleAsync(TEvent request, CancellationToken cancellationToken = default)
{
_metricsCollector.IncrementEventProcessed(typeof(TEvent).Name);
await Task.CompletedTask;
}
}
// Notification post-handler for critical events
public class CriticalEventNotificationPostHandler<TEvent> : IEventPostHandler<TEvent>
where TEvent : IEvent, ICriticalEvent
{
private readonly INotificationService _notificationService;
public CriticalEventNotificationPostHandler(INotificationService notificationService)
{
_notificationService = notificationService;
}
public async Task HandleAsync(TEvent request, CancellationToken cancellationToken = default)
{
await _notificationService.SendCriticalAlertAsync(new CriticalAlert
{
EventType = typeof(TEvent).Name,
Severity = request.Severity,
Message = request.GetAlertMessage(),
EventData = request,
Timestamp = DateTime.UtcNow
}, cancellationToken);
}
}
// Interface for critical events
public interface ICriticalEvent
{
AlertSeverity Severity { get; }
string GetAlertMessage();
}// Command with validation, authorization, and audit
public class UpdateUserProfileCommand : ICommand<UserProfileDto>
{
public int UserId { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string PhoneNumber { get; set; } = string.Empty;
}
// Pre-handler for authorization
public class UpdateUserProfileCommandAuthorizationPreHandler : ICommandPreHandler<UpdateUserProfileCommand>
{
private readonly ICurrentUser _currentUser;
public UpdateUserProfileCommandAuthorizationPreHandler(ICurrentUser currentUser)
{
_currentUser = currentUser;
}
public async Task HandleAsync(UpdateUserProfileCommand request, CancellationToken cancellationToken = default)
{
if (request.UserId != _currentUser.UserId && !_currentUser.IsAdmin)
{
throw new UnauthorizedAccessException("You can only update your own profile");
}
await Task.CompletedTask;
}
}
// Main handler
public class UpdateUserProfileCommandHandler : ICommandHandler<UpdateUserProfileCommand, UserProfileDto>
{
private readonly IUserRepository _userRepository;
private readonly IMapper _mapper;
public UpdateUserProfileCommandHandler(IUserRepository userRepository, IMapper mapper)
{
_userRepository = userRepository;
_mapper = mapper;
}
public async Task<UserProfileDto> HandleAsync(UpdateUserProfileCommand request, CancellationToken cancellationToken = default)
{
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null)
throw new NotFoundException($"User with ID {request.UserId} not found");
user.FirstName = request.FirstName;
user.LastName = request.LastName;
user.Email = request.Email;
user.PhoneNumber = request.PhoneNumber;
user.UpdatedAt = DateTime.UtcNow;
await _userRepository.UpdateAsync(user, cancellationToken);
await _userRepository.SaveChangesAsync(cancellationToken);
return _mapper.Map<UserProfileDto>(user);
}
}
// Post-handler for audit
public class UpdateUserProfileCommandAuditPostHandler : ICommandPostHandler<UpdateUserProfileCommand>
{
private readonly IAuditService _auditService;
private readonly ICurrentUser _currentUser;
public UpdateUserProfileCommandAuditPostHandler(IAuditService auditService, ICurrentUser currentUser)
{
_auditService = auditService;
_currentUser = currentUser;
}
public async Task HandleAsync(UpdateUserProfileCommand request, CancellationToken cancellationToken = default)
{
await _auditService.LogUserProfileUpdateAsync(new UserProfileUpdateAudit
{
UpdatedUserId = request.UserId,
UpdatedByUserId = _currentUser.UserId,
Changes = new
{
FirstName = request.FirstName,
LastName = request.LastName,
Email = request.Email,
PhoneNumber = request.PhoneNumber
},
Timestamp = DateTime.UtcNow
}, cancellationToken);
}
}// Query with authorization and analytics
public class GetUserOrdersQuery : IQuery<PagedResult<OrderDto>>
{
public int UserId { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 25;
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
}
// Pre-handler for authorization
public class GetUserOrdersQueryAuthorizationPreHandler : IQueryPreHandler<GetUserOrdersQuery>
{
private readonly ICurrentUser _currentUser;
public GetUserOrdersQueryAuthorizationPreHandler(ICurrentUser currentUser)
{
_currentUser = currentUser;
}
public async Task HandleAsync(GetUserOrdersQuery request, CancellationToken cancellationToken = default)
{
if (request.UserId != _currentUser.UserId && !_currentUser.HasPermission("ViewAllOrders"))
{
throw new UnauthorizedAccessException("You can only view your own orders");
}
await Task.CompletedTask;
}
}
// Main handler
public class GetUserOrdersQueryHandler : IQueryHandler<GetUserOrdersQuery, PagedResult<OrderDto>>
{
private readonly IOrderRepository _orderRepository;
private readonly IMapper _mapper;
public GetUserOrdersQueryHandler(IOrderRepository orderRepository, IMapper mapper)
{
_orderRepository = orderRepository;
_mapper = mapper;
}
public async Task<PagedResult<OrderDto>> HandleAsync(GetUserOrdersQuery request, CancellationToken cancellationToken = default)
{
var (orders, totalCount) = await _orderRepository.GetUserOrdersPagedAsync(
request.UserId,
request.Page,
request.PageSize,
request.StartDate,
request.EndDate,
cancellationToken);
var orderDtos = _mapper.Map<IEnumerable<OrderDto>>(orders);
return new PagedResult<OrderDto>
{
Items = orderDtos,
TotalCount = totalCount,
Page = request.Page,
PageSize = request.PageSize
};
}
}
// Post-handler for analytics
public class GetUserOrdersQueryAnalyticsPostHandler : IQueryPostHandler<GetUserOrdersQuery>
{
private readonly IAnalyticsService _analyticsService;
public GetUserOrdersQueryAnalyticsPostHandler(IAnalyticsService analyticsService)
{
_analyticsService = analyticsService;
}
public async Task HandleAsync(GetUserOrdersQuery request, CancellationToken cancellationToken = default)
{
await _analyticsService.TrackOrdersViewedAsync(new OrdersViewedEvent
{
UserId = request.UserId,
PageViewed = request.Page,
DateRange = new { request.StartDate, request.EndDate },
Timestamp = DateTime.UtcNow
}, cancellationToken);
}
}// Event with validation and enrichment
public class OrderCompletedEvent : IEvent
{
public int OrderId { get; set; }
public int CustomerId { get; set; }
public decimal TotalAmount { get; set; }
public DateTime CompletedAt { get; set; } = DateTime.UtcNow;
public string? TrackingNumber { get; set; }
public string? CustomerEmail { get; set; } // Will be enriched
public string? CustomerName { get; set; } // Will be enriched
}
// Pre-handler for validation and enrichment
public class OrderCompletedEventEnrichmentPreHandler : IEventPreHandler<OrderCompletedEvent>
{
private readonly ICustomerRepository _customerRepository;
public OrderCompletedEventEnrichmentPreHandler(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}
public async Task HandleAsync(OrderCompletedEvent request, CancellationToken cancellationToken = default)
{
// Validation
if (request.OrderId <= 0)
throw new ArgumentException("OrderId must be greater than 0");
if (request.TotalAmount <= 0)
throw new ArgumentException("TotalAmount must be greater than 0");
// Enrichment
var customer = await _customerRepository.GetByIdAsync(request.CustomerId, cancellationToken);
if (customer != null)
{
request.CustomerEmail = customer.Email;
request.CustomerName = $"{customer.FirstName} {customer.LastName}";
}
}
}
// Multiple event handlers
public class OrderCompletedEmailHandler : IEventHandler<OrderCompletedEvent>
{
private readonly IEmailService _emailService;
public OrderCompletedEmailHandler(IEmailService emailService)
{
_emailService = emailService;
}
public async Task HandleAsync(OrderCompletedEvent request, CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(request.CustomerEmail))
{
await _emailService.SendOrderCompletedEmailAsync(new OrderCompletedEmail
{
ToEmail = request.CustomerEmail,
CustomerName = request.CustomerName ?? "Customer",
OrderId = request.OrderId,
TotalAmount = request.TotalAmount,
TrackingNumber = request.TrackingNumber
}, cancellationToken);
}
}
}
public class OrderCompletedAnalyticsHandler : IEventHandler<OrderCompletedEvent>
{
private readonly IAnalyticsService _analyticsService;
public OrderCompletedAnalyticsHandler(IAnalyticsService analyticsService)
{
_analyticsService = analyticsService;
}
public async Task HandleAsync(OrderCompletedEvent request, CancellationToken cancellationToken = default)
{
await _analyticsService.TrackOrderCompletedAsync(new OrderCompletedAnalytics
{
OrderId = request.OrderId,
CustomerId = request.CustomerId,
TotalAmount = request.TotalAmount,
CompletedAt = request.CompletedAt
}, cancellationToken);
}
}
// Post-handler for audit
public class OrderCompletedEventAuditPostHandler : IEventPostHandler<OrderCompletedEvent>
{
private readonly IAuditService _auditService;
public OrderCompletedEventAuditPostHandler(IAuditService auditService)
{
_auditService = auditService;
}
public async Task HandleAsync(OrderCompletedEvent request, CancellationToken cancellationToken = default)
{
await _auditService.LogOrderCompletedAsync(new OrderCompletedAudit
{
OrderId = request.OrderId,
CustomerId = request.CustomerId,
CompletedAt = request.CompletedAt,
TotalAmount = request.TotalAmount,
AuditedAt = DateTime.UtcNow
}, cancellationToken);
}
}public class ValidationPreHandler<T> : ICommandPreHandler<T>, IQueryPreHandler<T>, IEventPreHandler<T>
where T : class
{
private readonly IValidator<T> _validator;
public ValidationPreHandler(IValidator<T> validator)
{
_validator = validator;
}
public async Task HandleAsync(T request, CancellationToken cancellationToken = default)
{
var result = await _validator.ValidateAsync(request, cancellationToken);
if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
}
}public class AuthorizationPreHandler<T> : ICommandPreHandler<T>, IQueryPreHandler<T>
where T : class, ISecuredRequest
{
private readonly ICurrentUser _currentUser;
private readonly IAuthorizationService _authorizationService;
public AuthorizationPreHandler(ICurrentUser currentUser, IAuthorizationService authorizationService)
{
_currentUser = currentUser;
_authorizationService = authorizationService;
}
public async Task HandleAsync(T request, CancellationToken cancellationToken = default)
{
var authResult = await _authorizationService.AuthorizeAsync(
_currentUser.User, request, request.RequiredPermission);
if (!authResult.Succeeded)
{
throw new UnauthorizedAccessException($"Insufficient permissions for {typeof(T).Name}");
}
}
}
public interface ISecuredRequest
{
string RequiredPermission { get; }
}public class AuditLoggingPostHandler<T> : ICommandPostHandler<T>, IEventPostHandler<T>
where T : class
{
private readonly IAuditService _auditService;
private readonly ICurrentUser _currentUser;
public AuditLoggingPostHandler(IAuditService auditService, ICurrentUser currentUser)
{
_auditService = auditService;
_currentUser = currentUser;
}
public async Task HandleAsync(T request, CancellationToken cancellationToken = default)
{
await _auditService.LogOperationAsync(new AuditEntry
{
OperationType = typeof(T).Name,
UserId = _currentUser.UserId,
Timestamp = DateTime.UtcNow,
Data = System.Text.Json.JsonSerializer.Serialize(request)
}, cancellationToken);
}
}public class PerformanceMonitoringPostHandler<T> : ICommandPostHandler<T>, IQueryPostHandler<T>
where T : class
{
private readonly IMetricsCollector _metricsCollector;
private readonly ILogger<PerformanceMonitoringPostHandler<T>> _logger;
private readonly IPerformanceContext _performanceContext;
public PerformanceMonitoringPostHandler(
IMetricsCollector metricsCollector,
ILogger<PerformanceMonitoringPostHandler<T>> logger,
IPerformanceContext performanceContext)
{
_metricsCollector = metricsCollector;
_logger = logger;
_performanceContext = performanceContext;
}
public async Task HandleAsync(T request, CancellationToken cancellationToken = default)
{
var operationName = typeof(T).Name;
var executionTime = _performanceContext.ElapsedMilliseconds;
_metricsCollector.RecordExecutionTime(operationName, executionTime);
if (executionTime > 2000) // Log operations taking longer than 2 seconds
{
_logger.LogWarning("Slow operation detected: {OperationName} took {ElapsedMs}ms",
operationName, executionTime);
}
await Task.CompletedTask;
}
}public class UserContextEnrichmentPreHandler<T> : ICommandPreHandler<T>, IQueryPreHandler<T>
where T : class, IUserContextAware
{
private readonly ICurrentUser _currentUser;
private readonly IUserService _userService;
public UserContextEnrichmentPreHandler(ICurrentUser currentUser, IUserService userService)
{
_currentUser = currentUser;
_userService = userService;
}
public async Task HandleAsync(T request, CancellationToken cancellationToken = default)
{
var userProfile = await _userService.GetUserProfileAsync(_currentUser.UserId, cancellationToken);
request.UserContext = new UserContext
{
UserId = _currentUser.UserId,
UserName = _currentUser.UserName,
Email = _currentUser.Email,
Roles = _currentUser.Roles,
TimeZone = userProfile.TimeZone,
Culture = userProfile.Culture
};
}
}
public interface IUserContextAware
{
UserContext UserContext { get; set; }
}public class ResponseTransformationPostHandler<T> : IQueryPostHandler<T>
where T : class, ITransformableResponse
{
private readonly ICurrentUser _currentUser;
private readonly IResponseTransformer _transformer;
public ResponseTransformationPostHandler(ICurrentUser currentUser, IResponseTransformer transformer)
{
_currentUser = currentUser;
_transformer = transformer;
}
public async Task HandleAsync(T request, CancellationToken cancellationToken = default)
{
await _transformer.TransformForUserAsync(request, _currentUser, cancellationToken);
}
}
public interface ITransformableResponse
{
object ResponseData { get; set; }
}var builder = WebApplication.CreateBuilder(args);
builder.Services.AddArcanicMediator()
.AddCommands(Assembly.GetExecutingAssembly())
.AddQueries(Assembly.GetExecutingAssembly())
.AddEvents(Assembly.GetExecutingAssembly());
var app = builder.Build();var builder = WebApplication.CreateBuilder(args);
// Register validation services
builder.Services.AddFluentValidation(config =>
{
config.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
});
// Register pre/post handlers
builder.Services.AddScoped(typeof(ValidationPreHandler<>));
builder.Services.AddScoped(typeof(AuthorizationPreHandler<>));
builder.Services.AddScoped(typeof(AuditLoggingPostHandler<>));
builder.Services.AddScoped(typeof(PerformanceMonitoringPostHandler<>));
// Register specific handlers
builder.Services.AddScoped<CreateOrderCommandValidationPreHandler>();
builder.Services.AddScoped<CreateOrderCommandAuthorizationPreHandler>();
builder.Services.AddScoped<CreateOrderCommandAuditPostHandler>();
builder.Services.AddArcanicMediator()
.AddCommands(Assembly.GetExecutingAssembly())
.AddQueries(Assembly.GetExecutingAssembly())
.AddEvents(Assembly.GetExecutingAssembly());
var app = builder.Build();var builder = WebApplication.CreateBuilder(args);
// Add audit logging only in production
if (builder.Environment.IsProduction())
{
builder.Services.AddScoped(typeof(AuditLoggingPostHandler<>));
}
// Add performance monitoring in non-development environments
if (!builder.Environment.IsDevelopment())
{
builder.Services.AddScoped(typeof(PerformanceMonitoringPostHandler<>));
builder.Services.AddSingleton<IMetricsCollector, MetricsCollector>();
}
// Always add validation
builder.Services.AddScoped(typeof(ValidationPreHandler<>));
builder.Services.AddArcanicMediator()
.AddCommands(Assembly.GetExecutingAssembly())
.AddQueries(Assembly.GetExecutingAssembly())
.AddEvents(Assembly.GetExecutingAssembly());// ✅ Good - Single responsibility
public class OrderValidationPreHandler : ICommandPreHandler<CreateOrderCommand>
{
// Only validates order data
}
public class OrderAuthorizationPreHandler : ICommandPreHandler<CreateOrderCommand>
{
// Only checks authorization
}
// ❌ Bad - Multiple responsibilities
public class OrderProcessingPreHandler : ICommandPreHandler<CreateOrderCommand>
{
public async Task HandleAsync(CreateOrderCommand request, CancellationToken cancellationToken = default)
{
// Validation
if (request.Items.Count == 0) throw new ValidationException("No items");
// Authorization
if (!_currentUser.CanCreateOrders) throw new UnauthorizedException("Cannot create orders");
// Pricing calculation
foreach (var item in request.Items) { /* pricing logic */ }
// Inventory check
await _inventoryService.CheckAvailabilityAsync(request.Items, cancellationToken);
}
}// ✅ Good - Reusable validation handler
public class FluentValidationPreHandler<T> : ICommandPreHandler<T>, IQueryPreHandler<T>
where T : class
{
private readonly IValidator<T>? _validator;
public FluentValidationPreHandler(IValidator<T>? validator = null)
{
_validator = validator;
}
public async Task HandleAsync(T request, CancellationToken cancellationToken = default)
{
if (_validator != null)
{
var result = await _validator.ValidateAsync(request, cancellationToken);
if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
}
}
}// ✅ Good - Graceful failure handling
public class EmailNotificationPostHandler : IEventPostHandler<OrderPlacedEvent>
{
public async Task HandleAsync(OrderPlacedEvent request, CancellationToken cancellationToken = default)
{
try
{
await _emailService.SendOrderConfirmationAsync(request.CustomerEmail, cancellationToken);
}
catch (EmailServiceException ex)
{
// Log but don't throw - email failure shouldn't break order processing
_logger.LogError(ex, "Failed to send order confirmation email for order {OrderId}", request.OrderId);
// Optionally queue for retry
await _retryQueue.EnqueueEmailAsync(request, cancellationToken);
}
}
}// ✅ Good - Proper dependency injection
public class UserValidationPreHandler : ICommandPreHandler<UpdateUserCommand>
{
private readonly IUserRepository _userRepository;
private readonly IValidator<UpdateUserCommand> _validator;
public UserValidationPreHandler(IUserRepository userRepository, IValidator<UpdateUserCommand> validator)
{
_userRepository = userRepository;
_validator = validator;
}
public async Task HandleAsync(UpdateUserCommand request, CancellationToken cancellationToken = default)
{
// Use injected dependencies
await _validator.ValidateAndThrowAsync(request, cancellationToken);
var existingUser = await _userRepository.GetByEmailAsync(request.Email, cancellationToken);
if (existingUser != null && existingUser.Id != request.Id)
{
throw new BusinessValidationException("Email already in use");
}
}
}/// <summary>
/// Validates that the user has permission to create orders for the specified customer.
/// Checks both direct customer ownership and delegated permissions.
/// </summary>
public class CreateOrderAuthorizationPreHandler : ICommandPreHandler<CreateOrderCommand>
{
/// <summary>
/// Verifies user authorization to create orders for the specified customer.
/// Throws UnauthorizedAccessException if user lacks permission.
/// </summary>
public async Task HandleAsync(CreateOrderCommand request, CancellationToken cancellationToken = default)
{
// Implementation
}
}public class CreateOrderValidationPreHandlerTests
{
private readonly Mock<IValidator<CreateOrderCommand>> _mockValidator;
private readonly CreateOrderValidationPreHandler _handler;
public CreateOrderValidationPreHandlerTests()
{
_mockValidator = new Mock<IValidator<CreateOrderCommand>>();
_handler = new CreateOrderValidationPreHandler(_mockValidator.Object);
}
[Fact]
public async Task HandleAsync_ValidCommand_DoesNotThrow()
{
// Arrange
var command = new CreateOrderCommand { CustomerId = 1, Items = [new OrderItem { ProductId = 1, Quantity = 1 }] };
var validationResult = new ValidationResult();
_mockValidator.Setup(x => x.ValidateAsync(command, It.IsAny<CancellationToken>()))
.ReturnsAsync(validationResult);
// Act & Assert
await _handler.HandleAsync(command, CancellationToken.None);
_mockValidator.Verify(x => x.ValidateAsync(command, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task HandleAsync_InvalidCommand_ThrowsValidationException()
{
// Arrange
var command = new CreateOrderCommand();
var validationResult = new ValidationResult(new[]
{
new ValidationFailure("CustomerId", "Customer ID is required")
});
_mockValidator.Setup(x => x.ValidateAsync(command, It.IsAny<CancellationToken>()))
.ReturnsAsync(validationResult);
// Act & Assert
await Assert.ThrowsAsync<ValidationException>(() =>
_handler.HandleAsync(command, CancellationToken.None));
}
}public class CreateOrderAuditPostHandlerTests
{
private readonly Mock<IAuditService> _mockAuditService;
private readonly Mock<ICurrentUser> _mockCurrentUser;
private readonly CreateOrderAuditPostHandler _handler;
public CreateOrderAuditPostHandlerTests()
{
_mockAuditService = new Mock<IAuditService>();
_mockCurrentUser = new Mock<ICurrentUser>();
_handler = new CreateOrderAuditPostHandler(_mockAuditService.Object, _mockCurrentUser.Object);
}
[Fact]
public async Task HandleAsync_ValidCommand_LogsAuditEntry()
{
// Arrange
var command = new CreateOrderCommand
{
CustomerId = 123,
Items = [new OrderItem { ProductId = 1, Quantity = 2, UnitPrice = 10.50m }]
};
_mockCurrentUser.Setup(x => x.UserId).Returns(456);
// Act
await _handler.HandleAsync(command, CancellationToken.None);
// Assert
_mockAuditService.Verify(x => x.LogCommandAsync(
It.Is<CommandAuditEntry>(entry =>
entry.CommandType == nameof(CreateOrderCommand) &&
entry.UserId == 456),
It.IsAny<CancellationToken>()),
Times.Once);
}
}public class PrePostHandlerIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public PrePostHandlerIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task CreateOrder_WithValidData_ExecutesAllHandlers()
{
// Arrange
using var scope = _factory.Services.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
var auditService = scope.ServiceProvider.GetRequiredService<IAuditService>();
var command = new CreateOrderCommand
{
CustomerId = 1,
Items = [new OrderItem { ProductId = 1, Quantity = 1, UnitPrice = 10.00m }]
};
// Act
await mediator.Send(command);
// Assert
// Verify that pre-handlers ran (validation, authorization)
// Verify that main handler ran (order created)
// Verify that post-handlers ran (audit logged)
var auditEntries = await auditService.GetAuditEntriesAsync("CreateOrderCommand");
Assert.Single(auditEntries);
}
[Fact]
public async Task CreateOrder_WithUnauthorizedUser_ThrowsBeforeMainHandler()
{
// Arrange
using var scope = _factory.Services.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
var command = new CreateOrderCommand
{
CustomerId = 999, // User doesn't have access to this customer
Items = [new OrderItem { ProductId = 1, Quantity = 1, UnitPrice = 10.00m }]
};
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => mediator.Send(command));
// Verify main handler and post-handlers didn't execute
// (order wasn't created, no audit log entry)
}
}public class HandlerExecutionOrderTests
{
private readonly List<string> _executionOrder = new();
private readonly TestHandlerA _preHandler;
private readonly TestHandlerB _mainHandler;
private readonly TestHandlerC _postHandler;
public HandlerExecutionOrderTests()
{
_preHandler = new TestHandlerA(_executionOrder);
_mainHandler = new TestHandlerB(_executionOrder);
_postHandler = new TestHandlerC(_executionOrder);
}
[Fact]
public async Task Handlers_ExecuteInCorrectOrder()
{
// Arrange
var request = new TestRequest();
// Act
await _preHandler.HandleAsync(request, CancellationToken.None);
await _mainHandler.HandleAsync(request, CancellationToken.None);
await _postHandler.HandleAsync(request, CancellationToken.None);
// Assert
Assert.Equal(new[] { "PreHandler", "MainHandler", "PostHandler" }, _executionOrder);
}
}
public class TestHandlerA : ICommandPreHandler<TestRequest>
{
private readonly List<string> _executionOrder;
public TestHandlerA(List<string> executionOrder) => _executionOrder = executionOrder;
public async Task HandleAsync(TestRequest request, CancellationToken cancellationToken = default)
{
_executionOrder.Add("PreHandler");
await Task.CompletedTask;
}
}- Command Module - Learn about command-specific pre/post handlers
- Query Module - Learn about query-specific pre/post handlers
- Event Module - Learn about event-specific pre/post handlers
- Pipeline Behaviors - Advanced pipeline processing
- Getting Started - Basic setup and first steps
- Sample Projects - Complete examples