C# - Criando API com Jwt Token - Autorização e Autenticação - Módulo 3 - Arquitetura 3 camadas - Repository Pattern
Matheus Paixão
Posted on April 7, 2022
Sejam Bem Vindos a mais um módulo da nossa série.
Código Modulo 3 - Repositório GitHub
Primeiramente. Olá
Melhorias desse modulo
- CRUD de Usuários
- Introdução do Repository Pattern
- Divisão em 3 Projetos (Infra - Domain - API)
Nesse módulo, irei abordar a evolução do módulo anterior, será muito fácil se perder na nova arquitetura que implementei, pois são vários caminhos percorridos para conseguir executar um end point.
Divisão em 3 Camadas
- API
- Na camada de API colocamos os itens principais do projeto, Controllers, as models de entrada e saída (RequestDTO e ResponseDTO) e as configurações pertinentes a
program.cs(.net 6)
oustartup.cs (.net 5 ou anterior)
.
- Na camada de API colocamos os itens principais do projeto, Controllers, as models de entrada e saída (RequestDTO e ResponseDTO) e as configurações pertinentes a
- INFRA
- Na camada de infraestrutura colocamos toda a infra de banco de dados e repositórios. (Context, Mapeamentos, Repositories)
- DOMAIN
- Na camada de Domain colocamos tudo que é compartilhado entre os outros projetos, as Models, os Services, Utilitários, Interfaces e helpers compartilhados entre os outros projetos. Domain é onde fica toda a regra de negócio da aplicação onde será compartilhado com as outras camadas.
Arquitetura 3 camadas
A arquitetura em 3 camadas serve para dividir as responsabilidades entre 3 projetos, podendo cada um focar em sua parte. Por exemplo, separando a camada de domínio e colocando toda a regra de negócio apenas ali, facilita assim, a manutenção do código, pois você já sabe que cada tecnologia, cada parte de código, está em seu devido lugar.
Como dividir o projeto em 3 camadas?
Criando Biblioteca de classes
Entenda que dividir o projeto em camadas nada mais é do que você criar sua própria biblioteca para consumo interno.
Para fazer a comunicação entre as camadas, é necessário referencia-las como no gif abaixo.
Repare que o projeto domain
, não contem referências, pois na verdade ele não depende de ninguém, são os outros projetos que dependem dele, pois é ali que se concentra toda a regra de negócio que precisa ser consumida pela infraestrutura e pelas controllers no projeto principal.
Repository Pattern
O repository pattern
é um dos padrões de projetos mais utilizamos mundialmente, ele é amplamente conhecido e aceito em projetos de todos os níveis.
Esse padrão de projeto visa a abstração de responsabilidade e encapsulamento do banco de dados, possibilitando a injeção de dependência.
Mas o que isso significa? Basicamente, você pode ter o banco de dados que for, e isso não irá afetar o resto do sistema. Pois agora cada um tem sua responsabilidade, se o repositório é responsável pelo acesso ao banco de dados, então quando tivermos alguma alteração de banco de dados, já sabemos que a alteração é mais provável que seja apenas ali.
Implementação
Primeiro de tudo eu crio o Repositório base, tem muitos exemplos prontos disponíveis pela internet com os métodos já prontos.
Entenda que pra todo repositório, ou serviço criado, você precisa também criar a Interface, que é o método de acesso a essa classe.
Base Repository
É aqui o único lugar onde terá uma estância do DataContext
que é nosso acesso ao Banco de dados.
Os repositórios sempre implementam os métodos definidos na interface dele, e representam de forma genérica uma entidade/model.
public class BaseRepository<T> : IBaseRepository<T> where T : Entity
{
private readonly DbSet<T> DbSet;
private readonly DataContext _context;
protected BaseRepository(DataContext context)
{
_context = context;
DbSet = _context.Set<T>();
}
public async Task<T?> GetById(Guid id) =>
await DbSet.FindAsync(id);
public async Task<IEnumerable<T>> GetAllAsync() =>
await DbSet.ToListAsync();
public async Task<T?> GetOneBy(Expression<Func<T, bool>> expression) =>
await DbSet.AsNoTracking().FirstOrDefaultAsync(expression);
public async Task<IEnumerable<T>> GetListBy(Expression<Func<T, bool>> expression) =>
await DbSet.Where(expression).ToListAsync();
public async Task<bool> Exists(Expression<Func<T, bool>> expression) =>
await DbSet.AnyAsync(expression);
public async Task AddAsync(T entity)
{
await DbSet.AddAsync(entity);
await SaveChanges();
}
public void Update(T entity) =>
DbSet.Update(entity);
public void Remove(T entity) =>
DbSet.Remove(entity);
public async Task<int> SaveChanges()
{
return await _context.SaveChangesAsync();
}
public void Dispose()
{
_context?.Dispose();
}
}
IBaseRepository Interface
Notem que a interface tem todos os métodos que implementamos no repositório base.
A interface é o método de acesso aos repositórios e serviços.
public interface IBaseRepository<T> : IDisposable where T : Entity
{
Task<T> GetById(Guid id);
Task<IEnumerable<T>> GetAllAsync();
Task<T> GetOneBy(Expression<Func<T, bool>> expression);
Task<IEnumerable<T>> GetListBy(Expression<Func<T, bool>> expression);
Task<bool> Exists(Expression<Func<T, bool>> expression);
Task<int> SaveChanges();
Task AddAsync(T entity);
public void Update(T entity);
public void Remove(T entity);
}
Service Login
Vou exemplificar como funciona o fluxo com o serviço de Login.
Repare que estanciamos o repositório de Login, que lá dentro acessa o repositório base que tem acesso ao banco de dados. Antes de chegar aqui, esse serviço é estanciado na controller através de sua interface.
public class LoginService : ILoginService
{
private readonly ILoginRepository _loginRepository;
private readonly TokenService _tokenService;
public LoginService(ILoginRepository loginRepository, TokenService tokenService)
{
_tokenService = tokenService;
_loginRepository = loginRepository;
}
public async Task<string> LoginAsync(string email, string password)
{
var user = await _loginRepository.GetUser(email);
if (user == null)
return "User or password invalid";
if (!PasswordHasher.Verify(user.PasswordHash, password))
return "User or password invalid";
try
{
var token = _tokenService.GenerateToken(user);
return token;
}
catch
{
return "Internal Error";
}
}
public void Dispose()
{
_loginRepository?.Dispose();
}
}
Controller
Terminando aqui o fluxo reverso de como acessar o banco de dados com o repository pattern, podemos ver que na controller estanciamos a interface do serviço de interesse.
public class AuthController : ControllerBase
{
private readonly ILoginService _loginService;
public AuthController(ILoginService loginService)
{
_loginService = loginService;
}
Colocarei aqui novamente a imagem do fluxo de dados no repository pattern. Da controller você acessa os serviço de interesse, onde tem toda a regra de negócio da entidade em questão, depois acessa o repositório da entidade através da interface, e por fim, acessa o repositório base pra acesso aos dados.
Pra tudo isso funcionar, temos que declarar as injeções de dependências, pois é dessa forma que estamos estanciando uma classe na outra.
Sempre declarar na ordem da esquerda pra direita no fluxo de dados, então primeiro a interface depois sua classe de implementação.
public static IServiceCollection ResolveDependencies(this IServiceCollection services)
{
services.AddDbContext<DataContext>();
services.AddScoped<TokenService>();
services.AddScoped<ILoginService, LoginService>();
services.AddScoped<ILoginRepository, LoginRepository>();
services.AddScoped<IRoleRepository, RoleRepository>();
services.AddScoped<IRoleService, RoleService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IUserRepository, UserRepository>();
return services;
}
Porque implementar tudo isso?
Temos diversos padrões e designs de arquiteturas a adotar, no fim das contas implemente aquele que mais se encaixa com sua demanda.
Se você vai fazer uma API simples, com 2 models, 1 controller e um crud simples, talvez não faça sentido você aplicar essa complexidade. Mas é uma decisão e se tomar com o seu time de desenvolvimento.
Lembrando os benefícios que você aplicar conceitos como S.O.L.I.D e Repository pattern por exemplo.
- O sistema ser agnóstico de banco de dados, você pode trocar o banco de dados com facilidade sem afetar o resto do código.
- Código centralizado em um único ponto, evitando duplicidade.
- Facilita a implementação de testes. Em caso de testes mais avançados, talvez você não consiga fazer sem adotar uma arquitetura mais organizada.
- Diminui o acoplamento entre classes.
- Padronização de códigos e serviços.
- Facilita a reutilização de código.
Fique ligado no próximo artigo da série, onde vamos adicionar o padrão de UOW - Unity of Work para acesso ao banco de dados.
Confira os módulos anteriores
Posted on April 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.