Skip to content

Pipeline Behaviors

MediatR pipeline behaviors handle cross-cutting concerns before/after handlers.

Overview

flowchart TB
    A[Request] --> B[ValidationBehavior]
    B --> C[UserIdBehavior]
    C --> D[GroupIdBehavior]
    D --> E[HoneypotBehavior]
    E --> F[CaptchaBehavior]
    F --> G[BlockedDomainBehavior]
    G --> H[CachingBehavior]
    H --> I[Handler]
    I --> J[CacheInvalidationBehavior]
    J --> K[Response]

Marker Interfaces

Interface Purpose Behavior
IRequestWithUserId Inject current user ID UserIdBehavior
IRequestWithGroupId Inject current group ID GroupIdBehavior
ICacheable Cache response CachingBehavior
ICacheInvalidator Invalidate cache keys CacheInvalidationBehavior
IRequestWithHoneypot Bot protection HoneypotBehavior
IRequestWithCaptcha CAPTCHA validation CaptchaBehavior

Honeypot Protection

Hidden field that bots fill but humans don't.

public interface IRequestWithHoneypot
{
    string? Honeypot { get; }
}

public sealed record RegisterCommand(RegisterRequest Request)
    : IRequest<Unit>, IRequestWithHoneypot
{
    public string? Honeypot => Request.Honeypot;
}

Behavior rejects requests with filled honeypot:

if (!string.IsNullOrEmpty(request.Honeypot))
{
    throw new ValidationException("Invalid request");
}

CAPTCHA Validation

Cloudflare Turnstile validation for public endpoints.

public interface IRequestWithCaptcha
{
    string? CaptchaToken { get; }
}

public sealed record RegisterCommand(RegisterRequest Request)
    : IRequest<Unit>, IRequestWithCaptcha
{
    public string? CaptchaToken => Request.CaptchaToken;
}

Behavior validates token with Turnstile API:

var isValid = await turnstileService.ValidateAsync(request.CaptchaToken);

if (!isValid)
{
    throw new ValidationException("CAPTCHA validation failed");
}

Fail-closed: Invalid/missing tokens are rejected.

Blocked Email Domains

Prevents registration with disposable email domains.

public class BlockedEmailDomainBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
{
    public async Task<TResponse> Handle(TRequest request, ...)
    {
        if (request is IRequestWithEmail emailRequest)
        {
            var domain = emailRequest.Email.Split('@').Last();

            if (blockedDomains.Contains(domain))
            {
                throw new ValidationException("Email domain not allowed");
            }
        }

        return await next();
    }
}

Blocked domains configured via Security__Account__BlockedDomains.

Behavior Registration

Behaviors registered in order:

services.AddMediatR(cfg =>
{
    cfg.AddBehavior<ValidationBehavior>();
    cfg.AddBehavior<UserIdBehavior>();
    cfg.AddBehavior<GroupIdBehavior>();
    cfg.AddBehavior<HoneypotBehavior>();
    cfg.AddBehavior<CaptchaBehavior>();
    cfg.AddBehavior<BlockedEmailDomainBehavior>();
    cfg.AddBehavior<CachingBehavior>();
    cfg.AddBehavior<CacheInvalidationBehavior>();
});

Creating a Behavior

public class MyBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        // Before handler
        var response = await next();
        // After handler
        return response;
    }
}