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

Ответ

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. Его ценность возрастает в крупных проектах с чётким разделением слоёв и сложной бизнес-логикой.

Ответ 18+ 🔞

Ну что за мода пошла — на каждый чих городить репозитории отдельные? Сидишь, пишешь для Product одно и то же, для Order — то же самое, для Customer — опять этот блядский CRUD. Рука отваливается, честно. А потом ещё тесты для каждого писать, мокать это всё… Пиздец, а не жизнь.

Вот поэтому умные дядьки и придумали Generic Repository — одну хуйню, которая работает с любыми сущностями. Типа универсальный пульт для управления данными. Выглядит примерно так:

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);
}

Смотри, какой красивый блять — один класс на все случаи жизни. Хочешь работать с продуктами — Repository<Product>, хочешь с заказами — Repository<Order>. Всё, больше нихуя не нужно. Код в разы меньше, тестить проще, подменить моком — вообще как два пальца.

Но, как всегда, есть одно огромное НО. Если тебе вдруг понадобится какой-то хитрый запрос — с джойнами, группировками, или фильтрацией по трём полям сразу — тут этот универсальный репозиторий может показать тебе фигу. Он для базовых операций хорош, а для сложной логики — уже не очень. Приходится либо IQueryable наружу высовывать (что само по себе пиздец как некрасиво), либо городить отдельный паттерн Specification, что тоже не сахар.

И ещё момент: если у тебя приложение — три экрана и база на пять таблиц, то этот Generic Repository — просто лишняя прослойка, которая только мозги парит. Ты можешь спокойно работать прямо с DbSet и не ебаться. А вот в больших проектах, где слоёв как в матрёшке, и бизнес-логика сложнее, чем инструкция к стиральной машине — там он реально может жизнь упростить.

Так что думай сам, надо тебе это или нет. Иногда проще не умничать, а делать как работает.