.NET MediatR CQRS C# Clean Architecture

MediatR Behaviors no .NET: pipeline de cross-cutting concerns sem poluir seus handlers

Como usar MediatR Behaviors no .NET para implementar logging, validação, caching, transações e auditoria como pipeline reutilizável — sem duplicar código.

N
Neryx Digital Architects
20 de dezembro de 2025
11 min de leitura
220 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

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&lt;LoggingBehavior&lt;TRequest, TResponse&gt;&gt; logger)
    => _logger = logger;

public async Task&lt;TResponse&gt; Handle(
    TRequest request, RequestHandlerDelegate&lt;TResponse&gt; 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&lt;IValidator&lt;TRequest&gt;&gt; validators)
    => _validators = validators;

public async Task&lt;TResponse&gt; Handle(
    TRequest request, RequestHandlerDelegate&lt;TResponse&gt; next, CancellationToken ct)
{
    if (!_validators.Any()) return await next(); // sem validator registrado = passa direto

    var context = new ValidationContext&lt;TRequest&gt;(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&lt;TResponse&gt; Handle(
    TRequest request, RequestHandlerDelegate&lt;TResponse&gt; 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&lt;TResponse&gt;(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&lt;TResponse&gt; Handle(
    TRequest request, RequestHandlerDelegate&lt;TResponse&gt; 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&lt;TResponse&gt; Handle(
    TRequest request, RequestHandlerDelegate&lt;TResponse&gt; 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&lt;ProdutoDto&gt; 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&lt;TResponse&gt; Handle(
    TRequest request, RequestHandlerDelegate&lt;TResponse&gt; next, CancellationToken ct)
{
    var sw = Stopwatch.StartNew();
    var response = await next();
    sw.Stop();

    if (sw.ElapsedMilliseconds &gt; 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:

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.