ASP.NET Core Web API - Clean Architecture - II


 Hoje vamos criar um projeto ASP.NET Core Web API aplicando o padrão de arquitetura Clean Architecture.

Continuando o artigo anterior vamos implementar agora os recursos de infraestrutura no projeto Persistence da pasta Infrastructure.

Camada Infrastructure

A camada de infraestrutura é um componente da Clean Architecture que implementa os detalhes técnicos de um aplicativo, como acesso a dados, registro, e-mail e outros mecanismos de comunicação. A camada de infraestrutura interage com sistemas e tecnologias externas, como bancos de dados, APIs ou serviços em nuvem.

Essa camada deve conter os detalhes de implementação da infraestrutura do aplicativo, como a implementação de repositórios de acesso a dados ou o sistema de registro. A camada de infraestrutura não deve conter nenhuma lógica de negócios ou conhecimento de domínio e deve interagir apenas com a camada de domínio por meio da camada de aplicativo (Application).

O objetivo da camada de infraestrutura é encapsular os detalhes técnicos do aplicativo para que possam ser facilmente alterados ou substituídos sem afetar o restante do aplicativo. Essa camada deve utilizar abstrações e interfaces para interagir com a camada de aplicação, possibilitando alterar a implementação de um componente específico sem afetar o restante da aplicação.

Dentro da pasta Infrastructure, haverá uma biblioteca de classes chamada Persistence que contém a implementação das interfaces do repositório. O projeto Persistence deve referenciar o projeto Application.

O projeto Persistence contém as pastas : Context, Repositories e Migrations e o arquivo ServiceExtensions

Na pasta Context temos o arquivo AppDbContext que é o arquivo de contexto que herda de DbContext do EF Core

using CleanArchitecture.Domain.Entities;
using
Microsoft.EntityFrameworkCore;

namespace CleanArchitecture.Persistence.Context;

public class AppDbContext : DbContext
{
 
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
  {
  }
  
public DbSet<User> Users { get; set; }
}

O arquivo de contexto é uma classe que herda da classe DbContext fornecida pelo Entity Framework Core. Essa classe é responsável por representar o banco de dados em código e fornece um ponto de entrada para interagir com o banco de dados por meio de operações de criação, leitura, atualização e exclusão (CRUD) em objetos persistentes.

No construtor da classe AppDbContext, recebemos uma instância de DbContextOptions<AppDbContext> options, que contém informações de configuração para o banco de dados, como a conexão, o provedor de banco de dados, etc. Ao passar essas opções para o construtor da classe base (DbContext), estamos configurando a conexão e outros aspectos do banco de dados.

Dentro do AppDbContext, você define propriedades do tipo DbSet<TEntity>, onde TEntity representa uma entidade de domínio da sua aplicação. Aqui temos a definição de DbSet<User> chamado Users. Esses conjuntos de dados definem como as entidades são mapeadas para tabelas do banco de dados. Você pode usar esses conjuntos de dados para consultar, adicionar, atualizar ou excluir registros no banco de dados associado à entidade correspondente.

Na pasta Repositories temos as implementações das interfaces definidas no projeto Domain:

1- UserRepository

using CleanArchitecture.Domain.Entities;
using
CleanArchitecture.Domain.Interfaces;
using
CleanArchitecture.Persistence.Context;
using
Microsoft.EntityFrameworkCore;

namespace CleanArchitecture.Persistence.Repositories;

public class UserRepository : BaseRepository<User>, IUserRepository
{
 
public UserRepository(AppDbContext context) : base(context)
  {}

  public async Task<User> GetByEmail(string email, CancellationToken cancellationToken)
  {
   
return await Context.Users.FirstOrDefaultAsync(x => x.Email == email, cancellationToken);
  }
}

Esta classe é um repositório que lida com operações de acesso a dados relacionadas à entidade User. A classe UserRepository herda da classe BaseRepository<User> e implementa a interface IUserRepository.

O construtor aceita uma instância de AppDbContext como parâmetro. Ele chama o construtor da classe base (BaseRepository<User>) passando o contexto do banco de dados. Isso implica que a classe UserRepository está usando a mesma instância do contexto do banco de dados definido na classe AppDbContext para realizar operações de acesso a dados.

Este método é usado para obter um usuário pelo seu endereço de e-mail. Ele faz uma consulta ao banco de dados usando o Context fornecido no construtor. O método FirstOrDefaultAsync é usado para retornar o primeiro usuário que corresponda à condição especificada, que neste caso é que o campo Email do usuário seja igual ao valor fornecido.

O parâmetro cancellationToken é usado para suportar o cancelamento assíncrono, permitindo que a operação seja cancelada caso o token de cancelamento seja acionado.

2- BaseRepository

using CleanArchitecture.Domain.Common;
using
CleanArchitecture.Domain.Interfaces;
using
CleanArchitecture.Persistence.Context;
using
Microsoft.EntityFrameworkCore;

namespace CleanArchitecture.Persistence.Repositories;

public class BaseRepository<T> : IBaseRepository<T> where T : BaseEntity
{
 
protected readonly AppDbContext Context;
 
public BaseRepository(AppDbContext context)
  {
     Context = context;
  }

