Единица работы с EF 6 и инъекцией зависимостей Проблемы проектирования

Я разрабатываю веб-приложение с инфраструктурой сущностей 6 и испытываю трудности с проектированием структуры приложения. Моя основная проблема заключается в том, как бороться с инъекцией зависимостей в моем конкретном случае.

Ниже приведен код, как хотелось бы, чтобы приложение выглядело. Я использую Autofac, но я думаю, это достаточно просто для каждого пользователя DI:

public interface IUnitOfWork
{
    bool Commit();
}

public class UnitOfWork : IUnitOfWork, IDisposable
{
    private DbContext _context;

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

    public bool Commit()
    {
        // ..
    }

    public bool Dispose()
    { 
          _context.Dispose();
    }
}

public class ProductsController : ApiController 
{
     public ProductsController(IProductsManager managet)
}   


public class ProductsManager : IProductsManager
{
    private Func<Owned<IUnitOfWork>> _uowFactory;
    private IProductsDataService _dataService;

    public Manager(Func<Owned<IUnitOfWork>> uowFactory, IProductsDataService dataService)
    {
        _dataService = dataService;
        _uowFactory = uowFactory;
    }

    public bool AddProduct(ProductEntity product)
    {
        using (ownedUow = _uowFactory())
        {
            var uow = ownedUow.Value;

            var addedProduct = _dataService.AddProduct(product);

            if (addedProduct != null)
                uow.Commit();
        }
    }
}

public interface IProductsDataService
{
    ProductEntity AddProduct (Product product)
}

public class ProductsDataService : IProductsDataService 
{
    private IRepositoriesFactory _reposFactory;

    public DataService(IRepositoriesFactory reposFactory)
    {
        _reposFactory = reposFactory;
    }

    public ProductEntity AddProduct(ProductEntity product)
    {
        var repo = reposFactory.Get<IProductsRepository>();

        return repo.AddProduct(product);
    }
}


public interface IRepositoriesFactory
{
    T Get<T>() where T : IRepository
}

public class RepositoriesFactory : IRepositoriesFactory
{
    private ILifetimeScope _scope;

    public RepositoriesFactory(ILifetimeScope scope)
    {
        _scope = scope;
    }

    public T Get<T>() where T : IRepository
    {
        return _scope.Resolve<T>();
    }

}

public interface IProductsRepository
{
    ProductEntity AddProduct(ProductEntity);
}


public ProductsRepository : IProductsRepository
{
    private DbContext _context;

    public ProductsRepository(DbContext context)
    {
        _context = context;
    }

    public ProductEntity AddProduct(ProductEntity)
    {
        // Implementation..
    }
}

Это реализация, которую я нахожу идеальным, однако я не знаю, как это сделать, потому что my ProductsDataService является singleton, поэтому он не связан с областью Owned, созданной подразделением работ factory. Есть ли способ связать созданные Хранилища и взять в их ctor тот же DbContext, который был создан для единицы работы? Как-то изменить код в RepositoriesFactory?

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

Я знаю, что могу применить метод UnitOfWork к методам DataService, но я бы предпочел использовать инъекцию Ctor, поскольку это выглядит лучше, на мой взгляд.

Я хочу отделить это: менеджер, который должен выполнять экземпляр единицы работ и, при необходимости, передавать их, а другой класс (DataService), который фактически выполняет логику.

Независимо от того, я хотел бы услышать ваше мнение об этой реализации, если у вас есть комментарии/идеи для улучшения.

Спасибо за ваше время!

EDIT: Это то, с чем я закончил:

public interface IUnitOfWork
{
    bool Commit();
}

public class DatabaseUnitOfWork : IUnitOfWork
{
    private DbContext _context;

    public DatabaseUnitOfWork(DbContext context)
    {
        _context = context;
    }

    public bool Commit()
    {
        // ..
    }
}

// Singleton
public class ProductsManager : IProductsManager
{
    private Func<Owned<IProductsDataService>> _uowFactory;

    public ProductsManager(Func<Owned<IProductsDataService>> uowFactory)
    {
        _uowFactory = uowFactory;
    }

    public bool AddProduct(ProductEntity product)
    {
        using (ownedUow = _uowFactory())
        {
            var dataService = ownedUow.Value;

            var addedProduct = _dataService.AddProduct(product);

            if (addedProduct != null)
                uow.Commit();
        }
    }
}

public interface IProductsDataService : IUnitOfWork
{
    ProductEntity AddProduct (Product product)
}

public class ProductsDataService : DatabaseUnitOfWork, IDataService 
{
    private IRepositoriesFactory _reposFactory;

    public DataService(IRepositoriesFactory reposFactory)
    {
        _reposFactory = reposFactory;
    }

    public ProductEntity AddProduct(ProductEntity product)
    {
        var repo = _reposFactory .Get<IProductsRepository>();

        return repo.AddProduct(product);
    }
}


