Arquitetura Limpa e sua Estrutura de Pastas em C#
arquitetura limpa é uma abordagem para estruturar sua aplicação em camadas que separam as diferentes responsabilidades do sistema. Ela foi proposta por Robert C. Martin, também conhecido como Uncle Bob, e tem como objetivo tornar o código mais fácil de entender, testar, manter e evoluir.
A ideia principal da arquitetura limpa é que as dependências do código devem apontar para dentro, ou seja, as camadas mais internas não devem saber nada sobre as camadas mais externas. Isso permite que o núcleo do sistema, que contém as regras de negócio e de aplicação, fique isolado de detalhes de infraestrutura, como banco de dados, frameworks, UI, etc.
Neste artigo, vamos ver como podemos aplicar a arquitetura limpa em um projeto .NET Core, usando C# como linguagem de programação. Vamos criar uma solução com quatro projetos, cada um representando uma camada da arquitetura:
- Domain: contém as entidades, objetos de valor, eventos de domínio, exceções e interfaces de repositório. É a camada mais interna e não depende de nenhuma outra.
- Application: contém os casos de uso, serviços, comandos, consultas e eventos da aplicação. É a camada que orquestra as regras de domínio e depende apenas da camada de domínio.
- Infrastructure: contém as implementações concretas das interfaces definidas nas camadas internas, como repositórios, serviços de email, mensageria, etc. É a camada que lida com os detalhes de infraestrutura e depende das camadas internas.
- Presentation: contém os controladores que definem os endpoints da API do sistema. É a camada que lida com a interação com o usuário e depende das camadas internas.
Vamos ver como podemos criar essa estrutura de pastas usando um template do .NET Core.
Criando a solução Para criar a solução usando a arquitetura limpa, vamos usar um template disponível no GitHub1. Esse template já vem com alguns exemplos de código e testes para nos ajudar a entender melhor como funciona a arquitetura.
Para instalar o template, precisamos executar o seguinte comando no terminal:
dotnet new -i Ardalis.CleanArchitecture.Template
Depois de instalado, podemos navegar até o diretório onde queremos criar a solução e executar o seguinte comando:
dotnet new clean-arch -o NomeDoProjeto
Isso vai criar uma pasta chamada NomeDoProjeto com a seguinte estrutura:
📁 NomeDoProjeto
|__ 📁 src
|__ 📁 Application
|__ 📁 Domain
|__ 📁 Infrastructure
|__ 📁 WebUI
|__ 📁 tests
|__ 📁 Application.UnitTests
|__ 📁 Domain.UnitTests
|__ 📁 Infrastructure.IntegrationTests
|__ 📁 WebUI.IntegrationTests
|__ #️⃣ NomeDoProjeto.sln
Podemos ver que cada projeto corresponde a uma camada da arquitetura e que há também projetos de teste para cada camada. Além disso, há um arquivo .sln que representa a solução completa.
Para abrir a solução no Visual Studio Code, podemos executar o seguinte comando na pasta raiz:
code .
Isso vai abrir o VS Code com todos os projetos carregados. Podemos ver também que há alguns arquivos README.md em cada projeto, explicando o propósito e o conteúdo de cada um.
Vamos analisar cada projeto em mais detalhes.
Projeto Domain O projeto Domain é o mais simples e contém apenas uma classe chamada ToDoItem.cs na pasta Entities. Essa classe representa uma entidade do nosso domínio, que é uma tarefa a ser feita. Ela tem as seguintes propriedades:
- Id: um identificador único do tipo Guid.
- Title: um título descritivo do tipo string.
- Description: uma descrição opcional do tipo string.
- Done: um indicador se a tarefa foi concluída ou não do tipo bool.
- Priority: uma enumeração que indica a prioridade da tarefa, podendo ser Low, Medium ou High.
- Created: uma data e hora que indica quando a tarefa foi criada do tipo DateTime.
Além disso, a classe tem um método chamado MarkComplete, que marca a tarefa como concluída e atualiza a propriedade Done.
O projeto Domain também contém uma interface chamada IToDoItemRepository.cs na pasta Repositories. Essa interface define os métodos que um repositório de tarefas deve implementar, como:
- ListAsync: retorna uma lista de todas as tarefas do tipo IReadOnlyList<ToDoItem>.
- AddAsync: adiciona uma nova tarefa do tipo ToDoItem e retorna o Id gerado do tipo Guid.
- GetByIdAsync: retorna uma tarefa pelo Id do tipo ToDoItem.
- UpdateAsync: atualiza uma tarefa existente do tipo ToDoItem.
- DeleteAsync: deleta uma tarefa pelo Id do tipo bool.
Essa interface é usada pela camada de aplicação para abstrair os detalhes de persistência das tarefas. A implementação concreta dessa interface fica na camada de infraestrutura.
O projeto Domain não tem nenhuma dependência externa, apenas o pacote Microsoft.NETCore.App.
Projeto Application O projeto Application é o mais complexo e contém vários arquivos e pastas. Vamos ver cada um deles.
Na pasta Behaviors, há duas classes chamadas LoggingBehavior.cs e PerformanceBehavior.cs. Essas classes são exemplos de middlewares que interceptam os pedidos da camada de apresentação e executam alguma lógica antes ou depois dos casos de uso. Por exemplo, o LoggingBehavior registra os detalhes do pedido e da resposta no console, e o PerformanceBehavior mede o tempo de execução do caso de uso e registra um aviso se for maior que um limite definido.
Na pasta Common, há duas classes chamadas AuditableEntity.cs e BaseEntity.cs. Essas classes são usadas como classes base para as entidades do domínio, adicionando algumas propriedades comuns, como Id, CreatedBy, Created, LastModifiedBy e LastModified. Essas propriedades são usadas para rastrear as alterações nas entidades e podem ser preenchidas automaticamente usando o EF Core.
Na pasta Contracts, há duas interfaces chamadas ICurrentUserService.cs e IDateTime.cs. Essas interfaces são usadas para abstrair os serviços externos que a camada de aplicação precisa usar, como obter o usuário atual ou a data e hora atual. As implementações dessas interfaces ficam na camada de infraestrutura.
Na pasta Exceptions, há uma classe chamada NotFoundException.cs. Essa classe é usada para representar uma exceção personalizada que indica que um recurso não foi encontrado. Ela recebe um nome e um Id do recurso e constrói uma mensagem adequada.
Na pasta Features, há duas pastas chamadas ToDoItems e WeatherForecast. Cada pasta representa um recurso da aplicação e contém os casos de uso relacionados a ele. Cada caso de uso é representado por três arquivos: um comando ou consulta, um manipulador e um modelo de resposta.
Por exemplo, na pasta ToDoItems há o caso de uso CreateToDoItem. Ele é composto pelos seguintes arquivos:
- CreateToDoItemCommand.cs: define o comando para criar uma nova tarefa, contendo as propriedades Title, Description e Priority.
- CreateToDoItemCommandHandler.cs: define o manipulador do comando, que recebe uma instância do comando e uma instância do repositório de tarefas. Ele valida o comando, cria uma nova entidade ToDoItem, adiciona ao repositório e retorna o Id gerado.
- CreateToDoItemCommandResponse.cs: define o modelo de resposta do comando, contendo apenas a propriedade Id.
Os outros casos de uso seguem a mesma estrutura, mas com diferentes comandos, consultas, manipuladores e modelos de resposta.
O projeto Application depende apenas do projeto Domain e dos pacotes MediatR (para implementar o padrão mediator), FluentValidation (para validar os comandos e consultas) e Microsoft.NETCore.App.
Projeto Infrastructure O projeto Infrastructure é o responsável por implementar as interfaces definidas nas camadas internas e lidar com os detalhes de infraestrutura. Ele contém os seguintes arquivos e pastas:
Na pasta Data, há duas classes chamadas ApplicationDbContext.cs e ApplicationDbContextFactory.cs. A primeira classe representa o contexto do banco de dados usando o EF Core. Ela herda da classe IdentityDbContext para usar a autenticação baseada em identidade do ASP.NET Core. Ela também implementa a interface IApplicationDbContext para expor os DbSets das entidades do domínio.