Chapter 03

Pipeline
Behaviors

Cross-cutting concerns — logging, validation, caching qua IPipelineBehavior

Pipeline Behavior là gì?

Pipeline Behavior trong MediatR hoạt động giống middleware trong ASP.NET Core, nhưng ở tầng application logic. Mỗi behavior bọc quanh handler, có thể chạy code trướcsau khi handler thực thi — tạo thành mô hình onion (hành tây).

Hãy hình dung: mỗi request phải đi qua nhiều "lớp vỏ" trước khi đến handler ở giữa, và đi ngược lại qua các lớp đó khi trả response. Mỗi lớp vỏ là một behavior.

Mô hình Onion

Logging Behavior
Log request/response, thời gian thực thi
Validation Behavior
Validate request, throw nếu invalid
Authorization Behavior
Kiểm tra quyền trước khi xử lý
Handler
Business logic chính
Authorization Behavior
post-processing (nếu có)
Validation Behavior
post-processing (nếu có)
Logging Behavior
Log kết quả, thời gian tổng

IPipelineBehavior Interface

Mọi behavior đều implement IPipelineBehavior<TRequest, TResponse>:

C#
public interface IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken);
}

// RequestHandlerDelegate là delegate gọi behavior/handler tiếp theo
// Gọi next() = đi sâu thêm một lớp trong onion
// Không gọi next() = short-circuit, handler không bao giờ chạy

Logging Behavior

Behavior phổ biến nhất — log request, response, và thời gian thực thi:

C#
public class LoggingBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(
        ILogger<LoggingBehavior<TRequest, TResponse>> logger)
        => _logger = logger;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        var requestName = typeof(TRequest).Name;

        _logger.LogInformation(
            "[MediatR] Handling {RequestName}: {@Request}",
            requestName, request);

        var sw = Stopwatch.StartNew();

        var response = await next(); // Gọi behavior/handler tiếp theo

        sw.Stop();

        _logger.LogInformation(
            "[MediatR] Handled {RequestName} in {ElapsedMs}ms",
            requestName, sw.ElapsedMilliseconds);

        return response;
    }
}

Validation Behavior

Kết hợp với FluentValidation — tự động validate mọi request trước khi handler chạy:

Bash
dotnet add package FluentValidation.DependencyInjectionExtensions
C#
public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(
        IEnumerable<IValidator<TRequest>> validators)
        => _validators = validators;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);

        var failures = (await Task.WhenAll(
                _validators.Select(v => v.ValidateAsync(context, ct))))
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Count > 0)
            throw new ValidationException(failures);

        return await next(); // Chỉ chạy handler nếu valid
    }
}

Ví dụ Validator

C#
public class CreateProductValidator
    : AbstractValidator<CreateProductCommand>
{
    public CreateProductValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Tên sản phẩm không được trống")
            .MaximumLength(200).WithMessage("Tên tối đa 200 ký tự");

        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Giá phải lớn hơn 0");

        RuleFor(x => x.Category)
            .NotEmpty().WithMessage("Danh mục không được trống");
    }
}
Inject IEnumerable<IValidator<TRequest>> thay vì IValidator<TRequest>. Nếu request chưa có validator, collection sẽ rỗng — behavior sẽ skip và gọi next() luôn, không crash.

Performance Behavior

Cảnh báo khi một request chạy quá lâu — hữu ích để phát hiện bottleneck:

C#
public class PerformanceBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly ILogger<PerformanceBehavior<TRequest, TResponse>> _logger;
    private const int ThresholdMs = 500;

    public PerformanceBehavior(
        ILogger<PerformanceBehavior<TRequest, TResponse>> logger)
        => _logger = logger;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        var sw = Stopwatch.StartNew();
        var response = await next();
        sw.Stop();

        if (sw.ElapsedMilliseconds > ThresholdMs)
        {
            _logger.LogWarning(
                "[PERF] {RequestName} took {ElapsedMs}ms! Request: {@Request}",
                typeof(TRequest).Name,
                sw.ElapsedMilliseconds,
                request);
        }

        return response;
    }
}

Authorization Behavior

Kiểm tra quyền trước khi handler chạy. Dùng marker interface hoặc attribute để đánh dấu request nào cần authorize:

C#
// Marker interface
public interface IRequireAuthorization
{
    string[] RequiredRoles { get; }
}

// Request cần authorize
public record DeleteProductCommand(int Id)
    : IRequest, IRequireAuthorization
{
    public string[] RequiredRoles => ["Admin"];
}

// Behavior
public class AuthorizationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly IHttpContextAccessor _httpContext;

    public AuthorizationBehavior(IHttpContextAccessor httpContext)
        => _httpContext = httpContext;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        if (request is IRequireAuthorization authRequest)
        {
            var user = _httpContext.HttpContext?.User;

            if (user?.Identity?.IsAuthenticated != true)
                throw new UnauthorizedAccessException();

            var hasRole = authRequest.RequiredRoles
                .Any(role => user.IsInRole(role));

            if (!hasRole)
                throw new ForbiddenAccessException();
        }

        return await next();
    }
}

Đăng ký Behaviors

Đăng ký behaviors trong Program.cs. Thứ tự đăng ký = thứ tự thực thi (ngoài vào trong):

C#
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);

    // Thứ tự: Logging → Validation → Authorization → Handler
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehavior<,>));
});
LoggingBehavior — Outermost: log mọi thứ, kể cả validation errors
ValidationBehavior — Fail fast nếu request invalid
AuthorizationBehavior — Chỉ kiểm tra quyền nếu request đã valid
Handler — Innermost: business logic chính
Quan trọng: Nếu dùng AddOpenBehavior() thay vì AddBehavior(), thứ tự không được đảm bảo. Dùng AddBehavior() khi cần kiểm soát thứ tự chính xác.

Pre & Post Processors

MediatR cung cấp hai interface đặc biệt cho logic chỉ chạy trước hoặc chỉ chạy sau handler:

C#
// Chạy TRƯỚC handler
public class LogRequestPreProcessor<TRequest>
    : IRequestPreProcessor<TRequest>
    where TRequest : notnull
{
    private readonly ILogger<LogRequestPreProcessor<TRequest>> _logger;

    public LogRequestPreProcessor(
        ILogger<LogRequestPreProcessor<TRequest>> logger)
        => _logger = logger;

    public Task Process(TRequest request, CancellationToken ct)
    {
        _logger.LogInformation(
            "[PRE] {RequestName}: {@Request}",
            typeof(TRequest).Name, request);
        return Task.CompletedTask;
    }
}

// Chạy SAU handler
public class LogResponsePostProcessor<TRequest, TResponse>
    : IRequestPostProcessor<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly ILogger<LogResponsePostProcessor<TRequest, TResponse>> _logger;

    public LogResponsePostProcessor(
        ILogger<LogResponsePostProcessor<TRequest, TResponse>> logger)
        => _logger = logger;

    public Task Process(
        TRequest request, TResponse response,
        CancellationToken ct)
    {
        _logger.LogInformation(
            "[POST] {RequestName} → {@Response}",
            typeof(TRequest).Name, response);
        return Task.CompletedTask;
    }
}
IRequestPreProcessor chạy trước tất cả behaviors. IRequestPostProcessor chạy sau tất cả behaviors (sau handler trả response). Chúng tự động được MediatR đăng ký — không cần AddBehavior().