Clean Architecture com Blazor: organizando sua solução .NET

Banner do artigo: Clean Architecture com Blazor: organizando sua solução .NET

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 dados
  • UI.* — interfaces de usuário: WebApp (Blazor), WebApi, CLI, mobile
  • Gateways.* — clientes de APIs externas: pagamento, mapas, e-mail
  • Workers.* — 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

The Clean Arch — camadas concêntricas com seta sólida (dependência direta) e seta pontilhada (comunicação indireta via port)

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.

← Ver todos os artigos
Ocorreu um erro inesperado. Recarregar 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.