public interface IRepositoriesFactory
{
    Get<T>() where T : IRepository
}

public class RepositoriesFactory : IRepositoriesFactory
{
    private ILifetimeScope _scope;

    public RepositoriesFactory(ILifetimeScope scope)
    {
        _scope = scope;
    }

    public Get<T>() where T : IRepository
    {
        return _scope.Resolve<T>();
    }

}

public interface IProductsRepository
{
    ProductEntity AddProduct(ProductEntity);
}


public ProductsRepository : IProductsRepository
{
    private DbContext _context;

    public ProductsRepository(DbContext context)
    {
        _context = context;
    }

    public ProductEntity AddProduct(ProductEntity)
    {
        // Implementation..
    }
}

Ответы

Ответ 1

Я согласен с советом Бруно Гарсиа о проблеме с вашим кодом. Однако я вижу несколько других проблем.

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

Проблема, с которой Бруно не попала, заключается в том, что у вас плохое разделение проблем. Он немного намекнул на это, и я объясню больше: у вашего контроллера есть два отдельных конкурирующих объекта, которые пытаются использовать один и тот же ресурс (DbContext). По его словам, то, что вы хотите сделать, это иметь только один DbContext для каждого запроса. Там проблема с этим: хотя нет ничего, что помешало бы Контроллеру пытаться продолжать использовать ProductRepository после удаления UnitOfWork. Если вы это сделаете, соединение с базой данных уже удалено.

Поскольку у вас есть два объекта, которые должны использовать один и тот же ресурс, вы должны перестроить его, где один объект инкапсулирует другой. Это также дает дополнительную выгоду от того, чтобы скрывать от Controller любые проблемы вообще о распространении данных.

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

Один из других способов, которыми вы могли бы обойти это, - это получить ProductRepository от UnitOfWork, чтобы вам не пришлось беспокоиться о дублированном коде.

Затем, в вашем методе AddProduct, вы должны вызвать _context.SaveChanges(), прежде чем возвращать этот объект обратно по конвейеру к вашему контроллеру.

ОБНОВЛЕНИЕ (фигурные скобки для компактности)

Вот макет того, что вы хотите сделать:

UnitOfWork - это ваш самый нижний слой, который включает подключение к базе данных. Однако сделайте это abstract, поскольку вы не хотите допускать его конкретную реализацию. Вам больше не нужен интерфейс, поскольку то, что вы делаете в своем методе Commit, никогда не должно быть раскрыто, а сохранение объекта должно выполняться в рамках методов. Я покажу, как по линии.

public abstract class UnitOfWork : IDisposable {
    private DbContext _context;

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

    protected bool Commit() {
        // ... (Assuming this is calling _context.SaveChanges())
    }

    public bool Dispose() {
        _context.Dispose();
    }
}

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

public interface IProductsRepository {
    ProductEntity AddProduct(ProductEntity product);
}

public ProductsRepository: UnitOfWork, IProductsRepository {
    public ProductsRepository(DbContext context) : base(context) { }

    public ProductEntity AddProduct(ProductEntity product) {
        // Don't forget to check here. Only do that where you're using it.
        if (product == null) {
            throw new ArgumentNullException(nameof(product));
        }

        var newProduct = // Implementation...

        if (newProduct != null) {
            Commit();
        }

        return newProduct;
    }
}

С этим на месте все, о чем вы заботитесь сейчас, это просто наличие ваших продуктовRepository. На вашем уровне DataService используйте Injection Dependency Injection и просто передайте сам ProductRepository. Если вы действительно настроены на использование factory, а затем передайте factory, но ваша переменная-член еще будет IProductsRepository. Не заставляйте каждый метод учитывать это.

Не забывайте, что все ваших интерфейсов получаются из IDisposable

public interface IProductsDataService : IDisposable {
    ProductEntity AddProduct(ProductEntity product);
}

public class ProductsDataService : IProductsDataService {
    private IProductsRepository _repository;

    public ProductsDataService(IProductsRepository repository) {
        _repository = repository;
    }

    public ProductEntity AddProduct(ProductEntity product) {
        return _repository.AddProduct(product);
    }

    public bool Dispose() {
        _repository.Dispose();
    }
}

Если вы устарели, используя ProductsManager, вы можете, но это просто еще один слой, который больше не приносит большой пользы. То же самое было бы с этим классом.

Я закончу с вашим контроллером, как и я.

public class ProductsController : Controller {
    private IProductsDataService _service;

    public ProductsController(IProductsDataService service) {
        _service = service;
    }

    protected override void Dispose(bool disposing) {
        _service.Dispose();

        base.Dispose(disposing);
    }

    // Or whatever you're using it as.
    [HttpPost]
    public ActionResult AddProduct(ProductEntity product) {
        var newProduct = _service.AddProduct(product);

        return View(newProduct);
    }
}

Ответ 2

Вы не хотите использовать singleton DbContext в одном экземпляре. Это нормально, это можно сделать с помощью factory. Кроме того, вы хотите поделиться этим DbContext. Это также хорошо, вы можете разрешить и вернуть DbContext со связанным сроком службы на factory. Проблема; вы хотите совместно использовать не-singleton DbContext в одном экземпляре без управления временем жизни (запрос Tcp/Ip).

Какая причина ProductService и ProductManager является одиночной? Я предлагаю вам использовать ProductService и ProductManager на каждый период жизни. Когда у вас есть HTTP-запрос, это хорошо. Когда у вас есть запрос tcp/ip, вы можете начать новую область жизни (как можно более высокий уровень), а затем разрешите ProductManager.

Обновить: Код для решения 1, о котором я упомянул в комментариях.

Managers должны быть одноточечными (как вы сказали).

Кроме Managers, вы должны зарегистрировать DbContext, services, repositories и Uow как область per lifetime.

Мы могли бы инициализировать так:

public class ProductsManager : IProductsManager
    {
        //This is kind of service locator. We hide Uow and ProductDataService dependencies.
        private readonly ILifetimeScope _lifetimeScope;

        public ProductsManager(ILifetimeScope lifetimeScope)
        {
            _lifetimeScope = lifetimeScope;
        }
    }

Но это своего рода локатор сервисов. Мы скрываем зависимости Uow и ProductDataService.

Итак, мы должны реализовать поставщика:

public IProductsManagerProvider : IProductsManager
{

}
public class ProductsManagerProvider : IProductsManagerProvider
{
    private readonly IUnitOfWork _uow;
    private readonly IProductsDataService _dataService;

    public ProductsManagerProvider (IUnitOfWork uow, IProductsDataService dataService)
    {
        _dataService = dataService;
        _uow = uow;
    }

    public bool AddProduct(ProductEntity product)
    {
        var result=false;
        var addedProduct = _dataService.AddProduct(product);
        if (addedProduct != null)
            result=_uow.Commit()>0;
        return result;
    }
}

И мы регистрируем его как per dependency (потому что мы будем использовать его с factory).

container.RegisterType<ProductsManagerProvider>().As<IProductsManagerProvider>().InstancePerDependency();

Ваш класс ProductsManager должен быть таким. (Теперь мы не скрываем никаких зависимостей).

public class ProductsManager : IProductsManager
{
    private readonly Func<Owned<IProductsManagerProvider>> _managerProvider;
    //Now we don't hide any dependencies.
    public ProductsManager(Func<Owned<IProductsManagerProvider>> managerProvider)
    {
        _managerProvider = managerProvider;
    }

    public bool AddProduct(ProductEntity product)
    {
        using (var provider = _managerProvider())
        {
            return provider.Value.AddProduct(product);
        }
    }
}

Я тестировал свои классы.

У вас есть экземпляр менеджера oneton, у которого есть factory, чтобы создать поставщика управляющего. Поставщики-диспетчеры зависят от зависимости, потому что каждый раз, когда мы должны получать новый экземпляр в singleton. Все в провайдерах за всю жизнь, поэтому их продолжительность жизни связана с провайдерами на время жизни зависимых.

Когда вы добавляете товар в менеджер Container создает 1 Provider, 1 DbContext, 1 DataService и 1 Uow (DbContext является общим). Provider располагается (зависимость) со всеми реальными экземплярами (DbContex, Uow, DataService) после возврата метода в Manager.

Ответ 3

Кажется, проблема не в том, чтобы убедиться, что экземпляр DbContext, введенный в UnitOfWork и ProductsRepository, тот же.

Это может быть достигнуто путем регистрации DbContext как InstancePerLifetimeScope и создания нового LifetimeScope перед разрешением IUnitOfWork и ProductsRepository. Любая зависимость, не принадлежащая вам, будет удалена во время удаления LifetimeScope.

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

Там нет прямой связи между вашим UoW и репозиториями. Это не похоже на шаблон UoW.

Я не мог понять, кто собирается Dispose IRepository, созданный вашим IRepositoryFactory. Вы используете контейнер для его разрешения (через ILifetimeScope, который вы ввели в RepositoriesFactory). Если только тот, кто получает этот экземпляр из Factory, не располагает, он будет удален только путем размещения LifetimeScope, введенного в IRepositoryFactory.

Другая проблема, которая возникла бы, - это право собственности на DbContext. Вы можете утилизировать его в этом блоке using через Dispose на IUnitOfWork. Но ваш UnitOfWork тоже не имеет этого экземпляра. Контейнер. Будут ли репозитории также пытаться избавиться от DbContext? Они также получали через инъекцию конструктора.

Я предлагаю переосмыслить это решение.