Как универсально создавать репозитории, которые наследуются от универсального типа?

В настоящее время я пытаюсь реализовать шаблон репозитория поверх моего DbContext. Дело в том, что я в конечном итоге сталкиваюсь с ситуацией, когда мне приходится вводить несколько репозиториев в UnitOfWork конструктор, вот так:

public class UnitOfWork
{
    private DbContext _context;
    ICustomerRepository Customers { get; private set; }
    IEmployeeRepository Employees { get; private set; } 
    public UnitOfWork(DbContext context, ICustomerRepository cust, IEmployeeRepository emp)
    {
        _context = context;
        Customers = cust;
        Employees = emp;
    }    
}

Тем не менее, поскольку все они должны были бы использовать один и тот же DbContext, я не вижу возможности их внедрения.

Вот почему я думал о создании RepositoryFactory класс, но так как все репозитории наследуются от универсального интерфейса, я обнаружил, что невозможно создать один Create() метод, потому что, в конце концов, у них нет реального общего предка, который был бы допустимым типом возврата.

Чтобы дать вам больше понимания, вот как выглядит код:

public interface IRepository<TEntity> where TEntity:class
{
    TEntity Get(int id);
    IEnumerable<TEntity> GetAll();
}

public interface ICustomerRepository : IRepository<Customer>
{
    IEnumerable<Customer> GetSeniorCustomers();
}

public class CustomerRepository : ICustomerRepository
{
    private readonly DbContext _context;
    public CustomerRepository(DbContext context) : base(context)
    {
        _context = context;
    }
// ... implementation of ICustomerRepo here
}

Теперь, это текущее состояние вещей:

И то, что я хотел бы сделать, это:

public UnitOfWork(DbContext context, RepositoryFactory fac)
{
     _context = context;
     Customers = fac.Create(context, RepoType.Customer);
     Employees = fac.Create(context, RepoType.Employee);
}  

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

Но, как я упоминал ранее, я не могу придумать допустимый тип возвращаемого значения для метода Create().

Итак, у меня возникла идея создать несколько методов внутри класса RepositoryFactory вместо одного параметризованного, например:

public class RepositoryFactory
{
    public ICustomerRepository CreateCustomerRepo(DbContext context){/*...*/}
    public IEmployeeRepository CreateEmployeeRepo(DbContext context){/*...*/} 
}

Итак, вопросы:

  1. Может ли то, что я делаю, даже называться фабричным методом?
  2. Если нет, то является ли это как минимум верным решением? Если нет, как я могу добиться того же самого более чистым способом?

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

Спасибо за всю помощь заранее.

3 ответа

Решение

Ваше решение "множественные методы внутри класса RepositoryFactory" весьма неплохо, если вам не очень важен принцип Open/Close. Если это соответствует вашим потребностям, вы можете пойти с этим.

Может ли то, что я делаю, даже называться фабричным методом?

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

Если нет, то является ли это как минимум верным решением?

Как сказано выше, если это соответствует вашим потребностям; да, это действительно.

Если нет, как я могу добиться того же самого более чистым способом?

Другой альтернативой является фабричный метод, подобный приведенному ниже:

public T CreateRepository<T>() where T : IRepositoryId
{
    IRepositoryId repository = null;
    if(typeof(T) == typeof(ICustomerRepository))
        repository = new CustomerRepository(context);
    ......
    ......
    else
        throw new XyzException("Repository type is not handled.");
    return (T)repository;
}

public interface IRepositoryId
{
    Guid RepositoryId { get; }
}

Ваш существующий IRepository интерфейс является общим. Это создает проблемы при реализации вышеуказанного метода с этим интерфейсом. Итак, просто создайте другой интерфейс, как показано выше. Получите каждый репозиторий из этого интерфейса.

Во-первых, будьте ясны в отношении вашей цели. Шаблон репозитория имеет (как минимум) 3 основных причины существования:

1) Абстрагировать слой данных. Что касается EF, то если это ваша цель, то шаблон репозитория больше не является полезным. Попытка абстрагировать приложения от Entity Framework - гораздо больше проблем, чем оно того стоит. В конечном итоге вы получите либо / или урезанный DAL, в котором недоступна внушаемость EF, либо неэффективный / медленный, либо очень сложный набор методов репозитория с выражениями и другими неприятностями в качестве параметров. Попытки абстрагировать ваше приложение от EF (на случай, если вы захотите, например, перейти на другой ORM) бессмысленно. Примите EF так же, как вы приняли бы тот факт, что вы пишете свое заявление в.Net. Чтобы абстрагировать EF до такой степени, что его можно заменить, вы также можете не использовать его, потому что вы не увидите никаких преимуществ, которые EF может на самом деле предоставить.

2) Сделать бизнес-логику проще для тестирования. IMO Это все еще допустимый аргумент для репозиториев с Entity Framework. Да, EF DbContexts можно высмеивать, но это все еще грязный бизнес. Пересмешивать репозитории не сложнее, чем любая другая зависимость.

3) служить доменным привратником. Шаблоны, такие как DDD, пытаются заблокировать действия с данными в объектах и ​​сервисах домена. Шаблон репозитория может помочь с этим при использовании EF, чтобы помочь содержать методы, которые отвечают за манипулирование доменом. Для чистого DDD я бы не рекомендовал их, хотя я бы не рекомендовал использовать Entities в качестве объектов домена DDD. Я использую шаблон репозитория для управления CR и D. аспектами CRUD и полагаюсь на модели представлений для инкапсуляции логики домена вокруг U.

