Repositórios Genéricos

Repositórios Genéricos no Repository Pattern

Introdução aos Repositórios Genéricos

Com os fundamentos do Repository Pattern estabelecidos, avança-se para um tópico mais avançado: os repositórios genéricos. Esta abordagem, embora popular em determinados contextos, apresenta considerações arquiteturais importantes que devem ser ponderadas antes de sua adoção.

A implementação de um repositório genérico visa criar uma classe base que encapsule operações CRUD comuns, permitindo que repositórios específicos herdem essa funcionalidade. Contudo, esta estratégia nem sempre se alinha com práticas de design como Domain-Driven Design (DDD) ou Arquitetura Limpa, onde as operações de acesso a dados frequentemente possuem requisitos específicos do domínio.

Design da Interface Genérica

Princípio de Segregação de Interface (ISP)

Ao definir uma interface de repositório genérico, é essencial considerar o Princípio de Segregação de Interface (Interface Segregation Principle - ISP) dos princípios SOLID. Este princípio estabelece que uma interface não deve forçar suas implementadoras a depender de métodos que não utilizam.

Uma interface genérica IRepository que define um CRUD completo obrigaria todas as entidades do sistema a implementar todas as operações, mesmo quando algumas delas não são necessárias para determinado domínio.

namespace RepositoryStore.Repositories.Abstractions;

// Implementação inicial - potencial violação do ISP
public interface IRepository<T> where T : class
{
    Task<T> CreateAsync(T entity, CancellationToken cancellationToken = default);
    Task<T> UpdateAsync(T entity, CancellationToken cancellationToken = default);
    Task<T> DeleteAsync(T entity, CancellationToken cancellationToken = default);
    Task<T?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
    Task<List<T>> GetAllAsync(int skip = 0, int take = 25, 
        CancellationToken cancellationToken = default);
}

Consideração arquitetural: Em sistemas que seguem rigorosamente o ISP, poderiam ser definidas interfaces separadas:

Modelagem com Entidade Base

Para sistemas que utilizam uma hierarquia de entidades, pode-se definir uma classe base abstrata:

A interface genérica pode então restringir o tipo T para derivados de Entity:

Para fins demonstrativos, manteremos a restrição where T : class por ser mais genérica e aplicável a diferentes cenários.

Refatorando as Interfaces Específicas

Com a interface genérica definida, as interfaces específicas podem ser simplificadas. A interface IProductRepository passa a herdar da interface genérica, especificando o tipo concreto:

Observação: Em versões mais recentes do C# (12+), a sintaxe pode ser ainda mais concisa:

Esta abordagem mantém a flexibilidade para adicionar métodos específicos do domínio quando necessário, enquanto herda automaticamente todas as operações CRUD definidas na interface genérica.

Implementação da Classe Base Genérica

Estrutura da Classe Abstrata

Cria-se uma classe abstrata Repository<T> que implementa a interface IRepository<T>:

Análise da estrutura:

  • A classe é declarada como abstract para prevenir instanciação direta

  • O construtor primário recebe DbContext (não AppDbContext) para maior reutilização

  • A propriedade _dbSet é inicializada usando context.Set<T>(), que obtém o DbSet<T> apropriado para a entidade genérica

  • O DbContext é armazenado em campo protegido para uso nas operações de persistência

Obtendo o DbSet Genérico

O método context.Set<T>() é uma funcionalidade fundamental do Entity Framework que retorna a instância de DbSet<T> correspondente à entidade T. Este mecanismo permite que a classe base opere com qualquer entidade mapeada no contexto, sem precisar conhecer especificamente qual DbSet utilizar.

Implementação das Operações CRUD

Implementação Completa da Classe Base

Refatorando os Repositórios Específicos

Com a classe base implementada, os repositórios específicos tornam-se extremamente concisos:

Repositório de Produtos

