Pipeline Behaviors¶
MiniatR pipeline behaviors handle cross-cutting concerns before/after handlers.
Overview¶
Behaviors execute in registration order. Validation is handled separately via ASP.NET Core auto-validation.
flowchart TB
A[Request] --> B[HoneypotBehavior]
B --> C[BlockedEmailDomainBehavior]
C --> D[CaptchaBehavior]
D --> E[CurrentUserIdBehavior]
E --> F[CurrentGroupIdBehavior]
F --> G[CachingBehavior]
G --> H[Handler]
H --> I[CacheInvalidationBehavior]
I --> J[Response] Marker Interfaces¶
| Interface | Purpose | Behavior |
|---|---|---|
IRequestWithCurrentUserId | Inject current user ID | CurrentUserIdBehavior |
IRequestWithCurrentGroupId | Inject current group ID (for write-side stamping) | CurrentGroupIdBehavior |
ICacheableQuery | Cache response with tags | CachingBehavior |
ICacheInvalidatingCommand | Invalidate cache tags | CacheInvalidationBehavior |
IRequestWithHoneypot | Bot protection | HoneypotBehavior |
IRequestWithCaptcha | CAPTCHA validation | CaptchaBehavior |
IRequestWithEmail | Block disposable domains | BlockedEmailDomainBehavior |
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:
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 are registered in order via dependency injection:
services.AddMiniatR(cfg => cfg.RegisterServicesFromAssemblyContaining<GetLocationsQuery>())
.AddScoped(typeof(IPipelineBehavior<,>), typeof(HoneypotBehavior<,>))
.AddScoped(typeof(IPipelineBehavior<,>), typeof(BlockedEmailDomainBehavior<,>))
.AddScoped(typeof(IPipelineBehavior<,>), typeof(CaptchaBehavior<,>))
.AddScoped(typeof(IPipelineBehavior<,>), typeof(CurrentUserIdBehavior<,>))
.AddScoped(typeof(IPipelineBehavior<,>), typeof(CurrentGroupIdBehavior<,>))
.AddScoped(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>))
.AddScoped(typeof(IPipelineBehavior<,>), typeof(CacheInvalidationBehavior<,>));
Creating a Behavior¶
public sealed class MyBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(TRequest request, PipelineDelegate<TResponse> next, CancellationToken cancellationToken)
{
// Before handler
var response = await next(cancellationToken);
// After handler
return response;
}
}
Related¶
- CQRS — Command/Query pattern
- Validation — Request validation
- Caching — Server-side caching
- Group Scoping — Multi-tenant data isolation