Это один из наиболее часто используемых и рекомендуемых шаблонов для применения в приложении, которое работает с любым типом базы данных. Это простой способ создать стандарт для ваших CRUD-операций. Если вам нравятся дженерики, вам понравится этот шаблон.

Почему так хорошо?

Потому что централизует все основные операции с сущностью в одном основном классе. Без этого подхода у нас было бы несколько классов с одинаковой логикой, но для другой сущности.

Следующий блок кода показывает основной интерфейс:

public interface IRepository<TEntity>
{
    TEntity Find(params object[] keyValues);
    IEnumerable<TEntity> FindAll();
    void Insert(TEntity entity);
    void Update(TEntity entity);
    void Delete(TEntity entity);
}

Как мы видим, у нас есть все 4 основные операции для сущности. Интерфейс предназначен для общей сущности, чтобы мы могли использовать ее для любой существующей сущности.

Как я могу получить доступ к базе данных?

Вы можете использовать любой ORM по своему усмотрению, в этом случае я буду использовать Entity Framework Core. Эта библиотека дает нам класс DbContext, который в основном представляет саму базу данных. Он также дает нам класс DbSet<TEntity> для представления всех сущностей в контексте данного типа.

Имея это в виду, давайте посмотрим на код:

public TEntity Find(params object[] keyValues)
{
    return _dbSet.Find(keyValues);
}
public IEnumerable<TEntity> FindAll()
{
    return _dbContext.Set<TEntity>();
}
public void Insert(TEntity entity)
{
    _dbSet.Add(entity);
}
public void Update(TEntity entity)
{
    _dbContext.Entry(entity).State = EntityState.Modified;
}
public void Delete(TEntity entity)
{
    _dbSet.Remove(entity);
}

Как вы понимаете, эта реализация зависит от выбранного ORM. При взгляде на код имеет смысл использовать универсальный подход. Поведение для операции Insert одинаково для всех типов сущностей в базе данных.

Конструктор этого класса довольно прост. Нам просто нужно получить контекст базы данных и создать DbSet экземпляр для типа репозитория.

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
    private readonly DbContext _dbContext;
    private readonly DbSet<TEntity> _dbSet;
    public Repository(DbContext dbContext)
    {
        _dbContext = dbContext;
        _dbSet = _dbContext.Set<TEntity>();
    }
}

Как я могу создавать собственные запросы для чтения некоторых сущностей?

Какой замечательный вопрос! Метод Find недостаточно, потому что нам, вероятно, потребуются более конкретные запросы для некоторых сущностей. Вы можете создавать классы расширений для каждого типа репозитория, в который вы хотите добавить запросы. Для этого нам нужно добавить новый метод в интерфейс IRepository.

public interface IRepository<TEntity>
{
    TEntity Find(params object[] keyValues);
    IEnumerable<TEntity> FindAll();
    void Insert(TEntity entity);
    void Update(TEntity entity);
    void Delete(TEntity entity);
    IQueryable<TEntity> Queryable();
}

Метод Queryable даст нам текущий DbSet экземпляр для конкретного типа репозитория. Посмотрим на реализацию:

public IQueryable<TEntity> Queryable()
{
    return _dbSet;
}

В следующем блоке кода показан пример реализации класса расширения для сущности Person с новыми запросами.

public static class PersonRepository
{
    public static Person FindByIdAndName(
                  this IRepository<Person> repository, 
                  int id, 
                  string name)
    {
        return repository.
               Queryable().
               Where(p => p.Id == id && p.Name == name).
               SingleOrDefault();
    }
}

Единица работы Важность модели

Мы уже видели преимущества использования шаблона репозитория. Однако этот шаблон увеличивает свою производительность, используя также Unit Of Work. Вы можете увидеть подробности этого паттерна в другом моем посте здесь.