Versão concisa (C# 12+):

Repositório de Categorias

Repositório de Usuários

Manutenção da Configuração de Dependência

Um dos benefícios significativos desta abordagem é que a configuração de injeção de dependência no Program.cs permanece inalterada:

Os consumidores da interface IProductRepository continuam funcionando normalmente, sem necessidade de alterações, demonstrando a compatibilidade retroativa da refatoração.

Vantagens e Desvantagens

Vantagens dos Repositórios Genéricos

  1. Redução de Código Duplicado: Operações CRUD comuns são implementadas uma única vez

  2. Consistência: Garante padrão uniforme em todas as operações de persistência

  3. Manutenibilidade Simplificada: Correções e melhorias aplicam-se automaticamente a todos os repositórios

  4. Velocidade de Desenvolvimento: Novas entidades podem ter repositórios funcionais com mínimo esforço

  5. Curva de Aprendizado: Desenvolvedores novos compreendem rapidamente o padrão estabelecido

Desvantagens e Considerações Críticas

  1. Potencial Violação do ISP: Entidades podem ser forçadas a implementar operações não utilizadas

  2. Limitação em Consultas Complexas: Consultas específicas do domínio frequentemente exigem métodos adicionais

  3. Acoplamento ao Entity Framework: A implementação genérica está intimamente ligada ao EF Core

  4. Dificuldade com Agregados Complexos: Em DDD, agregados com múltiplas entidades podem não se adequar ao modelo genérico

  5. Falta de Especificidade do Domínio: Operações de negócio específicas ficam fora do escopo do repositório genérico

Casos de Uso Recomendados

  • Sistemas CRUD Simples: Aplicações com operações básicas de persistência

  • Prototipagem Rápida: Desenvolvimento inicial onde a agilidade é prioritária

  • APIs Administrativas: Interfaces onde operações genéricas são predominantes

  • Sistemas com Muitas Entidades Similares: Domínios com múltiplas entidades que compartilham comportamento idêntico

Casos onde Evitar

  • Sistemas com Lógica de Domínio Complexa: Onde cada entidade tem comportamentos únicos

  • Arquitetura Limpa/DDD Rigorosa: Onde cada agregado tem requisitos específicos de persistência

  • Consultas Otimizadas para Performance: Onde consultas personalizadas são necessárias

  • Sistemas com Múltiplas Fontes de Dados: Onde diferentes entidades podem usar diferentes estratégias de persistência

Extensões e Personalizações

Adicionando Métodos Específicos

Mesmo com a classe base genérica, repositórios específicos podem estender a funcionalidade:

Implementando Interfaces Adicionais

Para maior flexibilidade, podem ser definidas múltiplas interfaces:

Conclusão e Recomendações Práticas

A implementação de repositórios genéricos representa uma ferramenta poderosa no arsenal do desenvolvedor, mas sua adoção deve ser ponderada em relação aos requisitos específicos do projeto.

Diretrizes de Decisão

  1. Avaliar a Complexidade do Domínio: Sistemas com regras de negócio simples beneficiam-se mais da genericidade

  2. Considerar a Evolução do Sistema: Prever se operações específicas serão necessárias no futuro

  3. Analisar os Requisitos de Performance: Consultas genéricas podem não ser otimizadas para cenários específicos

  4. Manter a Flexibilidade Arquitetural: Garantir que a solução não comprometa princípios de design importantes

Melhores Práticas Identificadas

  1. Começar com Especificidade: Iniciar com repositórios específicos e extrair padrões comuns posteriormente

  2. Manter a Capacidade de Extensão: Projetar a classe base para permitir sobrescrita de métodos quando necessário

  3. Documentar Suposições e Limitações: Clarificar os cenários onde a solução genérica é apropriada

  4. Implementar Progressivamente: Adicionar genericidade gradualmente à medida que os padrões emergem

Alternativas Consideradas

  • Repositórios Específicos por Entidade: Máximo controle e especificidade, com custo de duplicação

  • Padrão Specification: Separação das regras de consulta dos repositórios

  • Query Objects: Encapsulamento de consultas complexas em objetos especializados

  • CQRS (Command Query Responsibility Segregation): Separação radical entre operações de leitura e escrita

A decisão final deve equilibrar os benefícios da reutilização de código com a necessidade de expressar adequadamente a lógica de domínio e atender aos requisitos específicos do sistema em desenvolvimento. A implementação apresentada serve como base sólida que pode ser adaptada conforme as necessidades do projeto evoluem.

Atualizado