У какого объекта есть право извлекать объекты из DI контейнера?
Осваиваю DI-контейнеры и нюансы их применения в приложении (на примере контейнера Autofac). Приложение десктопное.
В принципе, ничего сложного в теме нет, кроме одного нюанса. Взаимодействие с DI происходит в юзингах - создается scope, в нем извлекается из DI нужный объект, выполняется задача и затем все созданные объекты автоматически диспосятся.
Если программа простая, то такой using может быть всего один (прямо в методе Main), и все объекты созданные в нем живут всё время жизни программы. Но если модель сложная и структурированная, то нет смысла держать ее в памяти всю целиком, а лучше восстанавливать из DI отдельные ее части, необходимые для выполнения текущей задачи, по мере необходимости, и затем сразу от них избавляться с сохранением результата (на сколько я знаю, в вебе это стандарт - 1 scope на 1 http-запрос).
По логике вещей это должно быть организовано в виде методов, каждый из которых будет создавать свой локальный scope для выполнения задачи. Так вот, объект, в котором будут все эти методы, по сути напрямую взаимодействует с DI, должен получать его в конструктор, и вообще это очень напоминает Service Locator.
Но дело-то в том, что такой объект должен где-то быть, т.к. точки входа сами себя не создадут! И находиться он должен, учитывая специфику его работы, где-то очень близко от метода Main и жить всё время жизни программы.
Если отталкиваться от принципов DDD, то ни один слой приложения не подходит под описание такого объекта, и вообще никто из них не должен ничего знать про DI. А посему, не смотря на то что понимание принципа работы и предназначения DI-контейнеров у меня есть, я совершенно не понимаю как разрешить вышеописанную проблему, или в чем и почему я ошибаюсь, предполагая необходимость наличия отдельного объекта для создания точек входа.
Помогите, пожалуйста, разобраться, кто на самом деле имеет право извлекать из DI объекты. Заранее спасибо!
Ответ найден в статьях Марка Симана:
- Про Composition Root: https://blog.ploeh.dk/2011/07/28/CompositionRoot/
- Про создание объектного графа: https://blog.ploeh.dk/2011/03/04/Composeobjectgraphswithconfidence/
Ответы (2 шт):
При использовании DI базовое понятие - Composition Root (корень композиции). Это самое начало приложения, где определяются все объекты (зависимости), которые будут использоваться приложением. Они помещаются в DI-контейнер. После чего вызывается стартовый метод приложения, куда передаётся единственный объект из контейнера. Этот объект является графом, содержащим все зависимости. Всё, после этого DI-контейнер становится не нужен и прекращает своё существование. (Вернее, так должно быть в чистом DI).
Далее по ходу выполнения программы зависимости извлекаются из графа. Но при таком прямолинейном подходе получается, что все они должны быть созданы сразу, ещё в Composition Root. Это накладно как по времени, так и по памяти (в крупном приложении вообще может быть невозможно). Поэтому приходится так или иначе создавать многие сервисы отложенно.
Делать это можно двумя путями. Либо "старый-добрый" (да будь он проклят в веках!) Service Locator - по этому пути пошли в Microsoft, потому что индусокодеры "не осилили"...
Либо использовать фабрики. Фабрика в данном случае легковесный объект. В ней может быть всего один метод: CreateSomeService
. Приняв фабрику на входе, далее вызываем этот метод и получаем тяжёлый сервис.
Зачем это нужно? Все тяжёлые сервисы поместить в граф невозможно, а лёгких фабрик можно напихать сколько угодно.
Вы же используете Autofac? В Autofac есть набор фич, которые делают как раз то что вам нужно.
Если вам нужно получать зависимость не сразу, а по запросу - то подойдёт делегат Func<T>
из стандартной библиотеки, который разрешается Autofac в обращение к контейнеру без упоминания этого самого контейнера, а потому может использоваться в любом слое:
public class Foo(Func<Bar> barAccessor) {
// …
Bar bar = barAccessor();
}
Главное - не совершайте архитектурную ошибку и не считайте что подобный способ внедрения зависимостей даёт вам какую-то гарантию относительно уникальности (либо неуникальности) возвращённого значения.
Одним из сценариев, при котором подобный акцессор может понадобиться - это ленивое внедрение зависимости. Однако, для него есть обёртка проще - Lazy<T>
, тоже из стандартной библиотеки.
Если вам нужна зависимость в отдельном скоупе - для этого есть отдельная обёртка Owned<T>
:
public class Foo(Owned<Bar> barOwned) {
// …
Bar bar = barOwned.Value;
// …
barOwned.Dispose(); // Любые Owned надо прибирать!
}
Чаще всего этот Owned комбинируется с Func для получения фабрики:
public class Foo(Func<Owned<Bar>> barFactory) {
// …
await using Owned<Bar> barOwned = barFactory();
Bar bar = barOwned.Value;
// …
}
Иногда требуется чтобы два сервиса было создано в одном скоупе. Для этой цели можно использовать следующий паттерн:
public class Foo(Func<Owned<Foo.Scope>> scopeFactory) {
public record class Scope(Bar Bar, Baz Baz);
// …
await using Owned<Scope> scope = scopeFactory();
Bar bar = scope.Value.Bar;
Baz baz = scope.Value.Baz;
// …
}