ПРИМІТКА: Ця відповідь говорить про цілісні рамки Entity Framework DbContext
, але вона застосовна для будь-якої реалізації Блоку роботи, наприклад LINQ для SQL DataContext
та NHibernate ISession
.
Почнемо з наголосу Ian: Мати сингла DbContext
для всієї програми - погана ідея. Єдина ситуація, коли це має сенс, коли у вас є однопотоковий додаток та база даних, яка використовується виключно цим єдиним екземпляром програми. Це DbContext
не є безпечним для потоків, і, оскільки DbContext
кешує дані, він стає несвіжим досить скоро. Це доставить вам всілякі проблеми, коли на цій базі даних одночасно працюють декілька користувачів / додатків (що, звичайно, дуже часто). Але я сподіваюся, що ви вже це знаєте, і просто хочете знати, чому б не просто ввести новий екземпляр (тобто з перехідним способом життя) того, DbContext
хто цього потребує. (для отримання додаткової інформації про те, чому один DbContext
- або навіть за контекстом на потоку - поганий, прочитайте цю відповідь ).
Почніть з того, що реєстрація DbContext
як перехідного періоду може працювати, але, як правило, ви хочете мати один екземпляр такої одиниці роботи в певному обсязі. У веб-додатку може бути практичним визначення такої сфери застосування на межах веб-запиту; таким чином спосіб життя за запитом на веб. Це дозволяє дозволити цілому набору об'єктів діяти в одному контексті. Іншими словами, вони діють в межах однієї ділової операції.
Якщо у вас немає мети, щоб набір операцій діяв у тому ж контексті, у такому випадку перехідний спосіб життя прекрасний, але слід спостерігати кілька речей:
- Оскільки кожен об'єкт отримує власний екземпляр, кожному класу, який змінює стан системи, потрібно викликати
_context.SaveChanges()
(інакше зміни втратяться). Це може ускладнити ваш код, і додасть до нього другу відповідальність (відповідальність за контроль за контекстом) і є порушенням Принципу єдиної відповідальності .
- Вам потрібно переконатися, що сутності [завантажені та збережені a
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
від методу до методу (який називається Методом ін'єкції). Зауважте, що в певному сенсі це рішення є тим самим, що і "підхідний" підхід, але тепер сфера застосування контролюється в самому коді програми (і можливо повторюється багато разів). Саме додаток відповідає за створення та розпорядження одиницею роботи. Оскільки значення DbContext
створено після побудови графіка залежності, інжекція конструктора виходить із зображення, і вам потрібно перейти до методу ін'єкції, коли вам потрібно передати контекст від одного класу до іншого.
Інжекція методів не така вже й погана, але коли бізнес-логіка стає більш складною, і більше класів буде задіяно, вам доведеться передати її від методу до методу та класу до класу, що може значно ускладнити код (я бачив це в минулому). Для простого застосування, цей підхід буде добре, хоча.
Через недоліки цей заводський підхід є для більш великих систем, інший підхід може бути корисним, і це той, де ви дозволяєте контейнеру або інфраструктурному коду / Composite Root керувати одиницею роботи. Це стиль, про який йдеться у вашому питанні.
Дозволяючи контейнеру та / або інфраструктурі впоратися з цим, ваш додаток не забруднюється необхідністю створювати, (необов'язково) виконувати і видаляти екземпляр UoW, який забезпечує простоту та чистоту бізнес-логіки (лише Єдина відповідальність). Є певні труднощі з таким підходом. Наприклад, чи вчинили ви та розпоряджалися екземпляром?
Розпорядження одиницею роботи може бути виконано в кінці веб-запиту. Однак багато людей помилково припускають, що це також місце для здійснення одиниці роботи. Однак, на цьому етапі програми ви просто не можете точно визначити, що одиниця роботи має бути фактично виконана. Наприклад, якщо код бізнес-шару викинув виняток, який потрапив вище в стопку виклику, ви точно не хочете взяти на себе зобов’язання.
Справжнє рішення знову полягає в тому, щоб явно керувати якоюсь сферою дії, але цього разу зробити це всередині Composite Root. Абстрагуючи всю ділову логіку за схемою команд / обробника , ви зможете написати декоратор, який можна обернути навколо кожного обробника команд, який дозволяє це зробити. Приклад:
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>
послідовно обертатися навколо всіх реалізацій.