Skip to content
GitHubDiscord

Request Processors

Request processors provide a simpler alternative to pipeline behaviors for adding pre-processing and post-processing logic to your handlers. They’re ideal for cross-cutting concerns that don’t need to modify the request/response flow.

graph LR
    A[Request] --> B[Pre-Processors]
    B --> C[Handler]
    C --> D[Post-Processors]
    D --> E[Response]
AspectPre-ProcessorPost-Processor
WhenBefore handler executesAfter handler executes
AccessRequest onlyRequest + Response
Use CasesValidation, Authorization, Data enrichmentLogging, Auditing, Notifications
Can StopYes (throw exception)No
builder.Services.AddCortexMediator(
    new[] { typeof(Program).Assembly },
    options => options.AddProcessorBehaviors()
);
public interface IRequestPreProcessor<in TRequest>
{
    Task ProcessAsync(TRequest request, CancellationToken cancellationToken);
}
using Cortex.Mediator.Processors;

public class LoggingPreProcessor<TRequest> : IRequestPreProcessor<TRequest>
{
    private readonly ILogger<LoggingPreProcessor<TRequest>> _logger;

    public LoggingPreProcessor(ILogger<LoggingPreProcessor<TRequest>> logger)
    {
        _logger = logger;
    }

    public Task ProcessAsync(TRequest request, CancellationToken cancellationToken)
    {
        _logger.LogInformation(
            "Processing {RequestType}: {@Request}",
            typeof(TRequest).Name,
            request);

        return Task.CompletedTask;
    }
}

// Register for all requests
builder.Services.AddTransient(typeof(IRequestPreProcessor<>), typeof(LoggingPreProcessor<>));
public class OrderValidationPreProcessor : IRequestPreProcessor<CreateOrderCommand>
{
    private readonly IInventoryService _inventoryService;

    public OrderValidationPreProcessor(IInventoryService inventoryService)
    {
        _inventoryService = inventoryService;
    }

    public async Task ProcessAsync(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        // Validate inventory before handler executes
        foreach (var item in request.Items)
        {
            var available = await _inventoryService.CheckAvailabilityAsync(
                item.ProductId,
                item.Quantity,
                cancellationToken);

            if (!available)
            {
                throw new InsufficientInventoryException(item.ProductId);
            }
        }
    }
}

// Register for specific request type
builder.Services.AddTransient<IRequestPreProcessor<CreateOrderCommand>, OrderValidationPreProcessor>();
// For requests that return a value
public interface IRequestPostProcessor<in TRequest, in TResponse>
{
    Task ProcessAsync(TRequest request, TResponse response, CancellationToken cancellationToken);
}

// For void requests
public interface IRequestPostProcessor<in TRequest>
{
    Task ProcessAsync(TRequest request, CancellationToken cancellationToken);
}
public class AuditPostProcessor<TRequest, TResponse> 
    : IRequestPostProcessor<TRequest, TResponse>
{
    private readonly IAuditService _auditService;
    private readonly ICurrentUserService _currentUserService;

    public AuditPostProcessor(
        IAuditService auditService,
        ICurrentUserService currentUserService)
    {
        _auditService = auditService;
        _currentUserService = currentUserService;
    }

    public async Task ProcessAsync(
        TRequest request,
        TResponse response,
        CancellationToken cancellationToken)
    {
        var user = _currentUserService.GetCurrentUser();

        await _auditService.LogAsync(new AuditEntry
        {
            UserId = user?.Id,
            RequestType = typeof(TRequest).Name,
            ResponseType = typeof(TResponse).Name,
            Timestamp = DateTime.UtcNow,
            Success = true
        }, cancellationToken);
    }
}

// Register for all requests
builder.Services.AddTransient(
    typeof(IRequestPostProcessor<,>), 
    typeof(AuditPostProcessor<,>));
public class OrderCreatedPostProcessor : IRequestPostProcessor<CreateOrderCommand, OrderDto>
{
    private readonly IMediator _mediator;
    private readonly ILogger<OrderCreatedPostProcessor> _logger;

