Skip to main content

Overview

The Intent.Application.MediatR.Behaviours module generates MediatR pipeline behaviours that add cross-cutting concerns to your request handling. These behaviours wrap your command and query handlers with logging, authorization, validation, transactions, and more.
Behaviours are executed in a pipeline before and after your handlers run, making them ideal for cross-cutting concerns that apply to multiple operations.

Generated Behaviours

This module generates the following pipeline behaviours:

UnhandledExceptionBehaviour

Catches and logs any unhandled exceptions from your handlers.
UnhandledExceptionBehaviour.cs
public class UnhandledExceptionBehaviour<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly ILogger<UnhandledExceptionBehaviour<TRequest, TResponse>> _logger;
    private readonly bool _logRequestPayload;

    public UnhandledExceptionBehaviour(
        ILogger<UnhandledExceptionBehaviour<TRequest, TResponse>> logger,
        IConfiguration configuration)
    {
        _logger = logger;
        _logRequestPayload = configuration.GetValue<bool?>("CqrsSettings:LogRequestPayload") ?? false;
    }

    public async Task<TResponse> Handle(
        TRequest request, 
        RequestHandlerDelegate<TResponse> next, 
        CancellationToken cancellationToken)
    {
        try
        {
            return await next();
        }
        catch (Exception ex)
        {
            var requestName = typeof(TRequest).Name;
            
            if (_logRequestPayload)
            {
                _logger.LogError(ex, "MyApp Request: Unhandled Exception for Request {Name} {@Request}", 
                    requestName, request);
            }
            else
            {
                _logger.LogError(ex, "MyApp Request: Unhandled Exception for Request {Name}", requestName);
            }
            
            throw;
        }
    }
}
Features:
  • Logs exception details with request name
  • Optionally includes full request payload in logs
  • Configured via CqrsSettings:LogRequestPayload setting

LoggingBehaviour

Logs request information before handler execution.
LoggingBehaviour.cs
public class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest>
    where TRequest : notnull
{
    private readonly ILogger<LoggingBehaviour<TRequest>> _logger;
    private readonly ICurrentUserService _currentUserService;
    private readonly bool _logRequestPayload;

    public async Task Process(TRequest request, CancellationToken cancellationToken)
    {
        var requestName = typeof(TRequest).Name;
        var user = await _currentUserService.GetAsync();

        if (_logRequestPayload)
        {
            _logger.LogInformation(
                "MyApp Request: {Name} {@UserId} {@UserName} {@Request}",
                requestName, user?.Id, user?.Name, request);
        }
        else
        {
            _logger.LogInformation(
                "MyApp Request: {Name} {@UserId} {@UserName}",
                requestName, user?.Id, user?.Name);
        }
    }
}
Features:
  • Logs request name, user ID, and username
  • Optionally includes full request payload
  • Runs before handler execution

AuthorizationBehaviour

Enforces authorization rules based on attributes.
public class AuthorizationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly ICurrentUserService _currentUserService;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var authorizeAttributes = request.GetType()
            .GetCustomAttributes<AuthorizeAttribute>();

        if (authorizeAttributes.Any())
        {
            var user = await _currentUserService.GetAsync();

            if (user == null)
            {
                throw new UnauthorizedAccessException();
            }

            // Check roles and policies...
        }

        return await next();
    }
}
Features:
  • Enforces [Authorize] attribute semantics
  • Checks roles and policies
  • Integrates with ICurrentUserService

UnitOfWorkBehaviour

Ensures database changes are committed after successful handler execution.
UnitOfWorkBehaviour.cs
/// <summary>
/// Ensures that all operations processed as part of handling a <see cref="ICommand"/> either
/// pass or fail as one unit. This behaviour makes it unnecessary for developers to call
/// SaveChangesAsync() inside their business logic (e.g. command handlers), and doing so should
/// be avoided unless absolutely necessary.
/// </summary>
public class UnitOfWorkBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull, ICommand
{
    private readonly IUnitOfWork _unitOfWork;