Класс Unit Of Work имеет экземпляр DbContext. Таким образом, реализации репозитория не нужно получать его, просто нужно иметь зависимость от Unit Of Work. Давайте посмотрим на код, чтобы прояснить, что я говорю:

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{   
    private readonly IUnitOfWork _unitOfWork;
    private readonly DbContext _dbContext;
    private readonly DbSet<TEntity> _dbSet;
    public Repository(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;        
        _dbContext = unitOfWork.DbContext;
        _dbSet = _dbContext.Set<TEntity>();
    }
}

Как мы видим выше, выделенные жирным шрифтом, изменения, внесенные в класс Repository, были довольно небольшими. Основное отличие состоит в том, что мы получаем доступ к контексту базы данных через Unit Of Work.

Можно ли получить доступ к другому репозиторию в классе расширения репозитория?

Учитывая интерфейсы, представленные до сих пор, ответ отрицательный. Однако, если нам нужен этот тип функции, это просто, если мы уже используем шаблон «Единица работы», как показано ранее.

public interface IRepository<TEntity>
{
    TEntity Find(params object[] keyValues);
    IEnumerable<TEntity> FindAll();
    void Insert(TEntity entity);
    void Update(TEntity entity);
    void Delete(TEntity entity);
    IQueryable<TEntity> Queryable();
    IRepository<TEntity> GetRepository();
}

Чтобы лучше понять реализацию этого метода, я очень рекомендую вам прочитать мой пост о Unit Of Work.

public IRepository<TEntity> GetRepository()
{
    return _unitOfWork.Repository<TEntity>();
}

Поэтому, если нам нужно получить некоторую информацию о другом репозитории в PersonRepository, представленном ранее, нам просто нужно сделать следующее:

public static class PersonRepository
{
    public static bool IsCompanyActive(
                  this IRepository<Person> repository, 
                  Person person)
    {
        return repository.
               GetRepository<Company>().
               Queryable().
               Where(c => c.Id == person.CompanyId).
               SingleOrDefault().IsActive;
    }
}

Как мне зарегистрировать репозитории?

Вам нужно добавить свои репозитории в IServiceColletion, как вы обычно делаете для любого внедрения зависимостей.

services.AddScoped<IRepository<Person>, Repository<Person>>();
services.AddScoped<IRepository<Company>, Repository<Company>>();

Предложение: Если у вас несколько репозиториев, вы можете создать класс расширения для IServiceCollection, чтобы сохранить ответственность за регистрацию репозиториев на уровне репозитория.

private static void AddRepositories(this IServiceCollection services){
    services.AddScoped<IRepository<Person>, Repository<Person>>();
    services.AddScoped<IRepository<Company>, Repository<Company>>();
}

Что мне нужно делать, когда у меня есть новые классы сущностей?

После того, как вы создадите свои сущности, вам просто нужно зарегистрировать новый тип репозитория в вашем реестре внедрения зависимостей. Допустим, у вас есть новый User объект.

private static void AddRepositories(this IServiceCollection services){
    services.AddScoped<IRepository<Person>, Repository<Person>>();
    services.AddScoped<IRepository<Company>, Repository<Company>>();
    services.AddScoped<IRepository<User>, Repository<User>>();
}

Поскольку у вас определена эта новая зависимость, вы можете использовать репозиторий для доступа к сущности User для получения или создания данных из базы данных. Как видите, ваша задача - сосредоточиться на создании новой сущности. Реализация шаблона репозитория предоставляет вам операции CRUD практически бесплатно.

Заключение

Шаблон репозитория позволяет нам сделать наш код более удобным в сопровождении. Если у вас есть какие-либо проблемы с операцией вставки или обновления, вы знаете, что есть только один момент, чтобы проверить неожиданное поведение. Возможность создавать классы расширений для каждого репозитория с целью добавления более конкретных запросов дает нам желаемую расширяемость.

И последнее, но не менее важное: не забывайте о многочисленных преимуществах, которые дает нам реализация Шаблон единицы работы. Это ключ к усилению паттерна репозитория.