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.

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.
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.
// 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.
// 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.
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.
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.
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.
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.
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?
// 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.
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.
Você saberia encontrar o bug em .NET?
Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Escrito por
Anthony Fillion-MailletFundador 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

Tempo de Vida do DbContext no ASP.NET Core: Performance vs Segurança de Threads em Operações Async
Domine o gerenciamento do tempo de vida do DbContext no ASP.NET Core. Aprenda quando usar scoped vs transient, como lidar com operações async de forma segura e otimizar performance com pooling de DbContext.

Clean Code Architecture C#: Guia Completo e Perguntas de Entrevista 2026
Domine os princípios de Clean Code e Clean Architecture em C# para criar aplicações .NET manuteníveis e testáveis. Prepare-se para entrevistas técnicas com exemplos práticos.

LINQ Avançado em C# em 2026: Operadores, Performance e Perguntas de Entrevista
Dominar operadores LINQ avançados, otimizar a performance de consultas e se preparar para entrevistas técnicas C# com exemplos práticos.