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.

Table of Contents

Overview

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.

Key Benefits

  • 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

Execution Order

Request → Pre-Handlers → Main Handler → Post-Handlers → Response

Execution Flow

The execution flow varies slightly between Commands, Queries, and Events:

Commands

Command → Command Pre-Handlers → Command Handler → Command Post-Handlers → Result

Queries

Query → Query Pre-Handlers → Query Handler → Query Post-Handlers → Response Data

Events

Event → Event Pre-Handlers → Event Handlers (Multiple) → Event Post-Handlers

Pre-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

Command Pre-Handlers

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

Query Pre-Handlers

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

Event Pre-Handlers

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

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

Command Post-Handlers

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

Query Post-Handlers

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

Event Post-Handlers

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

Implementation by Pattern

Command Implementation

// 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 Implementation

// 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 Implementation

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

Common Use Cases

1. Input Validation

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

2. Authorization and Security

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

3. Audit Logging

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

4. Performance Monitoring

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

5. Data Enrichment

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

6. Response Transformation

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

Registration and Configuration

Basic Registration

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddArcanicMediator()
    .AddCommands(Assembly.GetExecutingAssembly())
    .AddQueries(Assembly.GetExecutingAssembly())
    .AddEvents(Assembly.GetExecutingAssembly());

var app = builder.Build();

Registering Specific Pre/Post Handlers

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

Conditional Registration

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

Best Practices

1. Keep Handlers Focused

// ✅ 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);
    }
}

2. Use Generic Handlers for Common Patterns

// ✅ 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);
            }
        }
    }
}

3. Handle Failures Appropriately

// ✅ 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);
        }
    }
}

4. Use Dependency Injection Correctly

// ✅ 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");
        }
    }
}

5. Document Handler Purpose

/// <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
    }
}

Testing

Unit Testing Pre-Handlers

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

Unit Testing Post-Handlers

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

Integration Testing

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

Testing Handler Order

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

Next Steps

Related Documentation

⚠️ **GitHub.com Fallback** ⚠️