У какого объекта есть право извлекать объекты из DI контейнера?

Осваиваю DI-контейнеры и нюансы их применения в приложении (на примере контейнера Autofac). Приложение десктопное.

В принципе, ничего сложного в теме нет, кроме одного нюанса. Взаимодействие с DI происходит в юзингах - создается scope, в нем извлекается из DI нужный объект, выполняется задача и затем все созданные объекты автоматически диспосятся.

Если программа простая, то такой using может быть всего один (прямо в методе Main), и все объекты созданные в нем живут всё время жизни программы. Но если модель сложная и структурированная, то нет смысла держать ее в памяти всю целиком, а лучше восстанавливать из DI отдельные ее части, необходимые для выполнения текущей задачи, по мере необходимости, и затем сразу от них избавляться с сохранением результата (на сколько я знаю, в вебе это стандарт - 1 scope на 1 http-запрос).

По логике вещей это должно быть организовано в виде методов, каждый из которых будет создавать свой локальный scope для выполнения задачи. Так вот, объект, в котором будут все эти методы, по сути напрямую взаимодействует с DI, должен получать его в конструктор, и вообще это очень напоминает Service Locator.

Но дело-то в том, что такой объект должен где-то быть, т.к. точки входа сами себя не создадут! И находиться он должен, учитывая специфику его работы, где-то очень близко от метода Main и жить всё время жизни программы.

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

Помогите, пожалуйста, разобраться, кто на самом деле имеет право извлекать из DI объекты. Заранее спасибо!


Ответ найден в статьях Марка Симана:

  1. Про Composition Root: https://blog.ploeh.dk/2011/07/28/CompositionRoot/
  2. Про создание объектного графа: https://blog.ploeh.dk/2011/03/04/Composeobjectgraphswithconfidence/

Ответы (2 шт):

Автор решения: Alexander Petrov

При использовании DI базовое понятие - Composition Root (корень композиции). Это самое начало приложения, где определяются все объекты (зависимости), которые будут использоваться приложением. Они помещаются в DI-контейнер. После чего вызывается стартовый метод приложения, куда передаётся единственный объект из контейнера. Этот объект является графом, содержащим все зависимости. Всё, после этого DI-контейнер становится не нужен и прекращает своё существование. (Вернее, так должно быть в чистом DI).

Далее по ходу выполнения программы зависимости извлекаются из графа. Но при таком прямолинейном подходе получается, что все они должны быть созданы сразу, ещё в Composition Root. Это накладно как по времени, так и по памяти (в крупном приложении вообще может быть невозможно). Поэтому приходится так или иначе создавать многие сервисы отложенно.

Делать это можно двумя путями. Либо "старый-добрый" (да будь он проклят в веках!) Service Locator - по этому пути пошли в Microsoft, потому что индусокодеры "не осилили"...
Либо использовать фабрики. Фабрика в данном случае легковесный объект. В ней может быть всего один метод: CreateSomeService. Приняв фабрику на входе, далее вызываем этот метод и получаем тяжёлый сервис.
Зачем это нужно? Все тяжёлые сервисы поместить в граф невозможно, а лёгких фабрик можно напихать сколько угодно.

→ Ссылка
Автор решения: Pavel Mayorov

Вы же используете 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;

  // …
}
→ Ссылка