MediatR Behaviors são middlewares do pipeline de Commands e Queries. Funcionam como um decorator automático: toda requisição que passa pelo MediatR atravessa os behaviors registrados antes (e depois) de chegar ao handler. Isso elimina o copy-paste de logging, validação e transações em cada handler — o cross-cutting concern fica em um único lugar.
Como o pipeline funciona
Quando você chama mediator.Send(command), o MediatR executa os behaviors em ordem de registro antes de invocar o handler real. É exatamente como middlewares ASP.NET Core, mas para a camada de aplicação:
// Ordem de execução:
// LoggingBehavior → ValidationBehavior → CachingBehavior → TransactionBehavior → Handler
// ← resposta flui de volta
Setup
dotnet add package MediatR
dotnet add package FluentValidation.DependencyInjectionExtensions
// Program.cs:
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
// Behaviors registrados na ordem em que serão executados:
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>));
});
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
Behavior 1: Logging automático de todos os handlers
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { 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; var sw = Stopwatch.StartNew(); _logger.LogInformation("→ Iniciando {Request}: {@Payload}", requestName, request); try { var response = await next(); sw.Stop(); _logger.LogInformation("✓ {Request} concluído em {Elapsed}ms", requestName, sw.ElapsedMilliseconds); return response; } catch (Exception ex) { sw.Stop(); _logger.LogError(ex, "✗ {Request} falhou após {Elapsed}ms", requestName, sw.ElapsedMilliseconds); throw; } }
} // Zero mudança nos handlers — todo command/query ganha logging automaticamente.
Behavior 2: Validação automática com FluentValidation
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { 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(); // sem validator registrado = passa direto var context = new ValidationContext<TRequest>(request); var results = await Task.WhenAll( _validators.Select(v => v.ValidateAsync(context, ct))); var failures = results .SelectMany(r => r.Errors) .Where(f => f != null) .ToList(); if (failures.Count != 0) throw new ValidationException(failures); // ValidationException é capturada pelo middleware global de erro → 400 return await next(); }}
// Validator do command — registrado automaticamente pelo DI: public class CriarProdutoCommandValidator : AbstractValidator<CriarProdutoCommand> { public CriarProdutoCommandValidator() { RuleFor(x => x.Nome).NotEmpty().MaximumLength(200); RuleFor(x => x.Preco).GreaterThan(0); RuleFor(x => x.Estoque).GreaterThanOrEqualTo(0); } } // O handler nunca vê dados inválidos — o behavior bloqueia antes.
Behavior 3: Cache automático para Queries
// Interface marcadora — Queries que podem ser cacheadas implementam ICacheable: public interface ICacheable { string CacheKey { get; } TimeSpan CacheDuration { get; } bool BypassCache { get; } // permite forçar atualização do cache }// Query com cache: public record GetProdutoQuery(Guid Id) : IRequest<ProdutoDto?>, ICacheable { public string CacheKey => $“produto:{Id}”; public TimeSpan CacheDuration => TimeSpan.FromMinutes(10); public bool BypassCache => false; }
// Behavior que só age em requests que implementam ICacheable: public class CachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly IDistributedCache _cache;
public CachingBehavior(IDistributedCache cache) => _cache = cache; public async Task<TResponse> Handle( TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct) { // Se a request não é cacheável, passa direto if (request is not ICacheable cacheable || cacheable.BypassCache) return await next(); // Tenta o cache var cached = await _cache.GetStringAsync(cacheable.CacheKey, ct); if (cached != null) return JsonSerializer.Deserialize<TResponse>(cached)!; // Cache miss: executa handler e armazena resultado var response = await next(); if (response != null) { await _cache.SetStringAsync( cacheable.CacheKey, JsonSerializer.Serialize(response), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheable.CacheDuration }, ct); } return response; }
} // Qualquer Query que implementar ICacheable ganha cache automático.
Behavior 4: Transações automáticas para Commands
// Interface marcadora — Commands que precisam de transação: public interface ITransactional { }public record CriarPedidoCommand(Guid ClienteId, List<ItemPedido> Itens) : IRequest<PedidoDto>, ITransactional;
// Behavior que envolve Commands em transação: public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly AppDbContext _db;
public TransactionBehavior(AppDbContext db) => _db = db; public async Task<TResponse> Handle( TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct) { if (request is not ITransactional) return await next(); // Início da transação await using var transaction = await _db.Database.BeginTransactionAsync(ct); try { var response = await next(); await transaction.CommitAsync(ct); return response; } catch { await transaction.RollbackAsync(ct); throw; } }
} // Handlers nunca gerenciam transações explicitamente — o behavior cuida disso.
Behavior 5: Auditoria de Commands sensíveis
public interface IAuditable { string AuditAction { get; } }public record ExcluirUsuarioCommand(Guid UserId) : IRequest<Unit>, ITransactional, IAuditable { public string AuditAction => “usuario.excluido”; }
public class AuditBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly AppDbContext _db; private readonly ICurrentUser _user;
public AuditBehavior(AppDbContext db, ICurrentUser user) { _db = db; _user = user; } public async Task<TResponse> Handle( TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct) { var response = await next(); if (request is IAuditable auditable) { _db.Auditorias.Add(new AuditoriaLog { Acao = auditable.AuditAction, UserId = _user.Id, Payload = JsonSerializer.Serialize(request), ExecutadoEm = DateTime.UtcNow, }); await _db.SaveChangesAsync(ct); } return response; }
}
Handler limpo — zero cross-cutting concerns
O resultado de todos os behaviors: handlers focados apenas na regra de negócio:
// Handler SEM behaviors: logging, validação, transação, cache, auditoria // todos misturados com a lógica de negócio (antes da refatoração)// Handler COM behaviors — apenas regra de negócio: public class CriarProdutoCommandHandler : IRequestHandler<CriarProdutoCommand, ProdutoDto> { private readonly IProdutoRepository _repo;
public CriarProdutoCommandHandler(IProdutoRepository repo) => _repo = repo; public async Task<ProdutoDto> Handle(CriarProdutoCommand command, CancellationToken ct) { // Só regra de negócio — validação, log, transação estão nos behaviors var produto = new Produto(command.Nome, command.Preco, command.Categoria, command.Estoque); await _repo.AddAsync(produto, ct); return ProdutoDto.FromEntity(produto); }
}
Behavior para performance: detectar queries lentas
public class PerformanceBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private const int LimiteMs = 500; // alerta acima de 500ms private readonly ILogger<PerformanceBehavior<TRequest, TResponse>> _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 > LimiteMs) _logger.LogWarning( "⚠️ Request lento: {Request} levou {Elapsed}ms — payload: {@Payload}", typeof(TRequest).Name, sw.ElapsedMilliseconds, request); return response; }
}
Conclusão
MediatR Behaviors transformam o pipeline de aplicação num lugar centralizado para cross-cutting concerns. Logging, validação, cache e transações são implementados uma única vez e aplicados automaticamente a todos os handlers que se encaixam no critério — sem herança, sem atributos, sem decorators manuais. O resultado são handlers menores, mais fáceis de testar e focados exclusivamente na regra de negócio.
Se você está implementando CQRS com MediatR e quer definir os padrões de pipeline certos desde o início, a Neryx pode ajudar. Consultoria inicial gratuita.
Leitura complementar: