Единица работы с 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? Они также получали через инъекцию конструктора.
Я предлагаю переосмыслить это решение.