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. |
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.
Nesse contexto, é importante manter o seguinte em mente:
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 => 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:
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:
C# - Tasks x Threads. Qual a diferença
DateTime - Macoratti.net
Null o que é isso ? - Macoratti.net
Formatação de data e hora para uma cultura ...
C# - Calculando a diferença entre duas datas
NET - Padrão de Projeto - Null Object Pattern
C# - Fundamentos : Definindo DateTime como Null ...
C# - Os tipos Nullable (Tipos Anuláveis)