ASP.NET Core - Repositório genérico e Unit Of Work


 Neste artigo veremos porque usar um repositório genérico com a implementação da Unit Of Work em um projeto ASP.NET Core.

Vamos supor que temos um projeto ASP.NET Core construído com o seguinte cenário :

Temos uma solução chamada ApiDemo contendo 3 projetos Class Library e um projeto Web API:

O projeto ApiDemo.ApiProdutos esta referenciando os projetos Infrastructure(indiretamente) e Application e isso não é uma violação direta do princípio da Clean Architecture, desde que essa dependência seja gerenciada de forma adequada e que a direção da dependência seja mantida corretamente.

A Clean Architecture, proposta por Robert C. Martin, incentiva uma arquitetura em camadas, com a camada mais interna (núcleo) sendo independente de qualquer detalhe de infraestrutura externa. Isso permite que a lógica de negócios seja mantida isolada e independente de detalhes de implementação externos. No entanto, a direção da dependência deve sempre apontar para o centro (núcleo) e não o contrário. Isso é conhecido como o Princípio de Dependência, um dos princípios do SOLID.

Nesse contexto, é importante manter o seguinte em mente:

  1. Direção da dependência: Certifique-se de que as dependências fluam do núcleo (camada de domínio e aplicação) para fora (camadas de infraestrutura, como acesso a dados). As camadas superiores (por exemplo, ApiProdutos) devem depender das camadas inferiores (por exemplo, Application e Infrastructure), e não o contrário.
  2. Abstração de Interfaces: Para manter a independência da camada de domínio e aplicação, você pode definir interfaces ou contratos que sejam implementados nas camadas de infraestrutura. Dessa forma, as camadas superiores dependem apenas das interfaces, enquanto as implementações concretas residem nas camadas de infraestrutura.
  3. Inversão de Controle (IoC): Use técnicas de IoC (Inversão de Controle) e DI (Injeção de Dependência) para fornecer instâncias das implementações concretas nas camadas superiores. Isso permite que você substitua facilmente implementações e mantenha a flexibilidade.
  4. Testabilidade: Uma vantagem de manter a lógica de negócios nas camadas superiores é que ela se torna mais testável, pois você pode injetar facilmente implementações de mock ou simuladas durante os testes.

Portanto, referenciar o projeto Infrastructure indiretamente a partir do projeto ApiProdutos não é necessariamente uma violação direta da Clean Architecture, desde que você siga os princípios mencionados acima e mantenha a direção da dependência apropriada. Certifique-se de que a camada de aplicação seja a ponte entre a camada de domínio (núcleo) e a camada de infraestrutura.

No projeto Domain temos a classe Produto na pasta Entities:

public class Produto
{
    public int Id { get; set; }
    public string? Nome { get; set; }
    public string? Descricao { get; set; }
    public string? CodigoBarras { get; set; }
    public decimal Preco { get; set; }
    public DateTime IncluidoEm { get; set; }
    public DateTime ModificadoEm { get; set; }
}

No projeto Domain , na pasta Interfaces temos as interfaces IRepository, IProdutoRepository e IUnitOfWork :

1- IRepository

public interface IRepository<T> where T : class
{
    Task<T> GetPorIdAsync(int id);
    Task<IReadOnlyList<T>> GetTodosAsync();
    Task AdicionarAsync(T entity);
    Task AtualizarAsync(T entity);
    Task DeletarAsync(int id);
}

2- IProdutoRepository

public interface IProdutoRepository : IRepository<Produto>
{
}

3- IUnitOfWork

public interface IUnitOfWork
{
    IProdutoRepository Produtos { get; }
}

A implementação da UnitOfWork foi feita com o seguinte código :

public class UnitOfWork : IUnitOfWork
{
    public UnitOfWork(IProdutoRepository productRepository)
    {
        Produtos = productRepository;
   }

    public IProdutoRepository Produtos { get; }

}

Obs: No projeto Application temos as pastas Interfaces e Services e nestas pastas temos a interface IProdutoService e a classe concreta ProdutoService.

Otimizando a implementação

Nesta implementação da interface IUnitOfWork a classe UnitOfWork está explicitamente acoplada ao IProdutoRepository. Isso significa que, se você decidir adicionar mais repositórios no futuro (por exemplo, IClienteRepository, IVendaRepository, etc.), precisará modificar a classe UnitOfWork para injetar esses repositórios adicionais. Isso viola o princípio Open-Closed do SOLID, que preconiza que as classes devem estar abertas para extensão, mas fechadas para modificação.

Podemos otimizar a injeção de dependência do repositório de produtos na classe UnitOfWork usando a técnica de injeção de dependência automática. Isso pode ser feito configurando o contêiner de injeção de dependência para registrar automaticamente os repositórios necessários no escopo da unidade de trabalho.

Para fazer isso  vamos criar refatorar o código da interface IUnitOfWork e criar uma nova implementação da uma classe de unidade de trabalho genérica que aceita repositórios genéricos.

1- IUnitOfWork

public interface IUnitOfWork
{
 
// Método genérico para obter um repositório com base no tipo de entidade
   IRepository<TEntity> GetRepository<
TEntity>() where TEntity : class;

   // Método para salvar todas as alterações pendentes no contexto
   Task<
int> CommitAsync();
}

2- UnitOfWork

public class UnitOfWork : IUnitOfWork
{

  private readonly AppDbContext _context;
 
private Dictionary<Type, object> _repositories;
 
public UnitOfWork(AppDbContext context)
  {
    _context = context;
    _repositories =
new Dictionary<Type, object>();
  }

  public IRepository<TEntity> GetRepository<TEntity>() where TEntity : class
  {
   
if (_repositories.ContainsKey(typeof(TEntity)))
    {
     
return (IRepository<TEntity>)_repositories[typeof(TEntity)];
    }
   
var repository = new Repository<TEntity>(_context);
   _repositories.Add(
typeof(TEntity), repository);
  
return repository;
  }

  public async Task<int> CommitAsync()
  {
   
try
    {
       
return await _context.SaveChangesAsync();
    }
   
catch (DbUpdateException ex)
    {
     
// Lidar com exceções de atualização do banco de dados, se necessário
     
throw new Exception("Ocorreu um erro ao salvar as alterações no banco de dados.", ex);
    }
  }

 
public void Dispose()
  {
    _context.Dispose();
  }
}

Aqui cabe destacar que a implementação da UnitOfWork genérica utiliza um dicionário interno (_repositories) para armazenar instâncias de repositórios.

Embora funcional, essa abordagem pode ter algumas desvantagens em relação ao gerenciamento de ciclo de vida dos repositórios pelo próprio contêiner de injeção de dependência do ASP.NET Core.

Uma alternativa seria registrar cada repositório específico no contêiner de DI e, na UnitOfWork, injetar um IEnumerable<IRepository<TEntity>>. A UnitOfWork então poderia usar o contêiner para resolver a implementação específica necessária.

No entanto, a abordagem com o dicionário também é válida e mantém o controle da criação das instâncias dentro da UnitOfWork.

A implementação do repositório na classe Repository<T> é feita na camada de infraestrutura:

Repository<T>

public class Repository<T> : IRepository<T> where T : class
{
    protected readonly AppDbContext _context;
    protected readonly DbSet<T> _dbSet;
    public Repository(AppDbContext context)
    {
        _context = context ??
            throw new ArgumentNullException(nameof(context));
        _dbSet = context.Set<T>();
    }
    // O método FindAsync é uma operação de pesquisa mais eficiente,
    // especialmente quando você deseja recuperar uma entidade pelo
    // valor de sua chave primária. Ele verifica primeiro se a entidade
    // com a chave especificada existe no contexto do EF Core.Se a entidade
    // estiver no contexto, ela será retornada imediatamente sem a necessidade
    // de uma consulta ao banco de dados.Se a entidade não estiver no contexto,
    // o FindAsync emitirá uma consulta para recuperar a entidade do banco de
    // dados e a adicionará ao contexto para uso futuro.Isso significa que o
    // FindAsync é eficaz em cenários em que você pode frequentemente buscar
    // a mesma entidade por ID durante a mesma solicitação HTTP.
    public async Task<T> GetPorIdAsync(int id)
    {
        return await _dbSet.FindAsync(id);
    }
    public async Task<IReadOnlyList<T>> GetTodosAsync()
    {
        return await _dbSet.ToListAsync();
    }
    public async Task AdicionarAsync(T entity)
    {
        await _dbSet.AddAsync(entity);
    }
    public async Task AtualizarAsync(T entity)
    {
        _context.Entry(entity).State = EntityState.Modified;
        await Task.CompletedTask;
    }
    public async Task DeletarAsync(int id)
    {
        var entity = await _dbSet.FindAsync(id);
        if (entity != null)
        {
            _dbSet.Remove(entity);
        }
    }
}

