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:
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 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);
}
}
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.
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.
Vamos usar o exemplo do código anterior sem a implementação e adequá-lo.
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.
Com a nossa interface construída, em seguida precisamos criar nossa classe concretas que vão implementar as interfaces dos repositórios. A classe concreta é a responsável por fornecer a lógica real de consultas e acesso aos dados de uma determinada entidade.
Em nosso exemplo estamos usando a ORM Entity Framework Core. Em nossa implementação assinamos todos os métodos que foram criados no contratos e todos eles fazem uma interação específica com _contexto
do banco de dados, separando a responsabilidade de acesso aos dados que anteriormente era do controller
para essa nova classe Repository.
A responsabilidade do nosso controller agora será apenas receber requisições HTTP relacionadas a nossa Entidade (Produto) e coordenar as operações efetuar as requisições. Será então, apenas um intermediador entre o cliente e o repositório de produtos. Com isso, temos a melhor separação de responsabilidades e testes, sem contar o menor acoplamento entre a camada de dados e negócios.
Certo, mas como isso acontece? Se formos comparar o código inicial, podemos ver que tínhamos em nosso construtor a instância do contexto banco de dados e esse contexto era o responsável por lidar com as operações do banco de dados além das requisições HTTP, levando a um forte acoplamento. Mas agora efetuamos uma injeção de dependência em nosso repositório de produtos no construtor do controller, introduzindo uma abstração entre nosso controller e o acesso aos dados. Então, o controller não precisa se preocupar com os detalhes de como os dados estão sendo acessados, pois ao invés disso ele apenas se preocupa em chamar os métodos do repositório (que criamos anteriormente) que faz esse trabalho, resultando em um código mais limpo, modular, escalável e com separações de responsabilidade.
Certifique-se de que a injeção de dependência para está configurada corretamente no seu projeto.
Existem duas formas de fazer isso, e a escolha depende do tamanho do seu projeto e escalabilidade.
var builder = WebApplication.CreateBuilder(args);
// Registro do repositório e manager no container de serviços
builder.Services.AddScoped<IContaRepository, ContaRepository>();
var app = builder.Build();
using AL.Data.Repository;
using AL.Manager.Implementation;
using AL.Manager.Interfaces.Managers;
using AL.Manager.Interfaces.Repositories;
namespace CL.WebApi.Configuration;
public static class DependencyInjectionConfig
{
public static void AddDependencyInjectionConfiguration(this IServiceCollection services)
{
services.AddScoped<IContaRepository, ContaRepository>();
services.AddScoped<IContaManager, ContaManager>();
}
}
using AL.WebApi.Configuration;
using CL.WebApi.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDataBaseConfiguration(builder.Configuration);
**builder.Services.AddDependencyInjectionConfiguration();**
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseDatabaseConfiguration();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
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.
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);
}
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();
}
}
}
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; }
}
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();
}
public class ClienteRepository : BaseRepository<Cliente>, IClienteRepository
{
public RestauranteRepository(DbContext context) : base(context)
{
}
public async Task<IEnumerable<Cliente>> GetCustomersWithOrdersAsync()
{
throw new NotImplementedException();
}
}
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.