Clean Architecture .NET em 2026: CQRS, MediatR e Perguntas de Entrevista para Desenvolvedores

Clean Architecture em .NET com CQRS e MediatR 14. Domine os padrões em camadas, pipeline behaviors e prepare-se para entrevistas de desenvolvedor senior.

Clean Architecture .NET em 2026: CQRS, MediatR e Perguntas de Entrevista para Desenvolvedores

A Clean Architecture em .NET separa a lógica de negócios das preocupações de infraestrutura, tornando as aplicações mais fáceis de testar, manter e escalar. Combinada com CQRS (Command Query Responsibility Segregation) e MediatR, essa arquitetura fornece uma abordagem estruturada para construir aplicações empresariais que os entrevistadores frequentemente avaliam.

Essencial para Entrevistas

Perguntas sobre Clean Architecture aparecem em 70% das entrevistas para desenvolvedores .NET senior. Os entrevistadores esperam que os candidatos expliquem a regra de dependência: as dependências apontam para dentro, com a camada de domínio não tendo nenhuma dependência de frameworks externos.

Camadas da Clean Architecture e a Regra de Dependência

O template Ardalis Clean Architecture para ASP.NET Core 10 organiza o código em quatro camadas concêntricas. Cada camada tem responsabilidades específicas e regras de dependência estritas.

csharp
// Core/Domain Layer - No external dependencies
// src/Core/Domain/Entities/Order.cs
namespace Core.Domain.Entities;

public class Order
{
    public Guid Id { get; private set; }
    public string CustomerEmail { get; private set; }
    public List<OrderLine> Lines { get; private set; } = new();
    public OrderStatus Status { get; private set; }
    public DateTime CreatedAt { get; private set; }

    // Domain logic encapsulated in the entity
    public void AddLine(Product product, int quantity)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Cannot modify a submitted order");

        var existingLine = Lines.FirstOrDefault(l => l.ProductId == product.Id);
        if (existingLine is not null)
        {
            existingLine.IncreaseQuantity(quantity);
            return;
        }

        Lines.Add(new OrderLine(product.Id, product.Price, quantity));
    }

    public void Submit()
    {
        if (!Lines.Any())
            throw new InvalidOperationException("Cannot submit an empty order");

        Status = OrderStatus.Submitted;
    }
}

A camada de domínio contém entidades, objetos de valor e serviços de domínio. Ela não tem conhecimento de bancos de dados, HTTP ou frameworks externos.

Padrão CQRS: Separando Leituras de Escritas

O CQRS divide as operações em Comandos (operações de escrita que alteram o estado) e Consultas (operações de leitura que retornam dados). Essa separação permite escalar e otimizar cada caminho de forma independente.

csharp
// UseCases Layer - Commands and Queries
// src/UseCases/Orders/Commands/CreateOrder/CreateOrderCommand.cs
namespace UseCases.Orders.Commands.CreateOrder;

public record CreateOrderCommand(
    string CustomerEmail,
    List<OrderLineDto> Lines
) : IRequest<Result<Guid>>;

public record OrderLineDto(Guid ProductId, int Quantity);

Comandos retornam dados mínimos, tipicamente apenas um indicador de sucesso ou um ID recém-criado. Consultas retornam DTOs otimizados para o consumidor.

src/UseCases/Orders/Queries/GetOrderById/GetOrderByIdQuery.cscsharp
namespace UseCases.Orders.Queries.GetOrderById;

public record GetOrderByIdQuery(Guid OrderId) : IRequest<Result<OrderDetailsDto>>;

public record OrderDetailsDto(
    Guid Id,
    string CustomerEmail,
    string Status,
    decimal TotalAmount,
    List<OrderLineDetailsDto> Lines
);

MediatR 14: Pipeline Behaviors e Implementação de Handlers

MediatR 14.2 fornece a infraestrutura de mensagens para CQRS. Cada comando ou consulta tem exatamente um handler, aplicando o princípio de responsabilidade única.

src/UseCases/Orders/Commands/CreateOrder/CreateOrderHandler.cscsharp
namespace UseCases.Orders.Commands.CreateOrder;

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IProductRepository _productRepository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateOrderHandler(
        IOrderRepository orderRepository,
        IProductRepository productRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _productRepository = productRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<Result<Guid>> Handle(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        // Load products to validate and get prices
        var productIds = request.Lines.Select(l => l.ProductId).ToList();
        var products = await _productRepository
            .GetByIdsAsync(productIds, cancellationToken);

        if (products.Count != productIds.Count)
            return Result.NotFound("One or more products not found");

        // Create order using domain logic
        var order = new Order(request.CustomerEmail);

        foreach (var line in request.Lines)
        {
            var product = products.First(p => p.Id == line.ProductId);
            order.AddLine(product, line.Quantity);
        }

        await _orderRepository.AddAsync(order, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return Result.Success(order.Id);
    }
}

Pronto para mandar bem nas entrevistas de .NET?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Pipeline Behaviors para Preocupações Transversais

Os pipeline behaviors no MediatR lidam com logging, validação, cache e gerenciamento de transações sem poluir os handlers.

src/UseCases/Common/Behaviors/ValidationBehavior.cscsharp
namespace UseCases.Common.Behaviors;

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 cancellationToken)
    {
        if (!_validators.Any())
            return await next();

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

        var validationResults = await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(context, cancellationToken)));

        var failures = validationResults
            .SelectMany(r => r.Errors)
            .Where(f => f is not null)
            .ToList();

        if (failures.Any())
            throw new ValidationException(failures);

        return await next();
    }
}

Os behaviors são registrados na ordem de execução desejada. A validação tipicamente executa primeiro, seguida pelo logging, depois pelo gerenciamento de transações.

src/Web/Program.cscsharp
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
    cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
    cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
    cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});

Camada de Infraestrutura: Implementação do Repository

A camada de infraestrutura implementa as interfaces definidas na camada core. Os repositories do Entity Framework Core traduzem as operações do domínio em chamadas de banco de dados.

src/Infrastructure/Data/Repositories/OrderRepository.cscsharp
namespace Infrastructure.Data.Repositories;

public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

    public OrderRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<Order?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        return await _context.Orders
            .Include(o => o.Lines)
            .FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
    }

    public async Task AddAsync(
        Order order,
        CancellationToken cancellationToken = default)
    {
        await _context.Orders.AddAsync(order, cancellationToken);
    }

    public async Task<IReadOnlyList<Order>> GetByCustomerEmailAsync(
        string email,
        CancellationToken cancellationToken = default)
    {
        return await _context.Orders
            .Where(o => o.CustomerEmail == email)
            .OrderByDescending(o => o.CreatedAt)
            .ToListAsync(cancellationToken);
    }
}

Perguntas de Entrevista: O Que Desenvolvedores Senior Devem Saber

Entrevistas técnicas para posições .NET senior frequentemente incluem perguntas sobre Clean Architecture. Estes são os padrões que os entrevistadores avaliam:

P: Por que a camada de domínio não tem dependências de frameworks externos?

A camada de domínio contém regras de negócio que devem permanecer estáveis independentemente das mudanças de infraestrutura. Se o domínio depende do Entity Framework, trocar para Dapper ou outro banco de dados exigiria modificar a lógica de negócios. A regra de dependência garante que a infraestrutura é um detalhe que pode ser trocado sem afetar o comportamento central.

P: Quando o CQRS é excessivo?

O CQRS adiciona complexidade que aplicações CRUD simples não precisam. Se os modelos de leitura e escrita são quase idênticos, se não há necessidade de escalonamento separado, e se a equipe é pequena, uma arquitetura mais simples é apropriada. O CQRS brilha quando os modelos de leitura diferem significativamente dos modelos de escrita, quando event sourcing é necessário, ou quando as cargas de leitura e escrita precisam de escalonamento independente.

P: Como lidar com transações através de múltiplos agregados?

csharp
// Transaction behavior wraps the entire handler execution
public class TransactionBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IUnitOfWork _unitOfWork;

    public TransactionBehavior(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        // Commands that modify multiple aggregates use explicit transactions
        if (request is not ICommand)
            return await next();

        await using var transaction = await _unitOfWork
            .BeginTransactionAsync(cancellationToken);

        try
        {
            var response = await next();
            await transaction.CommitAsync(cancellationToken);
            return response;
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }
}

Para operações que abrangem múltiplos agregados, usar consistência eventual com eventos de domínio quando possível. Transações síncronas através de agregados indicam um possível problema de design: os agregados podem pertencer juntos.

Otimização do Lado de Leitura com Modelos de Consulta Separados

As consultas ignoram completamente a camada de domínio quando o desempenho importa. O acesso direto ao banco de dados com Dapper ou SQL puro evita a sobrecarga de materializar grafos de entidades completos.

src/Infrastructure/Queries/GetOrderByIdQueryHandler.cscsharp
namespace Infrastructure.Queries;

public class GetOrderByIdQueryHandler
    : IRequestHandler<GetOrderByIdQuery, Result<OrderDetailsDto>>
{
    private readonly IDbConnection _connection;

    public GetOrderByIdQueryHandler(IDbConnection connection)
    {
        _connection = connection;
    }

    public async Task<Result<OrderDetailsDto>> Handle(
        GetOrderByIdQuery request,
        CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT o.Id, o.CustomerEmail, o.Status, o.CreatedAt,
                   SUM(ol.UnitPrice * ol.Quantity) AS TotalAmount
            FROM Orders o
            LEFT JOIN OrderLines ol ON ol.OrderId = o.Id
            WHERE o.Id = @OrderId
            GROUP BY o.Id, o.CustomerEmail, o.Status, o.CreatedAt
            """;

        var order = await _connection.QueryFirstOrDefaultAsync<OrderDetailsDto>(
            new CommandDefinition(sql, new { request.OrderId }, cancellationToken: cancellationToken));

        return order is null
            ? Result.NotFound($"Order {request.OrderId} not found")
            : Result.Success(order);
    }
}

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Preparação para Entrevistas de Clean Architecture: Pontos-Chave

  • A regra de dependência estabelece que as dependências do código-fonte só podem apontar para dentro. A camada de domínio não tem conhecimento de bancos de dados, frameworks ou mecanismos de entrega.
  • O CQRS separa comandos (mudanças de estado) de consultas (recuperação de dados). Comandos passam pelo modelo de domínio e validação. Consultas podem ignorar as entidades do domínio para melhor desempenho.
  • O MediatR 14 roteia as requisições para handlers. Os pipeline behaviors lidam com preocupações transversais como validação, logging e transações de forma componível.
  • As interfaces de repository vivem na camada core. As implementações vivem na infraestrutura. Essa inversão permite trocar EF Core por Dapper sem tocar na lógica de negócios.
  • As respostas de entrevista devem incluir trade-offs. Clean Architecture adiciona complexidade inicial mas compensa em aplicações grandes e de longa duração com múltiplos desenvolvedores.
  • O módulo Clean Architecture ASP.NET Core no SharpSkill cobre padrões adicionais incluindo Specification, objetos Result e Domain Events.
Desafio do dia

Você saberia encontrar o bug em .NET?

Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador da SharpSkill

Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.

Atualizado em 15 de setembro de 2026

Compartilhar

Artigos relacionados