    public OrderCreatedPostProcessor(
        IMediator mediator,
        ILogger<OrderCreatedPostProcessor> logger)
    {
        _mediator = mediator;
        _logger = logger;
    }

    public async Task ProcessAsync(
        CreateOrderCommand request,
        OrderDto response,
        CancellationToken cancellationToken)
    {
        _logger.LogInformation(
            "Order {OrderId} created, publishing notification",
            response.Id);

        // Publish notification after successful order creation
        await _mediator.PublishAsync(new OrderCreatedNotification
        {
            OrderId = response.Id,
            CustomerId = request.CustomerId,
            TotalAmount = response.TotalAmount
        }, cancellationToken);
    }
}

// Register for specific request/response types
builder.Services.AddTransient<
    IRequestPostProcessor<CreateOrderCommand, OrderDto>,
    OrderCreatedPostProcessor>();
public class DeleteUserPostProcessor : IRequestPostProcessor<DeleteUserCommand>
{
    private readonly ICacheInvalidator _cacheInvalidator;
    private readonly ISearchIndexService _searchIndex;

    public DeleteUserPostProcessor(
        ICacheInvalidator cacheInvalidator,
        ISearchIndexService searchIndex)
    {
        _cacheInvalidator = cacheInvalidator;
        _searchIndex = searchIndex;
    }

    public async Task ProcessAsync(
        DeleteUserCommand request,
        CancellationToken cancellationToken)
    {
        // Clean up after user deletion
        _cacheInvalidator.InvalidateByPrefix($"user-{request.UserId}");
        
        await _searchIndex.RemoveDocumentAsync(
            "users",
            request.UserId.ToString(),
            cancellationToken);
    }
}
public interface IRequiresAuthorization
{
    string RequiredPermission { get; }
    Guid? ResourceId { get; }
}

public class AuthorizationPreProcessor<TRequest> : IRequestPreProcessor<TRequest>
    where TRequest : IRequiresAuthorization
{
    private readonly IAuthorizationService _authorizationService;
    private readonly ICurrentUserService _currentUserService;

    public AuthorizationPreProcessor(
        IAuthorizationService authorizationService,
        ICurrentUserService currentUserService)
    {
        _authorizationService = authorizationService;
        _currentUserService = currentUserService;
    }

    public async Task ProcessAsync(
        TRequest request,
        CancellationToken cancellationToken)
    {
        var user = _currentUserService.GetCurrentUser();
        
        if (user == null)
        {
            throw new UnauthorizedException("User is not authenticated");
        }

        var authorized = await _authorizationService.AuthorizeAsync(
            user.Id,
            request.RequiredPermission,
            request.ResourceId,
            cancellationToken);

        if (!authorized)
        {
            throw new ForbiddenException(
                $"User does not have permission: {request.RequiredPermission}");
        }
    }
}

// Usage
public class UpdateOrderCommand : ICommand<OrderDto>, IRequiresAuthorization
{
    public Guid OrderId { get; init; }
    public string Status { get; init; }
    
    public string RequiredPermission => "orders.update";
    public Guid? ResourceId => OrderId;
}
public interface IHasUserContext
{
    Guid? UserId { get; set; }
    string UserEmail { get; set; }
    string UserName { get; set; }
}

public class UserContextEnrichmentPreProcessor<TRequest> : IRequestPreProcessor<TRequest>
    where TRequest : IHasUserContext
{
    private readonly ICurrentUserService _currentUserService;

    public UserContextEnrichmentPreProcessor(ICurrentUserService currentUserService)
    {
        _currentUserService = currentUserService;
    }

    public Task ProcessAsync(
        TRequest request,
        CancellationToken cancellationToken)
    {
        var user = _currentUserService.GetCurrentUser();
        
        if (user != null)
        {
            request.UserId = user.Id;
            request.UserEmail = user.Email;
            request.UserName = user.Name;
        }

        return Task.CompletedTask;
    }
}

