Quando o projeto vira uma bagunça
Você já abriu um projeto C# escrito há um ano e percebeu com horror que nada pode ser alterado sem o risco de quebrar tudo? A lógica de negócios está fortemente acoplada ao Entity Framework, os controladores ASP.NET guardam segredos do banco de dados, e os testes unitários se transformam em uma integração infernal com o banco real. Familiar?
Este é o sintoma clássico da falta de arquitetura. A boa notícia: existe uma receita comprovada — a Arquitetura Hexagonal (também conhecida como Ports & Adapters). Este padrão, criado por Alistair Cockburn, não é apenas um termo da moda. É uma salvação para projetos de médio e grande porte em C#. Vamos entender como funciona e já escrever código.
A essência da Arquitetura Hexagonal: núcleo e periferia
Imagine um hexágono. No centro está sua aplicação com lógica de negócios pura, escrita em C# puro, sem dependências externas. Nas bordas estão as portas (interfaces), através das quais o núcleo se comunica com o mundo externo. Os adaptadores são as implementações concretas: controladores ASP.NET, repositórios EF Core, clientes RabbitMQ.
A regra principal: o núcleo não sabe nada sobre o mundo externo. Ele conhece apenas suas portas. Isso permite trocar o banco de dados, a interface do usuário ou as filas de mensagens sem alterar a lógica de negócios.
Por que isso é importante para o desenvolvedor C#?
- Testabilidade: A lógica de negócios pode ser testada sem banco de dados, HTTP ou sistema de arquivos.
- Flexibilidade: Quer trocar PostgreSQL por MongoDB? Basta alterar o adaptador.
- Legibilidade: O código se torna semelhante a uma especificação de requisitos, e não a um emaranhado de dependências.
- Conformidade com SOLID: Especialmente o princípio da inversão de dependência (Dependency Inversion).
Estrutura do projeto em C#
Na Arquitetura Hexagonal, geralmente distinguimos três camadas principais. Para C#, é conveniente organizá-las como projetos separados em uma mesma solução:
MyApp.sln├── MyApp.Domain // Núcleo: entidades, value objects, portas (interfaces)├── MyApp.Application // Casos de uso (serviços da aplicação)├── MyApp.Infrastructure // Adaptadores: EF Core, APIs externas, logging└── MyApp.Api // Adaptador de entrada: ASP.NET Controllers / Minimal APIObserve: Domain e Application são bibliotecas de classes puras, sem referências a Microsoft.AspNetCore ou EntityFramework. Apenas System e System.Threading.Tasks.
Escrevendo código: exemplo real
Suponha que estamos criando um sistema de pedidos. Precisamos criar um pedido e salvá-lo.
Passo 1. Núcleo (Domain)
Aqui vivem as entidades e as portas — interfaces que definem como o núcleo se comunica com o mundo.
// MyApp.Domain/Entities/Order.cspublic class Order{ public Guid Id { get; private set; } public string CustomerName { get; private set; } public decimal TotalAmount { get; private set; } public DateTime CreatedAt { get; private set; }
public Order(string customerName, decimal totalAmount) { Id = Guid.NewGuid(); CustomerName = customerName; TotalAmount = totalAmount; CreatedAt = DateTime.UtcNow; }}
// MyApp.Domain/Ports/IOrderRepository.cs (Porta de saída)public interface IOrderRepository{ Task<Order> GetByIdAsync(Guid id); Task AddAsync(Order order);}
// MyApp.Domain/Ports/IOrderNotifier.cs (Porta de saída — notificações)public interface IOrderNotifier{ Task NotifyOrderCreatedAsync(Order order);}Observe: IOrderRepository e IOrderNotifier são as portas. O núcleo define o que ele quer, mas não como isso é implementado.