Clean Architecture com .NET: Guia Prático
Domine a Clean Architecture em .NET com C#. Aprenda os princípios SOLID, a separação de camadas e os padrões de implementação para aplicações sustentáveis.

A Clean Architecture, popularizada por Robert C. Martin (Uncle Bob), organiza o código colocando a lógica de negócio no centro da aplicação, independente de frameworks e detalhes de implementação. Essa abordagem arquitetural garante testabilidade, manutenibilidade e escalabilidade para aplicações .NET. Este guia apresenta uma implementação prática com ASP.NET Core.
Aplicações que misturam lógica de negócio com código de infraestrutura tornam-se rapidamente difíceis de manter. A Clean Architecture impõe uma separação rigorosa que permite alterar detalhes técnicos sem impactar o núcleo do negócio.
Princípios fundamentais da Clean Architecture
A Clean Architecture baseia-se na inversão de dependências: as camadas internas não conhecem as camadas externas. O domínio de negócio permanece isolado e pode evoluir independentemente das escolhas técnicas, como o framework web ou o banco de dados.
┌─────────────────────────────────────────────────────────┐
│ Presentation │
│ (Controllers, Razor Pages, Blazor, API Endpoints) │
├─────────────────────────────────────────────────────────┤
│ Infrastructure │
│ (EF Core, External APIs, File System, Email) │
├─────────────────────────────────────────────────────────┤
│ Application │
│ (Use Cases, Commands, Queries, DTOs) │
├─────────────────────────────────────────────────────────┤
│ Domain │
│ (Entities, Value Objects, Domain Services) │
└─────────────────────────────────────────────────────────┘As dependências sempre apontam para dentro: Presentation → Infrastructure → Application → Domain. O Domain não referencia nenhum outro projeto.
Estrutura do projeto .NET em Clean Architecture
A organização dos projetos reflete as diferentes camadas. Cada camada corresponde a um projeto distinto na solução do Visual Studio, garantindo uma separação física das responsabilidades.
# terminal
# Criar a estrutura da solução
dotnet new sln -n CleanArchitecture
# Criar projetos por camada
dotnet new classlib -n CleanArchitecture.Domain -o src/CleanArchitecture.Domain
dotnet new classlib -n CleanArchitecture.Application -o src/CleanArchitecture.Application
dotnet new classlib -n CleanArchitecture.Infrastructure -o src/CleanArchitecture.Infrastructure
dotnet new webapi -n CleanArchitecture.Api -o src/CleanArchitecture.Api
# Adicionar projetos à solução
dotnet sln add src/CleanArchitecture.Domain
dotnet sln add src/CleanArchitecture.Application
dotnet sln add src/CleanArchitecture.Infrastructure
dotnet sln add src/CleanArchitecture.Api
# Configurar referências entre projetos
cd src/CleanArchitecture.Application
dotnet add reference ../CleanArchitecture.Domain
cd ../CleanArchitecture.Infrastructure
dotnet add reference ../CleanArchitecture.Application
cd ../CleanArchitecture.Api
dotnet add reference ../CleanArchitecture.InfrastructureEssa estrutura garante que o Domain permaneça independente e que as dependências sigam a Dependency Rule da Clean Architecture.
A camada Domain: o núcleo do negócio
A camada Domain contém as entidades de negócio, os Value Objects e as interfaces dos repositórios. Nenhuma dependência externa é permitida nessa camada.
namespace CleanArchitecture.Domain.Entities;
// Entidade com identidade e ciclo de vida de negócio
public class Order
{
// Identificador único do pedido
public Guid Id { get; private set; }
// Referência ao cliente (Value Object para o e-mail)
public string CustomerEmail { get; private set; }
// Coleção de itens (relação um-para-muitos)
private readonly List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
// Status do pedido (enum de negócio)
public OrderStatus Status { get; private set; }
// Valor total calculado
public decimal TotalAmount => _items.Sum(i => i.TotalPrice);
// Datas de acompanhamento
public DateTime CreatedAt { get; private set; }
public DateTime? ShippedAt { get; private set; }
// Construtor privado força o uso do factory method
private Order() { }
// Factory method para criar um pedido válido
public static Order Create(string customerEmail)
{
// Validação das regras de negócio na criação
if (string.IsNullOrWhiteSpace(customerEmail))
throw new DomainException("O e-mail do cliente é obrigatório.");
if (!IsValidEmail(customerEmail))
throw new DomainException("Formato de e-mail inválido.");
return new Order
{
Id = Guid.NewGuid(),
CustomerEmail = customerEmail,
Status = OrderStatus.Pending,
CreatedAt = DateTime.UtcNow
};
}
// Método de negócio: adicionar um item
public void AddItem(Product product, int quantity)
{
// Regra de negócio: não é possível modificar um pedido enviado
if (Status == OrderStatus.Shipped)
throw new DomainException("Não é possível modificar um pedido já enviado.");
if (quantity <= 0)
throw new DomainException("A quantidade deve ser positiva.");
// Verificar se o produto já existe
var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id);
if (existingItem != null)
{
existingItem.IncreaseQuantity(quantity);
}
else
{
_items.Add(OrderItem.Create(this, product, quantity));
}
}
// Método de negócio: confirmar o pedido
public void Confirm()
{
if (Status != OrderStatus.Pending)
throw new DomainException("Apenas pedidos pendentes podem ser confirmados.");
if (!_items.Any())
throw new DomainException("Um pedido deve conter pelo menos um item.");
Status = OrderStatus.Confirmed;
}
// Método de negócio: enviar o pedido
public void Ship()
{
if (Status != OrderStatus.Confirmed)
throw new DomainException("O pedido deve ser confirmado antes do envio.");
Status = OrderStatus.Shipped;
ShippedAt = DateTime.UtcNow;
}
private static bool IsValidEmail(string email) =>
email.Contains('@') && email.Contains('.');
}A entidade Order encapsula as regras de negócio e protege seu estado interno. As modificações precisam passar por métodos de negócio que validam as invariantes.
namespace CleanArchitecture.Domain.Entities;
// Entidade filha com identidade própria
public class OrderItem
{
public Guid Id { get; private set; }
public Guid OrderId { get; private set; }
public Guid ProductId { get; private set; }
public string ProductName { get; private set; }
public decimal UnitPrice { get; private set; }
public int Quantity { get; private set; }
// Cálculo do preço total da linha
public decimal TotalPrice => UnitPrice * Quantity;
private OrderItem() { }
// Factory method com validação
public static OrderItem Create(Order order, Product product, int quantity)
{
return new OrderItem
{
Id = Guid.NewGuid(),
OrderId = order.Id,
ProductId = product.Id,
ProductName = product.Name,
UnitPrice = product.Price,
Quantity = quantity
};
}
// Método para aumentar a quantidade
public void IncreaseQuantity(int additionalQuantity)
{
if (additionalQuantity <= 0)
throw new DomainException("A quantidade adicional deve ser positiva.");
Quantity += additionalQuantity;
}
}namespace CleanArchitecture.Domain.Enums;
// Enumeração dos status do pedido
public enum OrderStatus
{
Pending = 0, // Aguardando confirmação
Confirmed = 1, // Confirmado, pronto para envio
Shipped = 2, // Enviado
Delivered = 3, // Entregue
Cancelled = 4 // Cancelado
}Value Objects como Money, Address ou Email encapsulam conceitos de negócio sem identidade própria. Sua igualdade é baseada nos valores, não em referências. Essa abordagem reforça a expressividade do domínio.
As interfaces Repository no Domain
As interfaces dos repositórios são definidas no Domain, mas suas implementações ficam no Infrastructure. Esse padrão respeita o princípio da inversão de dependências.
namespace CleanArchitecture.Domain.Interfaces;
// Interface do repositório Order
public interface IOrderRepository
{
// Recuperação por identificador
Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
// Recuperação com itens incluídos
Task<Order?> GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default);
// Recuperação pelo e-mail do cliente
Task<IEnumerable<Order>> GetByCustomerEmailAsync(
string email,
CancellationToken cancellationToken = default);
// Adicionar um novo pedido
Task AddAsync(Order order, CancellationToken cancellationToken = default);
// Atualizar um pedido existente
Task UpdateAsync(Order order, CancellationToken cancellationToken = default);
// Excluir um pedido
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
}namespace CleanArchitecture.Domain.Interfaces;
// Padrão Unit of Work para gerenciamento transacional
public interface IUnitOfWork : IDisposable
{
// Repositórios acessíveis via UoW
IOrderRepository Orders { get; }
IProductRepository Products { get; }
// Salvamento atômico de todas as alterações
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
// Gerenciamento explícito de transações
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
Task CommitTransactionAsync(CancellationToken cancellationToken = default);
Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
}O padrão Unit of Work coordena as operações em múltiplos repositórios dentro de uma única transação.
Pronto para mandar bem nas entrevistas de .NET?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
A camada Application: orquestração dos Use Cases
A camada Application contém a lógica aplicativa (use cases), os DTOs e as interfaces dos serviços externos. Ela orquestra as interações entre o domínio e o mundo externo.
using MediatR;
namespace CleanArchitecture.Application.Orders.Commands;
// Command que representa a intenção de criar um pedido
public record CreateOrderCommand(
string CustomerEmail,
List<OrderItemDto> Items
) : IRequest<Guid>;
// DTO para os itens do pedido
public record OrderItemDto(
Guid ProductId,
int Quantity
);using MediatR;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces;
namespace CleanArchitecture.Application.Orders.Commands;
// Handler que implementa o caso de uso
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly IUnitOfWork _unitOfWork;
public CreateOrderCommandHandler(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<Guid> Handle(
CreateOrderCommand request,
CancellationToken cancellationToken)
{
// Criar a entidade Order via factory method
var order = Order.Create(request.CustomerEmail);
// Adicionar os itens ao pedido
foreach (var item in request.Items)
{
// Recuperar o produto do repositório
var product = await _unitOfWork.Products
.GetByIdAsync(item.ProductId, cancellationToken);
if (product == null)
throw new NotFoundException($"Produto {item.ProductId} não encontrado.");
// Usar o método de negócio da entidade
order.AddItem(product, item.Quantity);
}
// Persistir via repositório
await _unitOfWork.Orders.AddAsync(order, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return order.Id;
}
}O MediatR implementa o padrão Mediator para desacoplar handlers dos controllers. Cada command tem um único handler responsável por seu processamento.
using MediatR;
namespace CleanArchitecture.Application.Orders.Queries;
// Query para recuperar um pedido pelo ID
public record GetOrderByIdQuery(Guid OrderId) : IRequest<OrderDto?>;
// DTO de resposta para um pedido
public record OrderDto(
Guid Id,
string CustomerEmail,
string Status,
decimal TotalAmount,
DateTime CreatedAt,
DateTime? ShippedAt,
List<OrderItemResponseDto> Items
);
// DTO para os itens na resposta
public record OrderItemResponseDto(
Guid Id,
string ProductName,
decimal UnitPrice,
int Quantity,
decimal TotalPrice
);using MediatR;
using CleanArchitecture.Domain.Interfaces;
namespace CleanArchitecture.Application.Orders.Queries;
public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, OrderDto?>
{
private readonly IOrderRepository _orderRepository;
public GetOrderByIdQueryHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<OrderDto?> Handle(
GetOrderByIdQuery request,
CancellationToken cancellationToken)
{
// Recuperar com os itens incluídos
var order = await _orderRepository
.GetByIdWithItemsAsync(request.OrderId, cancellationToken);
if (order == null)
return null;
// Mapeamento para DTO (projeção manual)
return new OrderDto(
Id: order.Id,
CustomerEmail: order.CustomerEmail,
Status: order.Status.ToString(),
TotalAmount: order.TotalAmount,
CreatedAt: order.CreatedAt,
ShippedAt: order.ShippedAt,
Items: order.Items.Select(i => new OrderItemResponseDto(
Id: i.Id,
ProductName: i.ProductName,
UnitPrice: i.UnitPrice,
Quantity: i.Quantity,
TotalPrice: i.TotalPrice
)).ToList()
);
}
}A separação Commands/Queries segue o padrão CQRS (Command Query Responsibility Segregation), otimizando leituras e escritas de forma independente.
Validação com FluentValidation
A validação dos commands ocorre na camada Application, antes da execução do handler.
using FluentValidation;
using CleanArchitecture.Application.Orders.Commands;
namespace CleanArchitecture.Application.Orders.Validators;
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
// E-mail obrigatório e formato válido
RuleFor(x => x.CustomerEmail)
.NotEmpty().WithMessage("O e-mail do cliente é obrigatório.")
.EmailAddress().WithMessage("Formato de e-mail inválido.");
// Pelo menos um item obrigatório
RuleFor(x => x.Items)
.NotEmpty().WithMessage("O pedido deve conter pelo menos um item.");
// Validação de cada item
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(i => i.ProductId)
.NotEmpty().WithMessage("O identificador do produto é obrigatório.");
item.RuleFor(i => i.Quantity)
.GreaterThan(0).WithMessage("A quantidade deve ser positiva.")
.LessThanOrEqualTo(100).WithMessage("Quantidade máxima: 100.");
});
}
}using FluentValidation;
using MediatR;
namespace CleanArchitecture.Application.Common.Behaviors;
// Pipeline behavior para validação automática
public class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
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)
{
// Se não houver validadores, continuar
if (!_validators.Any())
return await next();
// Executar todos os validadores
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
// Agregar erros
var failures = validationResults
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
// Lançar exceção se houver erros
if (failures.Any())
throw new ValidationException(failures);
return await next();
}
}O ValidationBehavior é executado automaticamente antes de cada handler, garantindo que apenas commands válidos cheguem à lógica de negócio.
A camada Infrastructure: implementação técnica
A camada Infrastructure fornece as implementações concretas das interfaces definidas em Domain e Application.
using Microsoft.EntityFrameworkCore;
using CleanArchitecture.Domain.Entities;
namespace CleanArchitecture.Infrastructure.Persistence;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<Order> Orders => Set<Order>();
public DbSet<OrderItem> OrderItems => Set<OrderItem>();
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Aplicar configurações do assembly
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using CleanArchitecture.Domain.Entities;
namespace CleanArchitecture.Infrastructure.Persistence.Configurations;
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
// Tabela e chave primária
builder.ToTable("Orders");
builder.HasKey(o => o.Id);
// Propriedades
builder.Property(o => o.CustomerEmail)
.IsRequired()
.HasMaxLength(256);
builder.Property(o => o.Status)
.IsRequired()
.HasConversion<string>(); // Armazenar como string legível
// Relação com OrderItems
builder.HasMany(o => o.Items)
.WithOne()
.HasForeignKey(i => i.OrderId)
.OnDelete(DeleteBehavior.Cascade);
// Índice para busca por e-mail
builder.HasIndex(o => o.CustomerEmail);
// Acessar o campo privado _items
builder.Navigation(o => o.Items)
.UsePropertyAccessMode(PropertyAccessMode.Field);
}
}using Microsoft.EntityFrameworkCore;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces;
using CleanArchitecture.Infrastructure.Persistence;
namespace CleanArchitecture.Infrastructure.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
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task<Order?> GetByIdWithItemsAsync(
Guid id,
CancellationToken cancellationToken = default)
{
// Eager loading dos itens
return await _context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task<IEnumerable<Order>> GetByCustomerEmailAsync(
string email,
CancellationToken cancellationToken = default)
{
return await _context.Orders
.Include(o => o.Items)
.Where(o => o.CustomerEmail == email)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(cancellationToken);
}
public async Task AddAsync(
Order order,
CancellationToken cancellationToken = default)
{
await _context.Orders.AddAsync(order, cancellationToken);
}
public Task UpdateAsync(
Order order,
CancellationToken cancellationToken = default)
{
// O EF Core rastreia automaticamente as mudanças
_context.Orders.Update(order);
return Task.CompletedTask;
}
public async Task DeleteAsync(
Guid id,
CancellationToken cancellationToken = default)
{
await _context.Orders
.Where(o => o.Id == id)
.ExecuteDeleteAsync(cancellationToken);
}
}Os repositórios não devem expor IQueryable diretamente, pois isso criaria uma dependência ao EF Core nas camadas superiores. Prefira métodos específicos com parâmetros claros.
Implementação do Unit of Work
using Microsoft.EntityFrameworkCore.Storage;
using CleanArchitecture.Domain.Interfaces;
using CleanArchitecture.Infrastructure.Repositories;
namespace CleanArchitecture.Infrastructure.Persistence;
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
private IDbContextTransaction? _transaction;
// Lazy loading dos repositórios
private IOrderRepository? _orderRepository;
private IProductRepository? _productRepository;
public UnitOfWork(AppDbContext context)
{
_context = context;
}
// Criação sob demanda (lazy initialization)
public IOrderRepository Orders =>
_orderRepository ??= new OrderRepository(_context);
public IProductRepository Products =>
_productRepository ??= new ProductRepository(_context);
public async Task<int> SaveChangesAsync(
CancellationToken cancellationToken = default)
{
return await _context.SaveChangesAsync(cancellationToken);
}
public async Task BeginTransactionAsync(
CancellationToken cancellationToken = default)
{
_transaction = await _context.Database
.BeginTransactionAsync(cancellationToken);
}
public async Task CommitTransactionAsync(
CancellationToken cancellationToken = default)
{
if (_transaction == null)
throw new InvalidOperationException("Nenhuma transação ativa.");
await _transaction.CommitAsync(cancellationToken);
await _transaction.DisposeAsync();
_transaction = null;
}
public async Task RollbackTransactionAsync(
CancellationToken cancellationToken = default)
{
if (_transaction == null)
throw new InvalidOperationException("Nenhuma transação ativa.");
await _transaction.RollbackAsync(cancellationToken);
await _transaction.DisposeAsync();
_transaction = null;
}
public void Dispose()
{
_transaction?.Dispose();
_context.Dispose();
}
}Configuração da injeção de dependências
O registro dos serviços ocorre em cada camada por meio de extension methods, depois orquestrados em Program.cs.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using CleanArchitecture.Domain.Interfaces;
using CleanArchitecture.Infrastructure.Persistence;
using CleanArchitecture.Infrastructure.Repositories;
namespace CleanArchitecture.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// Configuração do Entity Framework
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName)));
// Registrar Unit of Work (Scoped)
services.AddScoped<IUnitOfWork, UnitOfWork>();
// Repositórios individuais se necessário
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IProductRepository, ProductRepository>();
return services;
}
}using FluentValidation;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using CleanArchitecture.Application.Common.Behaviors;
namespace CleanArchitecture.Application;
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
var assembly = typeof(DependencyInjection).Assembly;
// Registrar MediatR com os handlers
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(assembly));
// Registrar os validadores do FluentValidation
services.AddValidatorsFromAssembly(assembly);
// Pipeline behaviors (a ordem de execução importa)
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
return services;
}
}using CleanArchitecture.Application;
using CleanArchitecture.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
// Registrar as camadas
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);
// Configuração da API
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();A camada Presentation: controllers da API
Os controllers são adaptadores simples que delegam o trabalho ao MediatR.
using MediatR;
using Microsoft.AspNetCore.Mvc;
using CleanArchitecture.Application.Orders.Commands;
using CleanArchitecture.Application.Orders.Queries;
namespace CleanArchitecture.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
public OrdersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
[ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(
[FromBody] CreateOrderCommand command,
CancellationToken cancellationToken)
{
// Delegação completa ao MediatR
var orderId = await _mediator.Send(command, cancellationToken);
return CreatedAtAction(
nameof(GetById),
new { id = orderId },
orderId);
}
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(
Guid id,
CancellationToken cancellationToken)
{
var order = await _mediator.Send(
new GetOrderByIdQuery(id),
cancellationToken);
if (order == null)
return NotFound();
return Ok(order);
}
[HttpPost("{id:guid}/confirm")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Confirm(
Guid id,
CancellationToken cancellationToken)
{
await _mediator.Send(new ConfirmOrderCommand(id), cancellationToken);
return NoContent();
}
[HttpPost("{id:guid}/ship")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Ship(
Guid id,
CancellationToken cancellationToken)
{
await _mediator.Send(new ShipOrderCommand(id), cancellationToken);
return NoContent();
}
}Os controllers permanecem leves e não contêm lógica de negócio, apenas o mapeamento HTTP.
Testes unitários da camada Application
A Clean Architecture facilita os testes unitários por meio do isolamento de dependências.
using Moq;
using Xunit;
using CleanArchitecture.Application.Orders.Commands;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces;
namespace CleanArchitecture.Tests.Application;
public class CreateOrderCommandHandlerTests
{
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
private readonly CreateOrderCommandHandler _handler;
public CreateOrderCommandHandlerTests()
{
_unitOfWorkMock = new Mock<IUnitOfWork>();
_handler = new CreateOrderCommandHandler(_unitOfWorkMock.Object);
}
[Fact]
public async Task Handle_ValidCommand_CreatesOrderAndReturnsId()
{
// Arrange
var product = new Product { Id = Guid.NewGuid(), Name = "Test", Price = 100 };
_unitOfWorkMock.Setup(x => x.Products.GetByIdAsync(
It.IsAny<Guid>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(product);
_unitOfWorkMock.Setup(x => x.Orders.AddAsync(
It.IsAny<Order>(),
It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
_unitOfWorkMock.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(1);
var command = new CreateOrderCommand(
CustomerEmail: "test@example.com",
Items: new List<OrderItemDto>
{
new(ProductId: product.Id, Quantity: 2)
});
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
Assert.NotEqual(Guid.Empty, result);
_unitOfWorkMock.Verify(x => x.Orders.AddAsync(
It.Is<Order>(o => o.CustomerEmail == "test@example.com"),
It.IsAny<CancellationToken>()), Times.Once);
_unitOfWorkMock.Verify(x => x.SaveChangesAsync(
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Handle_ProductNotFound_ThrowsNotFoundException()
{
// Arrange
_unitOfWorkMock.Setup(x => x.Products.GetByIdAsync(
It.IsAny<Guid>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((Product?)null);
var command = new CreateOrderCommand(
CustomerEmail: "test@example.com",
Items: new List<OrderItemDto>
{
new(ProductId: Guid.NewGuid(), Quantity: 1)
});
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(
() => _handler.Handle(command, CancellationToken.None));
}
}Os mocks do Moq permitem isolar o handler de suas dependências, testando apenas a lógica de orquestração.
Conclusão
A Clean Architecture com .NET fornece uma estrutura robusta para aplicações empresariais. A separação rigorosa de responsabilidades entre as camadas Domain, Application, Infrastructure e Presentation garante código sustentável e testável a longo prazo.
Checklist Clean Architecture .NET
- ✅ Domain isolado, sem dependências externas
- ✅ Entidades com lógica de negócio encapsulada
- ✅ Interfaces de repositórios no Domain
- ✅ Use Cases via Commands/Queries (CQRS)
- ✅ Validação com FluentValidation e Pipeline Behaviors
- ✅ Infrastructure implementa as interfaces do Domain
- ✅ Controllers leves delegando ao MediatR
- ✅ Testes unitários isolados com mocks
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
O investimento inicial nessa arquitetura compensa rapidamente em projetos médios e grandes, em que mudanças de requisitos técnicos (migração de banco de dados, troca de framework) impactam apenas as camadas externas, preservando o núcleo do negócio intacto.
Tags
Compartilhar
Artigos relacionados

Perguntas de Entrevista C# e .NET: Guia Completo 2026
As 25 perguntas mais comuns em entrevistas de C# e .NET. LINQ, async/await, injeção de dependência, Entity Framework e boas práticas com respostas detalhadas.

.NET 8: Construindo uma API REST com ASP.NET Core
Guia completo para construir uma API REST profissional com .NET 8 e ASP.NET Core. Controllers, Entity Framework Core, validacao e boas praticas explicadas passo a passo.

Entity Framework Core: Otimização de Performance e Boas Práticas em 2026
Guia completo de otimização de performance com Entity Framework Core 10 no .NET 10. AsNoTracking, queries compiladas, atualizações em lote, split queries e LeftJoin.