// Usage
public class CreateCommentCommand : ICommand<CommentDto>, IHasUserContext
{
    public string Content { get; init; }
    public Guid PostId { get; init; }
    
    // Auto-filled by pre-processor
    public Guid? UserId { get; set; }
    public string UserEmail { get; set; }
    public string UserName { get; set; }
}

Example 3: Performance Tracking Post-Processor

Section titled “Example 3: Performance Tracking Post-Processor”
public class PerformanceTrackingPostProcessor<TRequest, TResponse> 
    : IRequestPostProcessor<TRequest, TResponse>
{
    private readonly IMetricsCollector _metrics;
    private readonly ILogger<PerformanceTrackingPostProcessor<TRequest, TResponse>> _logger;

    public PerformanceTrackingPostProcessor(
        IMetricsCollector metrics,
        ILogger<PerformanceTrackingPostProcessor<TRequest, TResponse>> logger)
    {
        _metrics = metrics;
        _logger = logger;
    }

    public Task ProcessAsync(
        TRequest request,
        TResponse response,
        CancellationToken cancellationToken)
    {
        var requestName = typeof(TRequest).Name;

        // Record success metric
        _metrics.IncrementCounter("mediator_requests_total", new[]
        {
            ("request_type", requestName),
            ("status", "success")
        });

        // Log response size for analysis
        var responseJson = JsonSerializer.Serialize(response);
        if (responseJson.Length > 10000) // 10KB
        {
            _logger.LogWarning(
                "Large response detected for {RequestType}: {Size} bytes",
                requestName,
                responseJson.Length);
        }

        return Task.CompletedTask;
    }
}

Example 4: Cache Invalidation Post-Processor

Section titled “Example 4: Cache Invalidation Post-Processor”
public interface IInvalidatesCache
{
    IEnumerable<string> GetCacheKeysToInvalidate();
}

public class CacheInvalidationPostProcessor<TRequest, TResponse> 
    : IRequestPostProcessor<TRequest, TResponse>
    where TRequest : IInvalidatesCache
{
    private readonly ICacheInvalidator _cacheInvalidator;
    private readonly ILogger<CacheInvalidationPostProcessor<TRequest, TResponse>> _logger;

    public CacheInvalidationPostProcessor(
        ICacheInvalidator cacheInvalidator,
        ILogger<CacheInvalidationPostProcessor<TRequest, TResponse>> logger)
    {
        _cacheInvalidator = cacheInvalidator;
        _logger = logger;
    }

    public Task ProcessAsync(
        TRequest request,
        TResponse response,
        CancellationToken cancellationToken)
    {
        var keysToInvalidate = request.GetCacheKeysToInvalidate().ToList();

        foreach (var key in keysToInvalidate)
        {
            _cacheInvalidator.InvalidateByPrefix(key);
        }

        _logger.LogInformation(
            "Invalidated {Count} cache keys for {RequestType}",
            keysToInvalidate.Count,
            typeof(TRequest).Name);

        return Task.CompletedTask;
    }
}

// Usage
public class UpdateProductCommand : ICommand<ProductDto>, IInvalidatesCache
{
    public Guid ProductId { get; init; }
    public string Name { get; init; }
    public decimal Price { get; init; }

    public IEnumerable<string> GetCacheKeysToInvalidate()
    {
        yield return $"product-{ProductId}";
        yield return "products-list";
        yield return "products-search";
    }
}
public interface IPublishesNotification<TNotification> where TNotification : INotification
{
    TNotification CreateNotification();
}

public class NotificationPostProcessor<TRequest, TResponse, TNotification> 
    : IRequestPostProcessor<TRequest, TResponse>
    where TRequest : IPublishesNotification<TNotification>
    where TNotification : INotification
{
    private readonly IMediator _mediator;

    public NotificationPostProcessor(IMediator mediator)
    {
        _mediator = mediator;
    }

    public async Task ProcessAsync(
        TRequest request,
        TResponse response,
        CancellationToken cancellationToken)
    {
        var notification = request.CreateNotification();
        await _mediator.PublishAsync(notification, cancellationToken);
    }
}

