Implementando o Padrão Repository em .NET

Yuri Peixinho - Feb 26 - - Dev Community

Conceito

Imagine que você tem uma aplicação pequena, um sistema de padaria em que os usuários fazem a gestão do seu negócio. Então, a medida que seu aplicativo começa a ganhar popularidade, surge a a demanda de implementar novas funcionalidades, e consequentemente migrar para um banco de dados mais robusto e escalável. No entanto, seu sistema possui uma comunicação acoplada e simplificada com a camada de dados, esse fato torna a tarefa árdua, já que esse acoplamento entre a camada de dados (Domain Model) e aplicação obriga aos desenvolvedores a analisar todos os lugares, já que cada ponto que interage com o banco de dados precisa ser alterado manualmente, tornando uma atividade tediosa e propensa a erros.

O Padrão Repositório surge para evitar que situações como essas não aconteçam. O objetivo é desacoplar o nosso código da camada de dados (Domain Model) para que problemas como esse não aconteça. Então, podemos dizer que a aplicação não sabe qual o banco de dados está sendo usado, apenas o repositório que comunica para isso para a nossa aplicação.

Além disso, há outras vantagens, como:

  • Evitar códigos duplicados
  • Injeção de dependência
  • Facilita testes unitários
  • Flexibilidade (você pode facilmente trocar mecanismo de armazenamento (por exemplo um banco de dados SQL para um NoSQL) sem afetar o código dos negócios

Entendendo todos os conceitos acima, podemos afirmar dois fatos antes de continuar:

Não implementando o padrão repositório

A aplicação interage diretamente com o banco de dados.

(Banco de dados → Aplicação)

Implementando o padrão repositório

A aplicação usa o repositório como intermediador da comunicação entre aplicação e banco de dados.

(Banco de dados → Repositório → Aplicação)

Antes de implementar o Padrão Repository

Antes de começar a implementar a interface, vamos mostrar a versão do sem esse padrão de projeto?

    [ApiController]
    [Route("api/[controller]")]
    public class ProdutosController : ControllerBase
    {
        private readonly MeuDbContext _contexto;

        public ProdutosController(MeuDbContext contexto)
        {
            _contexto = contexto;
        }

        [HttpGet]
        public IActionResult ObterTodosProdutos()
        {
            var produtos = _contexto.Produtos.ToList();
            return Ok(produtos);
        }

        [HttpGet("{id}")]
        public IActionResult ObterProdutoPorId(int id)
        {
            var produto = _contexto.Produtos.FirstOrDefault(p => p.Id == id);
            if (produto == null)
            {
                return NotFound();
            }
            return Ok(produto);
        }

        [HttpPost]
        public IActionResult CriarProduto(Produto produto)
        {
            _contexto.Produtos.Add(produto);
            _contexto.SaveChanges();
            return CreatedAtAction(nameof(ObterProdutoPorId), new { id = produto.Id }, produto);
        }
    }
Enter fullscreen mode Exit fullscreen mode

Controller sem o uso do padrão Repository

Podemos ver nesse exemplo que temos alguns pontos a melhorar, como:

Acoplamento do Controller ao EF Core

Nosso Controller está a todo momento instanciando o contexto do banco de dados e usando diretamente na nossa camada de aplicação. Criando um acoplamento e dependência direta entre nossa camada de dados e aplicação

Concentração e responsabilidade de acesso a dados na mão do Controller

Em consequência do ponto anterior, além do acoplamento entre o Controller e camada de dados, a responsabilidade dos nossos métodos de acesso aos dados está concentrado no controller, deixando o código mais difícil de testar, pois mistura a lógica de negócios com lógica de acesso a dados.

Falta de reutilização de código

Quando não usamos o padrão Repository não existe uma abstração clara sobre como os dados são acessados e manipulados, então teríamos que repetir então os trechos de códigos de acesso aos dados como: _contexto[...] em todos os Controllers, o que pode levar a inconsistências e erros, pois o código pode ser modificado em um lugar e ser esquecido em outro.

Sabendo disso, ao introduzir o padrão Repository, criamos uma camada de abstração entre a lógica de negócios da aplicação e o mecanismo de armazenamento de dados. Isso permite que os controllers e outras partes da aplicação interajam com os dados por meio de uma interface bem definida, em vez de dependerem diretamente de detalhes de implementação, como o ORM utilizado ou a estrutura do banco de dados.

Implementando o Padrão Repository

Para a implementação do padrão é necessário seguir algumas etapas para garantir a estruturação correta e a eficiência da separação das responsabilidades.

  1. Interface do Repositório
  2. Classe concreta que implementa a interface criada
  3. Controller para usar o repositório

Vamos usar o exemplo do código anterior sem a implementação e adequá-lo.

Interface

A interface é a primeira etapa, ela é a responsável por definir o contrato (conjunto de operações) que os repositórios devem implementar e que será implementada pela classe concreta (próxima etapa). Simplificando, esse contrato são operações básicas de acesso aos dados, como buscar, adicionar, atualizar e excluir registros, definindo uma fronteira clara entre as partes da aplicação sem que haja uma dependência entre elas.

Para construir a interface temos que identificar a Entidade do Domínio e Métodos necessários para acessar e manipular os dados da Entidade.

  • [1] - Prática

    Então, ao analisar o nosso Controller, podemos ver que o nome da entidade é Produto e seus métodos são ObterTodosProdutos, ObterProdutoPorId e CriarProduto

        [ApiController]
        [Route("api/[controller]")]
        public class **ProdutosController** : ControllerBase
        {
            private readonly MeuDbContext _contexto;
    
            public ProdutosController(MeuDbContext contexto)
            {
                _contexto = contexto;
            }
    
            [HttpGet]
            public IActionResult **ObterTodosProdutos**()
            {
                var produtos = _contexto.Produtos.ToList();
                return Ok(produtos);
            }
    
            [HttpGet("{id}")]
            public IActionResult **ObterProdutoPorId**(int id)
            {
                var produto = _contexto.Produtos.FirstOrDefault(p => p.Id == id);
                if (produto == null)
                {
                    return NotFound();
                }
                return Ok(produto);
            }
    
            [HttpPost]
            public IActionResult **CriarProduto**(Produto produto)
            {
                _contexto.Produtos.Add(produto);
                _contexto.SaveChanges();
                return CreatedAtAction(nameof(ObterProdutoPorId), new { id = produto.Id }, produto);
            }
        }
    

    O processo da criação da interface é simples, apenas será criado os contratos com o nome da interface (seguindo as convenções) e seus métodos.

    public interface **IProdutoRepository**
    {
        IEnumerable<Produto> **ObterTodosProdutos**();
        Produto **ObterProdutoPorId**(int id);
        void **CriarProduto**(Produto produto);
    }
    

Implementando o Generic Repository

Para entender a implementação do repositório genérico, é importante entender conceitos acerca de Interfaces genéricas.

Vamos imaginar que seu projeto agora possui várias entidades, e estas entidades terão várias modelagens de DataAcess para cada classe.
Tendo esse contexto em mente, teremos que criar várias interfaces e repositórios e em seguida implementar esses métodos para cada entidade distinta. É nesse momento que entra o uso do Repositório Genérico, já que ele permite que você abstraia o acesso aos dados em uma única classe. Essa prática permite economizar mais tempo e deixar o código menos acoplado, além de seguir o princípio DRY.

Para entender com melhor eficiência sobre os repositórios genéricos pode-se dizer que precisamos dividir duas etapas em nosso exemplo, a primeira que é a criação do repositório genérico em si, enquanto a segunda será a implementação desse repositório.

  1. Criando a interface genérica
    1. Criando interface genérica
    2. Implementando interface genérica
  2. Implementação de classe específica

1a. Criando a Interface Genérica

Vamos começar a implementação da interface genérica IBaseRepository.

public interface IBaseRepository<T> where T : class
{
    Task<IEnumerable<T>> GetAllAsync();
    Task<T> GetByIdAsync(int id);
    Task<T> AddAsync(T entity);
    Task<T> UpdateAsync(T entity);
    DeleteAsync(int id);
}
Enter fullscreen mode Exit fullscreen mode

1b. Implementando Interface Genérica

Agora vamos fazer a implementação da interface IBaseRepository. A partir dessa implementação, nossas entidades podem utilizar os métodos genéricos contidos nessa implementação.

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;

public class BaseRepository<T> : IBaseRepository<T> where T : class
{
    protected readonly DbContext _context;

    public BaseRepository(DbContext context)
    {
        _context = context;
    }

    public async Task<IEnumerable<T>> GetAllAsync()
    {
        return await _context.Set<T>().ToListAsync();
    }

    public async Task<T> GetByIdAsync(int id)
    {
        return await _context.Set<T>().FindAsync(id);
    }

    public async Task AddAsync(T entity)
    {
        await _context.Set<T>().AddAsync(entity);
        await _context.SaveChangesAsync();
    }

    public async Task UpdateAsync(T entity)
    {
        _context.Set<T>().Update(entity);
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var entity = await _context.Set<T>().FindAsync(id);
        if (entity != null)
        {
            _context.Set<T>().Remove(entity);
            await _context.SaveChangesAsync();
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

2a. Entidade da Classe Específica

Agora que temos nossa classe genérica base vamos implementar uma Entidade em nosso sistema chamada Cliente.

public class Cliente 
{
        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

2b. Interface da Classe Específica

Observe que não repetimos oos método que estão no IBaseRepository, uma vez que são herdados, apenas criamos um método específico que não faz parte da classe base

public interface IClienteRepository : IBaseRepository<Cliente>
{
    Task<IEnumerable<Cliente>> GetCostumerWithOrderAsync();
}
Enter fullscreen mode Exit fullscreen mode

3b. Implementação da Classe Específica

public class ClienteRepository : BaseRepository<Cliente>, IClienteRepository
{
    public RestauranteRepository(DbContext context) : base(context)
    {
    }

    public async Task<IEnumerable<Cliente>> GetCustomersWithOrdersAsync()
    {
       throw new NotImplementedException();
    }
}
Enter fullscreen mode Exit fullscreen mode

Podemos entender a partir desse momento que se tivermos uma nova entidade chamada Atendente teremos o repositório genérico que fornece todos os métodos básicos para a manutenção de nossos acessos aos dados. E além disso, cada interface dedicada pode implementar uma lógica extra de negócio/dados sem interferir na implementação de outras entidades. Além disso tudo, temos um código mais coeso e com menos duplicação de código, respeitando o princípio DRY.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . .