Este artigo é baseado nas ideias centrais do The Clean Arch, nosso projeto open source de referência para implementação de Clean Architecture em .NET. Você encontra o projeto também na seção Open Source do nosso site.
Quando começamos um projeto Blazor Server, a tentação de colocar tudo no mesmo lugar é grande. Models, regras de negócio, acesso a banco e componentes de UI convivem no mesmo projeto, no mesmo namespace, às vezes no mesmo arquivo. Funciona para um MVP. Depois de seis meses, vira pesadelo.
A Clean Architecture não é sobre seguir um dogma. É sobre separar o que muda frequentemente do que muda raramente. E numa aplicação Blazor, isso importa muito.
Screaming Architecture: o projeto fala por si
Antes de falar em camadas, há um princípio anterior: a estrutura de diretórios e namespaces deve comunicar o domínio do negócio, não o framework utilizado. Se ao abrir a solução a primeira coisa que você vê é Controllers/, Pages/, Migrations/ — a solução está gritando "ASP.NET" em vez de gritar o problema que resolve.
O The Clean Arch chama isso de Screaming Architecture. A estrutura certa grita Contabilidade, Confeitaria, Leilões — o domínio do negócio em primeiro lugar.
As camadas e o que vai em cada uma
O The Clean Arch organiza a solução em dois grandes grupos: Business e InterfaceAdapters.
Business/Entities — o núcleo da aplicação. Entidades, objetos de valor, regras de negócio puras. Sem dependência de framework, sem referência ao ASP.NET, sem EF Core. Apenas C# puro. Em projetos que seguem Domain-Driven Design, essa camada costuma ser chamada de Domain. Se você consegue testar tudo aqui sem inicializar nada do ASP.NET, está no caminho certo.
Business/UseCases — casos de uso da aplicação. Aqui ficam os handlers que orquestram as entidades para realizar uma operação específica. Um CriarPedidoHandler que recebe um input, executa a lógica de negócio e retorna um resultado. Ainda sem dependência de infraestrutura — apenas ports (interfaces que definem o contrato de comunicação com o mundo externo).
InterfaceAdapters — implementações concretas que conectam o núcleo ao mundo externo. Essa camada se divide em subcategorias:
Data.*— repositórios, EF Core, acesso a banco de dadosUI.*— interfaces de usuário: WebApp (Blazor), WebApi, CLI, mobileGateways.*— clientes de APIs externas: pagamento, mapas, e-mailWorkers.*— serviços de background: filas, processamento assíncrono
O componente Blazor vive em InterfaceAdapters/UI.WebApp — ele é apenas um dos possíveis adapters de interface. O núcleo do negócio não sabe que existe Blazor.
Ports e Adapters — a comunicação entre camadas
A imagem do The Clean Arch combina dois padrões: Onion Architecture (camadas concêntricas com dependência apontando para dentro) e Hexagonal Architecture (comunicação entre camadas via ports).
Dois tipos de seta definem como as camadas interagem:
- Seta sólida — dependência direta: camadas externas podem importar tipos de camadas internas
- Seta pontilhada — comunicação indireta: quando a camada interna precisa "chamar para fora" (ex.: persistir dados), ela não importa o tipo concreto; ela define uma interface — o port — e quem implementa é o adapter na camada externa
Na prática, um caso de uso define um inbound port — o contrato que o mundo externo usa para acioná-lo — e pode declarar outbound ports — interfaces como IPedidoRepository que a camada de dados implementa.
O The Clean Arch fornece as abstrações IInboundPort e InboundPortHandler<TInput, TOutput> para estruturar isso de forma consistente:
// Business/UseCases — define o port de entrada e o handler
public record CriarPedidoInput(Guid ClienteId, List<ItemInput> Itens);
public record CriarPedidoOutput(Guid PedidoId);
public class CriarPedidoHandler : InboundPortHandler<CriarPedidoInput, CriarPedidoOutput>
{
private readonly IPedidoRepository _pedidos;
private readonly TimeProvider _time;
public CriarPedidoHandler(IPedidoRepository pedidos, TimeProvider time)
{
_pedidos = pedidos;
_time = time;
}
public override async Task<CriarPedidoOutput> HandleAsync(CriarPedidoInput input)
{
var pedido = new Pedido(input.ClienteId, _time.GetUtcNow());
// ... lógica de negócio
await _pedidos.SalvarAsync(pedido);
return new CriarPedidoOutput(pedido.Id);
}
}
// Business/UseCases — outbound port (interface)
public interface IPedidoRepository
{
Task SalvarAsync(Pedido pedido);
}
// InterfaceAdapters/Data.EFCore — implementa o port
public class PedidoRepositoryEFCore : IPedidoRepository
{
private readonly AppDbContext _db;
public PedidoRepositoryEFCore(AppDbContext db) => _db = db;
public async Task SalvarAsync(Pedido pedido)
{
_db.Pedidos.Add(pedido);
await _db.SaveChangesAsync();
}
}
O handler não sabe que existe EF Core. O repositório não sabe que existe Blazor. Cada peça depende apenas do contrato.
Por que isso importa no Blazor?
Blazor Server tem uma peculiaridade: os componentes são stateful. O @code { } de um componente é uma instância C# que vive enquanto o circuito SignalR existe. Isso cria uma tentação de colocar lógica de negócio diretamente nos componentes — afinal, o estado está ali, acessível.
O problema aparece na hora de testar. Componentes com regra de negócio são difíceis de testar unitariamente. Com bunit, você consegue renderizar o componente — mas testar a lógica de negócio embutida nele é mais trabalhoso do que precisa ser.
Quando o componente apenas aciona um inbound port, você testa o handler com um teste de unidade simples (sem bunit) e testa o componente com bunit verificando só o comportamento de UI.
Injeção de dependência como cola
O mecanismo que une as camadas no Blazor é a injeção de dependência nativa do ASP.NET Core. Em Program.cs, você registra as implementações dos ports. Nos componentes, você injeta o handler.
// Program.cs — InterfaceAdapters/UI.WebApp
builder.Services.AddScoped<CriarPedidoHandler>();
builder.Services.AddScoped<IPedidoRepository, PedidoRepositoryEFCore>();
builder.Services.AddSingleton(TimeProvider.System);
@* Componente Blazor — InterfaceAdapters/UI.WebApp *@
@inject CriarPedidoHandler CriarPedido
<button @onclick="HandleSubmit">Criar Pedido</button>
@code {
private async Task HandleSubmit()
{
await CriarPedido.HandleAsync(new CriarPedidoInput(...));
}
}
O componente não sabe que existe EF Core. Não sabe que existe banco de dados. Em testes, você substitui IPedidoRepository por um mock e injeta um FakeTimeProvider. Pronto — handler e componente testáveis de forma independente.
O que não fazer
Não coloque DbContext diretamente em componentes Blazor. O DbContext tem lifetime scoped, e o ciclo de vida de um componente Blazor Server é mais longo do que um request HTTP. Isso cria problemas sutis com change tracking e conexões de banco.
Não use repositórios concretos, sempre interfaces (outbound ports). Isso permite trocar a implementação sem mexer nos consumidores e facilita testes.
Não coloque validação de negócio em componentes. Validação de formato (campo obrigatório, tamanho máximo) pode ficar no componente. Validação de negócio (pedido mínimo de R$50, cliente com limite atingido) fica nas entidades ou no handler.
Não use DateTime.UtcNow diretamente em entidades ou handlers. Injete TimeProvider e use timeProvider.GetUtcNow(). Isso torna qualquer lógica dependente de data/hora testável sem manipulação de relógio real.
Estrutura de projeto na prática
Seguindo as convenções do The Clean Arch, a estrutura de uma solução Blazor fica assim:
src/
Business/
Entities/ ← MinhaApp.Business.Entities.csproj
UseCases/ ← MinhaApp.Business.UseCases.csproj
InterfaceAdapters/
Data.EFCore/ ← MinhaApp.InterfaceAdapters.Data.EFCore.csproj
UI.WebApp/ ← MinhaApp.InterfaceAdapters.UI.WebApp.csproj
test/
Business/
EntitiesUnitTests/ ← MinhaApp.Business.EntitiesUnitTests.csproj
UseCasesUnitTests/ ← MinhaApp.Business.UseCasesUnitTests.csproj
InterfaceAdapters/
UI.WebAppUnitTests/ ← MinhaApp.InterfaceAdapters.UI.WebAppUnitTests.csproj
A convenção de nomes é [Prefixo].[Categoria].[Componente]. Testes seguem o nome do componente com sufixo UnitTests (sem ponto separador).
Se o projeto não tiver entidades complexas e puder dispensar a separação de Entities e UseCases, as duas pastas podem ser combinadas em Business.UseCases/. O The Clean Arch permite essa simplificação quando há apenas um componente por categoria.
A regra de ouro

A dependência sempre aponta para dentro — e há dois modos de cruzar a fronteira entre camadas:
Direto (seta sólida): camadas externas importam e instanciam tipos de camadas internas. Um componente Blazor pode criar diretamente um CriarPedidoInput (record definido em UseCases) sem quebrar nenhuma regra.
Indireto (seta pontilhada): quando a camada interna precisa de algo da camada externa (persistência, e-mail, tempo), ela define uma interface no próprio projeto — o outbound port — e a camada externa implementa esse contrato. As entidades e handlers nunca importam nada de InterfaceAdapters. Quem depende de quem é sempre a camada de fora dependendo da de dentro, nunca o contrário.
Se você está numa reunião de arquitetura e alguém sugere que um handler vai importar o DbContext "só dessa vez", é hora de pausar a reunião.
Clean Architecture não é burocracia. É a diferença entre um codebase que você consegue evoluir daqui a dois anos e um que você vai reescrever.