ПРИМЕЧАНИЕ. В этом ответе говорится о Entity Framework DbContext
, но он применим к любому виду реализации Unit of Work, например LINQ to SQL DataContext
и NHibernate ISession
.
Давайте начнем с того, что повторю Йену: иметь сингл DbContext
для всего приложения - плохая идея. Единственная ситуация, в которой это имеет смысл, - это когда у вас есть однопоточное приложение и база данных, которая используется только этим единственным экземпляром приложения. Он DbContext
не является поточно-ориентированным и, поскольку DbContext
данные кэшируются, довольно скоро устареет. Это доставит вам массу неприятностей, когда несколько пользователей / приложений будут работать с этой базой данных одновременно (что, конечно, очень часто). Но я ожидаю, что вы уже знаете это и просто хотите знать, почему бы просто не внедрить новый экземпляр (то есть с временным образом жизни) в того, DbContext
кто в этом нуждается. (для получения дополнительной информации о том, почему одиночный DbContext
- или даже контекст на поток - плохо, прочитайте этот ответ ).
Позвольте мне начать с того, что регистрация в DbContext
качестве переходного процесса может работать, но, как правило, вы хотите иметь один экземпляр такой единицы работы в определенной области. В веб-приложении может оказаться целесообразным определить такую область на границах веб-запроса; таким образом, на веб-запрос стиль жизни. Это позволяет вам позволить целому набору объектов работать в одном контексте. Другими словами, они работают в рамках одной бизнес-операции.
Если у вас нет цели, чтобы набор операций работал в одном и том же контексте, в этом случае переходный образ жизни хорош, но есть несколько вещей, на которые стоит обратить внимание:
- Поскольку каждый объект получает свой собственный экземпляр, каждый класс, который изменяет состояние системы, должен вызываться
_context.SaveChanges()
(в противном случае изменения будут потеряны). Это может усложнить ваш код и добавить к нему вторую ответственность (ответственность за контроль контекста) и является нарушением принципа единой ответственности .
- Вы должны убедиться, что сущности [загруженные и сохраненные
DbContext
] никогда не покидают область действия такого класса, потому что они не могут использоваться в экземпляре контекста другого класса. Это может сильно усложнить ваш код, потому что, когда вам нужны эти объекты, вам нужно загрузить их снова по id, что также может вызвать проблемы с производительностью.
- Поскольку
DbContext
реализует IDisposable
, вы, вероятно, все еще хотите избавиться от всех созданных экземпляров. Если вы хотите сделать это, у вас есть два варианта. Вы должны располагать их в том же методе сразу после вызова context.SaveChanges()
, но в этом случае бизнес-логика становится владельцем объекта, который передается извне. Второй вариант заключается в удалении всех созданных экземпляров на границе запроса Http, но в этом случае вам все еще требуется некоторая область видимости, чтобы сообщить контейнеру о необходимости удаления этих экземпляров.
Другой вариант - вообще не вводить DbContext
. Вместо этого вы вводите, DbContextFactory
который может создать новый экземпляр (я использовал этот подход в прошлом). Таким образом, бизнес-логика явно контролирует контекст. Если может выглядеть так:
public void SomeOperation()
{
using (var context = this.contextFactory.CreateNew())
{
var entities = this.otherDependency.Operate(
context, "some value");
context.Entities.InsertOnSubmit(entities);
context.SaveChanges();
}
}
Плюсом этого является то, что вы управляете жизнью DbContext
явно, и это легко настроить. Это также позволяет вам использовать один контекст в определенной области, который имеет явные преимущества, такие как выполнение кода в одной бизнес-транзакции и возможность передавать объекты, поскольку они происходят из одного и того же DbContext
.
Недостатком является то, что вам придется переходить DbContext
от метода к методу (который называется инъекцией метода). Обратите внимание, что в некотором смысле это решение аналогично подходу «scoped», но теперь область действия контролируется в самом коде приложения (и, возможно, повторяется много раз). Это приложение, которое отвечает за создание и распоряжение единицей работы. Так как DbContext
объект создается после построения графа зависимостей, добавление в конструктор находится вне пределов видимости, и вам необходимо перейти к внедрению метода, когда вам нужно передать контекст из одного класса в другой.
Инъекция метода не так уж и плоха, но когда бизнес-логика усложняется и в нее вовлекается больше классов, вам придется передавать ее из метода в метод и из класса в класс, что может сильно усложнить код (я видел это в прошлом). Однако для простого приложения этот подход подойдет.
Из-за недостатков этот фабричный подход для больших систем может быть полезен другой подход, в котором вы позволяете контейнеру или коду инфраструктуры / Composition Root управлять единицей работы. Это стиль, о котором ваш вопрос.
Позволяя контейнеру и / или инфраструктуре обрабатывать это, код вашего приложения не загрязняется необходимостью создавать, (необязательно) фиксировать и утилизировать экземпляр UoW, что делает бизнес-логику простой и чистой (просто единая ответственность). Есть некоторые трудности с этим подходом. Например, вы фиксировали и утилизировали экземпляр?
Распоряжение единицей работы может быть сделано в конце веб-запроса. Однако многие люди ошибочно полагают, что это также место для фиксации единицы работы. Однако в этот момент в приложении вы просто не можете точно определить, что единица работы действительно должна быть зафиксирована. Например, если код бизнес-уровня выдал исключение, которое было перехвачено выше стека вызовов, вы определенно не хотите совершать .
Реальное решение снова состоит в том, чтобы явно управлять некоторой областью действия, но на этот раз сделайте это внутри корня композиции. Абстрагируясь от всей бизнес-логики, лежащей в основе шаблона команда / обработчик , вы сможете написать декоратор, который можно обернуть вокруг каждого обработчика команды, который позволяет это делать. Пример:
class TransactionalCommandHandlerDecorator<TCommand>
: ICommandHandler<TCommand>
{
readonly DbContext context;
readonly ICommandHandler<TCommand> decorated;
public TransactionCommandHandlerDecorator(
DbContext context,
ICommandHandler<TCommand> decorated)
{
this.context = context;
this.decorated = decorated;
}
public void Handle(TCommand command)
{
this.decorated.Handle(command);
context.SaveChanges();
}
}
Это гарантирует, что вам нужно написать этот код инфраструктуры только один раз. Любой твердотельный DI-контейнер позволяет вам настроить такой декоратор так, чтобы он ICommandHandler<T>
согласованно оборачивался вокруг всех реализаций.