    public UnitOfWorkBehaviour(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var response = await next();
        await _unitOfWork.SaveChangesAsync(cancellationToken);
        return response;
    }
}
Features:
  • Only applies to commands (via ICommand constraint)
  • Automatically commits changes after handler succeeds
  • Developers don’t need to call SaveChangesAsync() manually

PerformanceBehaviour

Measures and logs request execution time.
public class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly Stopwatch _timer;
    private readonly ILogger<PerformanceBehaviour<TRequest, TResponse>> _logger;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        _timer.Start();
        var response = await next();
        _timer.Stop();

        var elapsedMilliseconds = _timer.ElapsedMilliseconds;
        if (elapsedMilliseconds > 500) // Configurable threshold
        {
            var requestName = typeof(TRequest).Name;
            _logger.LogWarning(
                "Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@Request}",
                requestName, elapsedMilliseconds, request);
        }

        return response;
    }
}
Features:
  • Tracks request execution time
  • Logs warnings for slow requests
  • Configurable performance threshold

MessageBusPublishBehaviour

Publishes integration events after successful command execution.
public class MessageBusPublishBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly IEventBus _eventBus;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var response = await next();
        await _eventBus.FlushAllAsync(cancellationToken);
        return response;
    }
}
Features:
  • Publishes queued integration events
  • Only publishes after handler succeeds
  • Requires Intent.Eventing.Contracts module

Behaviour Execution Order

MediatR executes behaviours in registration order (outer → inner). Typical ordering:
The order matters! For example, UnhandledExceptionBehaviour should wrap the handler closely to catch all exceptions, while UnitOfWorkBehaviour needs to commit changes before MessageBusPublishBehaviour publishes events.

Configuration

Log Request Payload

CqrsSettings.LogRequestPayload
boolean
default:"false"
Controls whether the full request object is logged by LoggingBehaviour and UnhandledExceptionBehaviour.Security Warning: Enabling this may log sensitive data. Use with caution in production.
Configuration in appsettings.json:
{
  "CqrsSettings": {
    "LogRequestPayload": false
  }
}

Dependencies

This module requires:
  • Intent.Application.MediatR - Base MediatR implementation
  • Intent.Application.Identity - Provides ICurrentUserService for authorization
  • Intent.Application.DependencyInjection.MediatR - DI registration
  • Intent.Common.UnitOfWork - Unit of work abstraction for transactions
Optional dependencies:
  • Intent.Eventing.Contracts - For MessageBusPublishBehaviour
  • Intent.CosmosDB - Cosmos DB transaction support
  • Intent.MongoDb - MongoDB transaction support

When to Use

Use This Module When

  • You need consistent cross-cutting concerns
  • Medium to large applications
  • Multiple commands/queries benefit from same behaviors
  • Want to avoid scattering logging/auth/validation logic

Skip This Module When

  • Extremely small or script-like projects
  • Pipeline overhead is unnecessary
  • Custom behavior ordering is required
  • Using a different middleware pattern

Customization

Behaviours are generated with merge mode, allowing customization:
[IntentManaged(Mode.Merge, Signature = Mode.Fully)]
public partial class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest>
    where TRequest : notnull
{
    // Add custom fields or methods
    private readonly IMetricsService _metrics;

    partial void OnLogging(TRequest request)
    {
        // Custom logging logic
        _metrics.IncrementCounter("requests", typeof(TRequest).Name);
    }
}
Common customizations:
  • Add correlation IDs to logging
  • Integrate OpenTelemetry tracing
  • Custom authorization policies
  • Filter or batch event publication
  • Adjust performance thresholds

MediatR

Core CQRS implementation with MediatR

MediatR FluentValidation

Validation behaviour using FluentValidation

Application Identity

Provides ICurrentUserService for authorization

Additional Resources

Build docs developers (and LLMs) love