Шаблон репозитория, который, как я обнаружил, наиболее часто используется для обслуживания точек 2 и 3, состоит в том, чтобы покончить с очень распространенной концепцией универсальных репозиториев, и скорее относиться к репозиторию более в соответствии с тем, как вы будете обращаться с контроллером в MVC. За исключением случаев, когда между View и Model, между Model и Data. Репозиторий - это класс, который обслуживает контроллер (в MVC), поскольку он отвечает за создание, чтение (на уровне ядра) и удаление сущностей. Этот шаблон очень хорошо работает в сочетании с единицей работы. ( https://github.com/mehdime/DbContextScope - это реализация, которую я принимаю.)

При создании объекта он отвечает за обеспечение предоставления всех необходимых (ненулевых) значений и ссылок, возвращая объект, связанный с DbContext, готовым к работе. Эффективно предприятие сущности. Вы можете поспорить о разделении интересов, хотя, учитывая, что репозиторий уже имеет доступ к DbContext для извлечения связанных сущностей, это случай, когда он в значительной степени является лучшим местом для работы.

В объектах чтения он предоставляет базовую ссылку на другие объекты запроса, предоставляя IQueryable<TEntity>, соблюдение правил базового уровня, таких как .Where(x => x.IsActive) для сценариев мягкого удаления или фильтров для аутентификации / авторизации / аренды на основе зависимостей, которые могут раскрыть, например, текущего пользователя. Выставляя IQueryable вы сохраняете реализацию репозитория простой и предоставляете потребителю (контроллеру) контроль над тем, как используются данные. Это может использовать отложенное выполнение для:

  • Выберите только данные, необходимые для просмотра моделей.
  • Выполните подсчет и существуют проверки. (.Any())
  • Настройте логику фильтрации в структуре объекта для конкретного варианта использования.
  • Выполните нумерацию страниц.
  • Получите столько данных (.ToList(), .Take()) или столько данных (.SingleOrDefault(), .FirstOrDefault()), сколько необходимо.

Методы чтения чрезвычайно легко смоделировать, и поэтому объем реализации репозитория достаточно мал. Потребители должны знать, что они имеют дело с сущностями, и иметь дело с нюансами EF и это прокси, но потребитель является хранителем отдела работы (продолжительности жизни DbContext) так скрывая этот факт от него довольно спорного вопроса, Передача сложных выражений запросов в методы репозитория в качестве параметров оставляет потребителей в равной степени ответственными за знание нюансов EF. Выражение, которое вызывает закрытый метод, передаваемый в общий репозиторий для перехода в предложение Where, будет так же быстро разрушать вещи. Спускаясь по маршруту 1, выше, не отвлекайтесь от своего приложения.

При удалении объекта он обеспечивает правильное управление объектами и их ассоциациями, как жесткими, так и программными.

Я избегаю универсальных репозиториев, потому что так же, как контроллер (и представление) будет иметь дело с любым количеством связанных моделей представления домена, это означает, что им потребуется иметь дело с рядом связанных объектов данных. Операции против какой-либо одной сущности всегда будут связаны с операциями против других иждивенцев. В универсальных репозиториях это разделение означает: а) постоянно растущее число зависимостей и б) универсальные методы, которые выполняют тривиальную ерунду, и много пользовательского кода для обработки значимых вещей, или сложный код, чтобы попытаться упростить его в общем (базовом) виде, Наличие одного репозитория на контроллер и, возможно, некоторых действительно общих общих репозиториев для общих объектов. (например, поиск). Мои репозитории специально разработаны для обслуживания одной области приложения и имеют только одну причину для изменения. Конечно, может быть 2 или более экранов, которые требуют одинакового поведения от репозитория, но по мере развития этих аспектов приложения или службы их репозитории могут обновляться / оптимизироваться по мере необходимости без побочных эффектов. SRP и KISS легко превзойти DNRY.

Общие классы в целом имеют свое применение, но почти во всех случаях, когда я вижу, что разработчики пишут их, я бы сказал, что это преждевременная оптимизация. Начните с неуниверсальных реализаций, затем, по мере развития продукта, оптимизируйте генерики в код, а не пытайтесь проектировать архитектуру вокруг них. Результатом почти всегда является осознание того, что вам необходимо де-оптимизировать их или получить "умный" способ обойти обнаруженные ограничения, когда шаблоны мешают разработке.

Во всяком случае, некоторая пища для размышлений, кроме "репозитории не нужны с EF":)

1) Я думаю, что это не заводской метод. 2) пожалуйста, ищите информацию о DAO и ее различиях с хранилищем, я думаю, вам нужно использовать DAO. 3) Я поставил пример кода для единицы работы с EF, надеюсь, это будет полезно

public class UnitOfWork : IUnitOfWork, IDisposable
{
    private readonly ApplicationDbContext _context;
    public UnitOfWork()
    {
        if (_context == null)
            _context = new ApplicationDbContext();

        _context.ChangeTracker.AutoDetectChangesEnabled = false;
    }

    public UnitOfWork(ApplicationDbContext context)
    {
        _context = context;
    }

    public ApplicationDbContext Context => _context;
    public DbSet<T> Set<T>() where T : class
    {
        return _context.Set<T>();
    }

    public async Task<int> SaveChangesAsync()
    {
        return await _context.SaveChangesAsync();
    }

    private bool disposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                _context.Dispose();
            }
        }
        this.disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public int SaveChanges()
    {
         return _context.SaveChanges();
    }
}

и вы можете использовать это так:

public class DomesticStatusService : IDomesticStatusService
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly DbSet<DomesticStatus> _domesticStatuses;

    public DomesticStatusService(IUnitOfWork uow)
    {
        _unitOfWork = uow;
        _domesticStatuses = uow.Set<DomesticStatus>();
    }

    public async Task<List<ValueText>> SearchDomesticStatus()
    {
        return await _domesticStatuses.Select(c => new ValueText(c.Id, c.Name)).ToListAsync();
    }
}
Другие вопросы по тегам