DEV Community

Cover image for C# - Criando API com Jwt Token - Autorização e Autenticação - Módulo 3 - Arquitetura 3 camadas - Repository Pattern
Matheus Paixão
Matheus Paixão

Posted on • Updated on

C# - Criando API com Jwt Token - Autorização e Autenticação - Módulo 3 - Arquitetura 3 camadas - Repository Pattern

Sejam Bem Vindos a mais um módulo da nossa série.
 
Código Modulo 3 - Repositório GitHub
 

Primeiramente. Olá

Gifss

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

Image description

  • 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) ou startup.cs (.net 5 ou anterior).  
  • 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.

Criando Projeto

Para fazer a comunicação entre as camadas, é necessário referencia-las como no gif abaixo.

referencias

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

Repo 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();
        }
    }
Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
Enter fullscreen mode Exit fullscreen mode

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;
        }
Enter fullscreen mode Exit fullscreen mode

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.

Images

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;
        }
Enter fullscreen mode Exit fullscreen mode

Porque implementar tudo isso?

garoa

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

Modulo 1
Modulo 2

tHANKS

mgpaixao image

Top comments (0)