No projeto ApiProdutos , na classe Program, vamos configurar o container de injetção de dependência para registrar as interfaces e duas implementações:

...

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString(
"DefaultConnection")));

builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped(
typeof(IProdutoService<>), typeof(ProdutoService<>));
 

var app = builder.Build();
...
...

Com esta configuração, a ASP.NET Core irá automaticamente injetar a dependência correta do repositório de produtos no construtor da sua classe UnitOfWork sempre que você criar uma instância dela. Além disso, você pode usar essa técnica para injetar outros repositórios genéricos, se necessário, em sua aplicação.

Assim esta implementação, usando a injeção de dependência automática, tem várias vantagens como:

  1. Desacoplamento: Não há acoplamento direto entre a classe UnitOfWork e os repositórios específicos. Isso significa que você pode adicionar, remover ou alterar repositórios sem modificar a classe UnitOfWork. Isso segue o princípio de inversão de dependência (DIP) e facilita a manutenção e extensibilidade do código.
     
  2. Reutilização: Com a abordagem genérica, você pode usar o mesmo UnitOfWork e os mesmos repositórios genéricos para qualquer entidade do seu modelo de dados. Isso promove a reutilização de código e simplifica o desenvolvimento.
     
  3. Melhor escalabilidade: À medida que seu aplicativo cresce e você adiciona mais entidades ao seu modelo de dados, você não precisa criar uma classe UnitOfWork separada para cada entidade ou repositório. Você pode simplesmente usar os repositórios genéricos existentes para lidar com essas entidades adicionais.
     
  4. Facilidade de teste: A injeção de dependência automática facilita a substituição de implementações de repositórios por versões simuladas ou de teste durante os testes de unidade, tornando mais fácil testar a funcionalidade da classe UnitOfWork.
     
  5. Conformidade com princípios SOLID: A abordagem genérica segue os princípios SOLID, especialmente o princípio Open-Closed, facilitando a extensão sem modificação do código existente.

Para utilizar esta implementação no controlador ProdutosController podemos usar o seguinte código :

using ApiDemo.Application.Services;
using ApiDemo.Domain.Entities;
using Microsoft.AspNetCore.Mvc;
namespace ApiDemo.ApiProdutos.Controllers;
[Route("api/[controller]")]
[ApiController]
public class ProdutosController : ControllerBase
{
    private readonly IProdutoService _produtoService;
    public ProdutosController(IProdutoService produtoService)
    {
        _produtoService = produtoService;
    }
    // GET: api/Produtos
    [HttpGet]
    // Retornar IEnumerable<Produto> é mais flexivel que retornar List<Produto>
    // pois se você quiser retornar outro tipo de coleção (ICollection<Produto>,
    // IReadOnlyList<Produto>) no futuro não terá que alterar o codigo e assim
    // Usar IEnumrable é uma abrodagem mais genérica, flexível e aderente
    //  às boas práticas de design de API pois ela permite que seu código
    //  seja mais aberto a futuras mudanças e reutilização.
    public async Task<ActionResult<IEnumerable<Produto>>> Get()
    {
        var produtos = await _produtoService.GetAllProdutosAsync();
        return Ok(produtos);
    }
    // GET: api/Produtos/5
    [HttpGet("{id}")]
    // Usar ActionResult<Produto>, que especifica explicitamente o tipo de dado que será retornado
    // pela ação é mais seguro do que usar IActionResult pois torna o código mais seguro,
    // visto que assim a ASP.NET Core pode inferir o tipo automaticamente com base no tipo
    // especificado na assinatura do método. Além disso, fornece melhor documentação e legibilidade
    // do código e melhora a segurança de tipos
    public async Task<ActionResult<Produto>> Get(int id)
    {
        var produto = await _produtoService.GetProdutoPorIdAsync(id);
        if (produto == null)
        {
            return NotFound();
        }
        return Ok(produto);
    }
    // POST: api/Produtos
    [HttpPost]
    public async Task<IActionResult> Post([FromBody] Produto produto)
    {
        if (produto == null)
        {
            return BadRequest();
        }
        var novoProdutoId = await _produtoService.AdicionarProdutoAsync(produto);
        return CreatedAtAction(nameof(Get), new { id = novoProdutoId }, produto);
    }
    // PUT: api/Produtos/5
    [HttpPut("{id}")]
    public async Task<IActionResult> Put(int id, [FromBody] Produto produto)
    {
        if (produto == null || id != produto.Id)
        {
            return BadRequest();
        }
        var resultado = await _produtoService.AtualizarProdutoAsync(produto);
        if (resultado == 0)
        {
            return NotFound();
        }
        return NoContent();
    }
    // DELETE: api/Produtos/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        var resultado = await _produtoService.DeletarProdutoAsync(id);
        if (resultado == 0)
        {
            return NotFound();
        }
        return NoContent();
    }
}