// Usage
public class RegisterUserCommand 
    : ICommand<UserDto>, IPublishesNotification<UserRegisteredNotification>
{
    public string Email { get; init; }
    public string Name { get; init; }

    // Will be set after handler executes
    public Guid CreatedUserId { get; set; }

    public UserRegisteredNotification CreateNotification()
    {
        return new UserRegisteredNotification
        {
            UserId = CreatedUserId,
            Email = Email,
            Name = Name
        };
    }
}

For queries, use the dedicated query processor interfaces:

// Pre-processor for queries
public class QueryLoggingPreProcessor<TQuery, TResult> 
    : IRequestPreProcessor<TQuery>
    where TQuery : IQuery<TResult>
{
    private readonly ILogger<QueryLoggingPreProcessor<TQuery, TResult>> _logger;

    public QueryLoggingPreProcessor(
        ILogger<QueryLoggingPreProcessor<TQuery, TResult>> logger)
    {
        _logger = logger;
    }

    public Task ProcessAsync(TQuery request, CancellationToken cancellationToken)
    {
        _logger.LogDebug("Executing query {QueryType}", typeof(TQuery).Name);
        return Task.CompletedTask;
    }
}

// Post-processor for queries
public class QueryMetricsPostProcessor<TQuery, TResult> 
    : IRequestPostProcessor<TQuery, TResult>
    where TQuery : IQuery<TResult>
{
    private readonly IMetricsCollector _metrics;

    public QueryMetricsPostProcessor(IMetricsCollector metrics)
    {
        _metrics = metrics;
    }

    public Task ProcessAsync(
        TQuery request,
        TResult response,
        CancellationToken cancellationToken)
    {
        _metrics.IncrementCounter("queries_executed", new[]
        {
            ("query_type", typeof(TQuery).Name)
        });

        return Task.CompletedTask;
    }
}
// Generic pre-processor for all requests
builder.Services.AddTransient(
    typeof(IRequestPreProcessor<>), 
    typeof(LoggingPreProcessor<>));

// Generic post-processor for all request/response combinations
builder.Services.AddTransient(
    typeof(IRequestPostProcessor<,>), 
    typeof(AuditPostProcessor<,>));
// Pre-processor for specific command
builder.Services.AddTransient<
    IRequestPreProcessor<CreateOrderCommand>,
    OrderValidationPreProcessor>();

// Post-processor for specific command and response
builder.Services.AddTransient<
    IRequestPostProcessor<CreateOrderCommand, OrderDto>,
    OrderCreatedPostProcessor>();
// Register only for requests implementing an interface
builder.Services.Scan(scan => scan
    .FromAssemblyOf<Program>()
    .AddClasses(classes => classes
        .AssignableTo(typeof(IRequestPreProcessor<>)))
    .AsImplementedInterfaces()
    .WithTransientLifetime());
AspectProcessorsBehaviors
ComplexitySimpleMore complex
ModificationCan’t modify request/responseCan modify both
Short-circuitPre can throwFull control over flow
Wrap HandlerNoYes
Best ForSide effectsCross-cutting logic

Use Processors when you need to:

  • Log requests/responses
  • Validate before execution
  • Audit after execution
  • Publish notifications
  • Invalidate cache

Use Behaviors when you need to:

  • Transform requests/responses
  • Short-circuit based on conditions
  • Implement caching
  • Add retry logic
  • Measure execution time
  • Keep processors focused - One processor, one responsibility
  • Use interfaces - Define marker interfaces for conditional processing
  • Handle failures gracefully - Don’t let post-processor failures break the response
  • Log appropriately - Use proper log levels
  • Register in correct order - Pre-processors run in registration order
  • Don’t modify requests in post-processors - They run after the handler
  • Don’t use for business logic - Keep that in handlers
  • Don’t make post-processors critical - They shouldn’t affect the main response
  • Don’t forget async - Always use async/await
  • Don’t swallow exceptions - Log and rethrow if needed