Что такое Generic Repository (обобщённый репозиторий)?

«Что такое Generic Repository (обобщённый репозиторий)?» — вопрос из категории Паттерны, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Generic Repository — это реализация паттерна Repository, которая использует обобщённые типы (Generics) для создания единого, переиспользуемого класса, абстрагирующего операции доступа к данным (CRUD) для любой сущности предметной области.

Зачем это нужно? Чтобы избежать дублирования кода. Вместо создания отдельного репозитория для Product, Order, Customer и т.д., создаётся один общий репозиторий, работающий с типом TEntity.

Базовая реализация с Entity Framework Core:

public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    void Update(T entity);
    void Delete(T entity);
    Task SaveChangesAsync();
}

public class Repository<T> : IRepository<T> where T : class
{
    protected readonly DbContext _context;
    protected readonly DbSet<T> _dbSet;

    public Repository(DbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public async Task<T?> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
    public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();
    public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity);
    public void Update(T entity) => _dbSet.Update(entity);
    public void Delete(T entity) => _dbSet.Remove(entity);
    public async Task SaveChangesAsync() => await _context.SaveChangesAsync();
}

// Использование
public class ProductService
{
    private readonly IRepository<Product> _productRepo;
    public ProductService(IRepository<Product> productRepo) => _productRepo = productRepo;

    public async Task<Product?> GetProduct(int id) => await _productRepo.GetByIdAsync(id);
}

Преимущества:

  • Универсальность: Один класс для всех сущностей.
  • Сокращение кода: Устраняет шаблонный код для базовых операций.
  • Простота тестирования: Легко подменить моком через интерфейс.

Недостатки и предостережения:

  • Ограниченность: Сложные запросы с джойнами, фильтрацией или агрегацией могут не вписаться в общий шаблон. Часто это решают, либо возвращая IQueryable<T> из репозитория (что может нарушить инкапсуляцию), либо используя дополнительный паттерн Specification.
  • Антипаттерн? В простых CRUD-приложениях Generic Repository может быть излишним, добавляя ненужный слой абстракции поверх DbSet. Его ценность возрастает в крупных проектах с чётким разделением слоёв и сложной бизнес-логикой.