Neste código, o controlador ProdutosController injeta a dependência IProdutoService em seu construtor e usa os métodos do serviço para realizar operações CRUD. Os métodos do controlador simplesmente chamam os métodos do serviço e retornam as respostas apropriadas, como Ok(), NotFound(), ou NoContent(), dependendo do resultado das operações.

Com esta implementação, a lógica de negócios relacionada aos produtos é mantida no serviço ProdutoService, enquanto o controlador cuida da manipulação de solicitações HTTP e respostas. Isso mantém o código do controlador mais limpo e ajuda na separação de preocupações em sua aplicação.

Seria o padrão Repositório um anti-padrão ?

O padrão Repositório surgiu para abstrair a comunicação com o banco de dados, separando a lógica de persistência da lógica de negócios. Ele era útil quando se trabalhava diretamente com SQL ou ORMs menos avançados, garantindo um isolamento da infraestrutura.

Entretanto, com ORMs modernos como o Entity Framework Core, que já oferecem abstrações para acesso e manipulação de dados, o padrão repositório pode se tornar redundante. O EF Core já possui um DbContext que atua como um repositório, incluindo operações como Unit of Work, rastreamento de entidades e execução otimizada de queries.

O problema de usar repositórios sobre um ORM avançado é que pode limitar recursos poderosos, como queries expressivas, tracking automático e otimizações de execução. Isso pode gerar duplicação desnecessária de código e dificultar manutenção, tornando o padrão um anti-pattern em aplicações modernas.

Por outro lado, ele ainda pode ser útil em cenários específicos, como quando você quer padronizar acesso aos dados em múltiplos bancos ou desacoplar regras de negócio do ORM, facilitando testes e migração de tecnologia.

Assim, o padrão repositório não é sempre um anti-pattern, mas deve ser usado com cautela. Em aplicações modernas que usam ORMs como o EF Core, normalmente é melhor trabalhar diretamente com DbContext e aproveitar seus recursos nativo

O repositório genérico viola o princípio SOLID ISP ?

O Repositório Genérico pode entrar em conflito com o princípio ISP (Interface Segregation Principle) do SOLID, que defende que uma interface não deve forçar uma classe a depender de métodos que não usa.

Quando se cria um repositório genérico com métodos universais como Add(), Update(), Delete(), GetById(), todas as entidades são forçadas a ter esses métodos, mesmo que algumas não precisem deles. Isso quebra a segregação, gerando dependências desnecessárias e reduzindo a flexibilidade do código.

Outro problema é que, ao expor métodos genéricos, um repositório pode acabar limitando o uso de queries específicas de um ORM avançado como o Entity Framework Core, impedindo otimizações e forçando regras rígidas de acesso aos dados.

Uma alternativa melhor é definir repositórios específicos por contexto (exemplo: IProductRepository e ICategoryRepository), permitindo uma interface focada e evitando métodos irrelevantes. Ou, em casos onde o ORM já abstrai bem as operações, pode ser mais adequado trabalhar diretamente com o DbContext.

Assim, o repositório genérico não é sempre ruim, mas deve ser usado com cautela. Se for muito amplo, pode ferir ISP, dificultar manutenção e restringir a eficiência do acesso aos dados

Pegue o código do projeto aqui:  ApiDemo.zip

"Esta é a mensagem que dele ouvimos e transmitimos a vocês: Deus é luz; nele não há treva alguma.
Se afirmarmos que temos comunhão com ele, mas andamos nas trevas, mentimos e não praticamos a verdade"
1 João 1:5-6

Referências:


José Carlos Macoratti