  public
void Create(T entity)
  {
    Context.Add(entity);
  }
 
public void Update(T entity)
  {
   Context.Update(entity);
  }
 
public void Delete(T entity)
  {
    entity.DateCreated = DateTimeOffset.UtcNow;
    Context.Update(entity);
  }
 
public async Task<T> Get(Guid id, CancellationToken cancellationToken)
  {
   
return await Context.Set<T>().FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
  }
 
public async Task<List<T>> GetAll(CancellationToken cancellationToken)
  {
   
return await Context.Set<T>().ToListAsync(cancellationToken);
  }
}

A classe BaseRepository<T> é uma classe genérica que fornece operações básicas de acesso a dados, como criar, atualizar, excluir e obter entidades. Ela segue o padrão de repositório na arquitetura Clean Architecture (ou em padrões semelhantes), onde as operações de banco de dados são encapsuladas em um repositório para isolar a lógica de acesso a dados das outras partes da aplicação.

A classe possui uma propriedade protegida Context que é uma instância de AppDbContext, permitindo acesso ao contexto do banco de dados. O construtor aceita uma instância de AppDbContext como parâmetro e a atribui à propriedade Context.

O método Get é usado para obter uma entidade com base no seu identificador Guid. Ele utiliza o Set<T>() do contexto para selecionar o conjunto de entidades T e, em seguida, usa FirstOrDefaultAsync para buscar a entidade pelo seu Id; o método GetAll retorna todas as entidades do tipo T presentes no banco de dados, utilizando Set<T>() para selecionar o conjunto de entidades e ToListAsync para buscá-las.

O método Create é usado para adicionar uma nova entidade ao contexto do banco de dados e o método Update é usado para marcar uma entidade como modificada no contexto do banco de dados, para que suas alterações sejam persistidas ao chamar SaveChanges.

O método Delete não realmente exclui a entidade do banco de dados, mas, em vez disso, geralmente marca a entidade como excluída. Neste caso, a propriedade DateCreated da entidade é atualizada com o valor atual da data e hora em UTC antes de chamar Update no contexto.

3- UnitOfWork

public class UnitOfWork : IUnitOfWork
{
  
private readonly AppDbContext _context;
  
public UnitOfWork(AppDbContext context)
   {
     _context = context;
   }
  
public async Task Commit(CancellationToken cancellationToken)
   {
    
await _context.SaveChangesAsync(cancellationToken);
   }
}

Esta classe implementa a interface IUnitOfWork, que é usada para gerenciar transações e operações em lotes que envolvem múltiplas operações de acesso a dados. O conceito de Unidade de Trabalho visa garantir que várias operações relacionadas ao banco de dados sejam tratadas como uma única transação.

O construtor da classe UnitOfWork recebe uma instância de AppDbContext como parâmetro. Essa instância é usada para acessar o contexto do banco de dados, que é necessário para executar e gerenciar operações de acesso a dados.

O método Commit é responsável por efetivar as alterações pendentes no banco de dados. Ele chama o método SaveChangesAsync do contexto do banco de dados, que aplica todas as alterações realizadas nas operações anteriores de Create, Update, Delete e outras operações semelhantes.

O parâmetro cancellationToken é utilizado para permitir o cancelamento assíncrono, caso seja necessário cancelar a operação de salvamento.

A seguir temos no projeto a classe estática ServiceExtensions que define o método de extensão ConfigurePersistenceApp para a interface IServiceCollection:

4- ServiceExtensions

using CleanArchitecture.Domain.Interfaces;
using
CleanArchitecture.Persistence.Context;
using
CleanArchitecture.Persistence.Repositories;
using
Microsoft.EntityFrameworkCore;
using
Microsoft.Extensions.Configuration;
using
Microsoft.Extensions.DependencyInjection;

namespace CleanArchitecture.Persistence;

public static class ServiceExtensions
{
 
public static void ConfigurePersistenceApp(this IServiceCollection services,
                                             IConfiguration configuration)

  {

     var connectionString = configuration.GetConnectionString("Sqlite");
     services.AddDbContext<AppDbContext>(opt => opt.UseSqlite(connectionString));
     services.AddScoped<IUnitOfWork, UnitOfWork>();
     services.AddScoped<IUserRepository, UserRepository>();
  }
}

Este é um método de extensão que adiciona configurações relacionadas à persistência à coleção de serviços. Ele aceita dois parâmetros:

  1. services (uma instância de IServiceCollection) e
  2. configuration (uma instância de IConfiguration que contém as configurações da aplicação).   

Essas linhas de código configuram o contexto do banco de dados (AppDbContext) usando o Entity Framework Core. Elas obtêm a string de conexão chamada "Sqlite" das configurações e a utilizam para configurar o contexto do banco de dados usando o provedor SQLite. Isso significa que a aplicação usará o SQLite como banco de dados.

Aqui, são registradas as implementações de interfaces no contêiner de injeção de dependência:

Esses registros permitem que as classes da aplicação possam depender das interfaces e receber as implementações correspondentes, seguindo o princípio da inversão de controle (IoC) e a injeção de dependência.

Na próxima parte do artigo vamos implementar a camada de apresentação na pasta Presentation.

"Como guardaste a palavra da minha paciência, também eu te guardarei da hora da tentação que há de vir sobre todo o mundo, para tentar os que habitam na terra."
Apocalipse 3:10

Referências:


José Carlos Macoratti