Главная страница   /   3.3. Паттерны DI-контейнеров (Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Марк Симан

3.3. Паттерны DI-контейнеров

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

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

Composition Root

В каком месте нам следует формировать диаграммы объектов?

Как можно ближе к точке входа в приложение.

DI-контейнер – это библиотека, которую вы можете использовать в тех местах, в которых только пожелаете – но это не означает, что вы должны его использовать. Несмотря на то, что вы можете расширить использование контейнера таким образом, чтобы он распространился на огромное количество ваших классов, вместо этого сосредоточьте его в единственной области своего приложения. Это место называется Composition Root, и вам следует использовать DI-контейнер только из этого места.

Сущность Composition Root особо не связана с DI-контейнерами. Composition Root также применяется в тех случаях, когда вы используете Poor man's DI, но я думаю, что важно обсудить Composition Root именно в этом контексте, поскольку понимание этого паттерна позволяет вам использовать ваш DI-контейнер корректно и эффективно. Перед тем, как я приступлю к обсуждению роли Composition Root при использовании DI-контейнеров, я кратко, в общих чертах, рассмотрю его.

Рисунок 3-6: При формировании приложения из множества слабо связанных классов композиция должна осуществляться как можно ближе к точке входа в приложение. Composition Root формирует диаграмму объектов, которая затем выполняет фактическую работу приложения.

Composition Root как основная сущность

При написании слабо связанного кода вы создаете множество классов, которые вы должны скомпоновать для того, чтобы создать приложение. Компоновать незначительное количество этих классов в единицу времени может быть довольно заманчивой идеей для создания небольших подсистем, но такой подход ограничивает вашу способность перехватывать эти системы с целью модификации их поведения. Вместо этого вы должны компоновать все классы одновременно.

Определение

Composition Root – это (предпочтительно) уникальное местоположение в приложении, где модули соединяются друг с другом.

Подсказка

Composition Root может разворачиваться в рамках составных классов, пока они располагаются в единственном модуле.

При изолированном рассмотрении Constructor Injection вас может заинтересовать такой вопрос: "не перемещает ли он решение о выборе зависимости всего лишь в другое место?". Да, это именно так, и это хорошо; это означает, что вы получаете центральное место, в котором можете соединять классы. Composition Root играет роль стороннего компонента, который связывает покупателей с их сервисами. В действительности, Нэт Прайс предпочитает термин "Стороннее соединение" вместо "механизм внедрения зависимостей" именно по этой причине.

Чем дольше вы будете откладывать принятие решения о том, как соединять классы, тем дольше вы будете сохранять ваши опции открытыми. Таким образом, Composition Root необходимо размещать как можно ближе к точке входа в приложение.

Примечание

Мне нравится думать о Composition Root как об архитектурном эквиваленте понятия Бережливой разработки программного обеспечения (Lean Software Development): Последний ответственный момент. Смысл в том, чтобы откладывать все решения настолько долго, насколько это позволяют правила приличия (но не дольше), потому что нам хотелось бы сохранять наши опции открытыми и основывать наши решения на как можно большем объеме информации. Когда дело доходит до компоновки приложений, мы можем таким же образом отложить принятие решения о передаче зависимостей в основание приложения.

Даже у модульного приложения, которое для своего формирования использует слабое связывание и позднее связывание, есть основание, которое содержит точку входа в приложение. Ниже приведены примеры:

  • Консольное приложение – исполняемый файл (.exe) с методом Main.
  • ASP.NET веб-приложение – это библиотека (.dll) с обработчиком событий Application_Start в файле Global.asax.
  • WPF приложение – исполняемый файл (.exe) с файлом App.xaml.
  • WCF сервис – это библиотека (.dll) с классом, который наследуется от интерфейса сервиса, несмотря на то, что вы можете заполучить более низкоуровневую точку входа путем создания пользовательской ServiceHostFactory.

Существует множество других технологий, но общим для всех этих технологий является то, что один модуль содержит точку входа в приложение: это основание приложения, что проиллюстрировано на рисунке 3-7. Composition Root приложения должно располагаться в основании приложения таким образом, чтобы оно могло правильно формировать приложение.

Рисунок 3-7: Точка входа в приложение – это основание модульного приложения. Либо напрямую, либо не напрямую основание использует остальные модули. Composition Root должно размещаться в основании приложения – как можно ближе к точке входа

Вы не должны пытаться компоновать классы в любом модуле, потому что такой подход ограничивает ваши возможности. Все классы модулей приложения должны использовать Constructor Injection (или, в редких случаях, один из других паттернов, описанный в главе "DI-паттерны") и оставлять задачу формирования диаграммы объектов приложения за Composition Root. Любой используемый DI-контейнер должен быть ограничен Composition Root.

Использование DI-контейнера в Composition Root

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

Подсказка

К DI-контейнеру следует обращаться из Composition Root. Все остальные модули не должны ссылаться на контейнер.

На рисунке 3-8 вы можете увидеть, что только Composition Root ссылается на DI-контейнер. Остальная часть приложения не имеет ссылок на контейнер и вместо него полагается на паттерны, описанные в главе "DI-паттерны". DI-контейнеры понимают эти паттерны и используют их для того, чтобы формировать диаграмму объектов приложения.

Рисунок 3-8: Только Composition Root, содержащееся в основании приложения, должно иметь ссылку на DI-контейнер. Все остальные модули приложения должны целиком полагаться на DI-контейнеры и не ссылаться на контейнер

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

Подсказка

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

Когда дело касается приложений, в основе которых лежат запросы, например веб-сайтов или сервисов, вы можете сконфигурировать контейнер всего один раз, но преобразовывать диаграмму объектов для каждого входящего запроса. Шаблонное коммерческое приложение является примером такой ситуации.

Пример: Реализация Composition Root

Шаблонное коммерческое приложение из раздела "Расширение шаблонного приложения" должно обладать Composition Root для того, чтобы формировать диаграммы объектов для входящих HTTP-запросов. Что касается всех остальных .NET веб-приложений, то для них точкой входа является метод Application_Start файла Global.asax.

В этом примере я использую DI-контейнер Castle Windsor, но код может быть таким же и для любого другого контейнера. Для Castle Windsor метод Application_Start мог бы выглядеть следующим образом:

protected void Application_Start()
{
	MvcApplication.RegisterRoutes(RouteTable.Routes);

	var container = new WindsorContainer();
	container.Install(new CommerceWindsorInstaller());

	var controllerFactory = new WindsorControllerFactory(container);

	ControllerBuilder.Current.SetControllerFactory(controllerFactory);
}

Перед тем как вы сможете сконфигурировать контейнер, вы должны создать новый экземпляр. Поскольку полноценная установка приложения инкапсулирована в классе под названием CommerceWindsorInstaller, вы инсталлируете его в контейнер для того, чтобы настроить этот контейнер. Код CommerceWindsorInstaller, очевидно, реализован с помощью API Castle Windsor, но концептуально он идентичен примеру из раздела "Конфигурирование DI-контейнеров".

Для того чтобы позволить контейнеру подключать контроллеры в приложение, вы должны применить соответствующий шов ASP.NET MVC, который называется IControllerFactory (подробно рассматривается в разделе "Построение ASP.NET MVC приложений"). На данный момент достаточно понимать, что для интеграции с ASP.NET MVC вы должны создать адаптер в рамках контейнера и сообщить об этом фреймворку.

Поскольку метод Application_Start выполняется всего лишь раз, контейнер является единственным экземпляром, который инициализируется всего один раз. При получении запросов этот экземпляр контейнера должен обрабатывать все запросы одновременно – но поскольку все контейнеры реализованы с помощью методов Resolve, корректно исполняемых в многопоточной среде, это не является проблемой.

Поскольку вы устанавливаете ASP.NET MVC с пользовательской WindsorControllerFactory, он будет вызывать ее метод GetControllerInstance для каждого входящего HTTP-запроса (подробнее об этом вы можете прочитать в разделе "Построение ASP.NET MVC приложений"). Реализация делегирует работу контейнеру:

protected override IController GetControllerInstance(
	RequestContext requestContext, Type controllerType)
{
	return (IController)this.container.Resolve(controllerType);
}

Обратите внимание на то, что вы более или менее вернулись к вводным примерам из раздела "Контейнер "Hello"". Метод Resolve формирует окончательную диаграмму, которая должна использоваться для этого конкретного запроса, и возвращает эту диаграмму. Это единственное место в приложении, где вы вызываете метод Resolve.

Подсказка

База кода приложения должна содержать единственный вызов метода Resolve.

В этом примере Composition Root развернут в рамках нескольких классов, что продемонстрировано на рисунке 3-9. Это предсказуемо – важно, что все классы содержатся в одном и том же модуле, которым в данном случае является основание приложения.

Рисунок 3-9: Composition Root развернут в рамках трех классов, но все они определены в пределах одного и того же модуля

Самое важное, на что здесь нужно обратить внимание, – это то, что эти три класса являются единственными классами во всем шаблонном приложении, которые ссылаются на DI-контейнер. Вся остальная часть кода приложения использует только паттерн Construction Injection; вернитесь назад и перечитайте главу "Комплексный пример", если вы мне не верите.

Подсказка

Мне хотелось бы обобщить все правила, содержащиеся в данном разделе, перефразировав голливудский принцип: не вызывайте контейнер; он сам вызовет вас.

Common Service Locator

Существует проект с открытым исходным кодом под названием Common Service Locator (http://commonservicelocator.codeplex.com/), целью которого является отсоединение кода приложения от конкретного DI-контейнера путем скрытия каждого контейнера за универсальным интерфейсом IServiceLocator.

Надеюсь, что такое объяснение того, как Composition Root эффективно отделяет остальную часть кода приложения от DI-контейнеров, теперь позволит вам понять, почему вам не нужен Common Service Locator. Как я буду объяснять в разделе "Service Locator", в связи с тем, что Service Locator является анти-паттерном, будет лучше не использовать его – тем более с Composition Root он вам и не нужен.

Более подробно о том, как реализовывать Composition Roots в различных фреймворках (включая ASP.NET MVC), вы можете прочитать в главе "Построение объектов". В данном контексте то, как вы это делаете, более важно, чем то, где вы это делаете. Как и подразумевает его название, Composition Root – это составляющая основания приложения, в которой вы компонуете все слабо связанные классы. Это справедливо и при использовании DI-контейнера, и при использовании Poor Man's DI. Тем не менее, при использовании DI-контейнера вам следует руководствоваться паттерном Register Resolve Release.

Register Resolve Release

Как нам следует использовать DI-контейнер?

Следуя строгой последовательности вызовов методов Register Resolve Release

Паттерн Composition Root описывает то, где вам следует использовать DI-контейнер. Тем не менее, он не сообщает о том, как его использовать. Паттерн Register Resolve Release отвечает на этот вопрос.

DI-контейнер следует использовать в трех, следующих друг за другом фазах называемых Register, Resolve и Release. В таблице 3-3 более подробно описана каждая из этих фаз.

Определение

Паттерн Register Resolve Release указывает на то, что методы DI-контейнера должны вызываться в этой строгой последовательности: Регистрация (Register), Решение (Resolve) и Освобождение (Release) (см. рисунок 3-10).

Рисунок 3-10: Методы DI-контейнера должны вызываться в продемонстрированной строгой последовательности: сначала метод Register, затем метод Resolve и в завершении метод Release
Таблица 3-3: Фазы контейнера
Фаза Что происходит на данной фазе? Что прочитать дальше
Register Регистрация компонентов контейнера.

Вы конфигурируете контейнер, сообщая ему о том, какие классы он может использовать, как ему следует преобразовывать абстракции в конкретные типы и о том (необязательная информация), как определенные классы должны соединяться вместе.
В разделе "Конфигурирование DI-контейнеров" я уже обсуждал то, как сконфигурировать DI-контейнер.

В части "DI-контейнеры" я буду подробно обсуждать конфигурацию шести индивидуальных DI-контейнеров.
Resolve Преобразует компоненты основания.

Запрос одного типа преобразуется в одну диаграмму объектов.
В разделе "Знакомство с DI-контейнерами" мы увидели, как преобразовывать диаграммы объектов с помощью DI-контейнера.

В части "DI-контейнеры" вы можете более подробно почитать об API конкретных контейнеров.
Release Освобождает компоненты из контейнера.

Все диаграммы объектов, образованные на предыдущей фазе, должны быть освобождены в тот момент, когда в них уже нет нужды. Это сигнализирует о том, что контейнер может очистить диаграмму объектов, что определенно важно в тех случаях, когда некоторые компоненты являются лишними.
В главе "Жизненный цикл объектов" я обсуждаю вопрос управления жизненным циклом объектов, включая важность процесса очистки объекта.

Кроме того в части "DI-контейнеры" я рассматриваю API управления жизненным циклом индивидуальных DI-контейнеров.

Вы должны использовать эти три фазы в правильном порядке и вам не разрешается волей-неволей двигаться назад или вперед. К примеру, вам не следует возвращаться и заново конфигурировать контейнер, как только вы приступили к преобразованию диаграммы объектов. Иногда люди спрашивают о том, как добавить в контейнер дополнительные компоненты уже после того, как они приступили к преобразованию компонентов. Не делайте этого – это принесет вам одни огорчения.

Примечание

Некоторые DI-контейнеры не поддерживают явный Release диаграмм объектов и вместо этого полагаются на .NET сборщика мусора (garbage collector). При использовании таких контейнеров вы должны применять модифицированный паттерн Register Resolve и обращаться к потенциальным утечкам ресурсов в ваших реализациях объекта. Более подробно читайте об этом в главе "Жизненный цикл объектов".

В следующем разделе я буду говорить о методах Register, Resolve и Release, а также о фазах. Castle Windsor действительно обладает тремя методами с точно такими же названиями, а фазы названы в честь этих методов. Другие DI-контейнеры могут использовать другие названия, но их основополагающая сущность идентична. Я использую названия только Castle Windsor, поскольку они обеспечивают логичную терминологию – а также приятную аллитерацию.

Статическая структура

В своем истинном представлении паттерн Register Resolve Release утверждает, что вам следует выполнять вызов только одного метода в каждой фазе. Кшиштоф Козмиц называет это паттерном трех вызовов (Three Calls Pattern) – вам разрешено сделать только три вызова методов в контейнере.

Методы Resolve и Release упрощают это. В разделе "Composition Root" я уже утверждал о том, что в приложении должен содержаться единственный вызов метода Resolve. Как следствие вы должны всегда выпускать то, что вы преобразовываете.

Подсказка

Любая диаграмма объектов, сформированная с помощью метода Resolve, должна быть списана при помощи метода Release.

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

Autofac даже делает это представление явным при помощи выделения конфигурации контейнера в отдельный ContainerBuilder: вы регистрируете компоненты с помощью ContainerBuilder, и когда вы делаете это, вы просите ContainerBuilder создать экземпляр контейнера из конфигурации. В Autofac вы не конфигурируете контейнер напрямую.

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

Если вы вспомните листинг 3-2, то можете заявить, что он содержит более одного вызова метода. Регистрация всегда включает в себя множество операторов, но большинство DI-контейнеров обладают механизмом пакетирования, который позволяет инкапсулировать все эти конфигурационные операторы в единственном классе (возможно состоящим из других классов). Autofac называет их модулями (Modules), StructureMap называет их реестрами (Registries), а Castle Windsor – инсталляторами (Installers). Общим для всех них является тот факт, что все они могут использоваться для конфигурации контейнера с помощью единственного вызова метода. В разделе "Composition Root" вы уже видели, как Castle Windsor использует инсталлятор:

container.Install(new CommerceWindsorInstaller());

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

К совету, заключающемуся в том, что для Resolve и Release необходимо использовать только по одной единственной строке кода, следует отнестись серьезно – но для фазы Register данный факт должен восприниматься более концептуально. Важным вопросом является тот факт, что регистрация должна быть завершена до вызова метода Resolve. Рисунок 3-11 иллюстрирует то, как выглядит последовательность, включая инкапсуляцию множества вызовов метода Register.

Рисунок 3-11: На фазе Register может иметь место любое количество вызовов метода Register, но вы все еще должны расценивать его как элементарное действие. На фазах Resolve и Release вам, буквально, нужно только по одному вызову каждого метода.

Универсальным источником заблуждений является то, что паттерн трех вызовов создает твердое убеждение о том, как часто каждый метод должен появляться в базе кода, но он ничего не говорит о том, сколько раз он должен вызываться.

Динамическое взаимодействие

Название паттерна Three Calls Pattern может привести вас к мысли о том, что каждый метод должен вызываться всего лишь раз. Источник этого заблуждения находится в самом названии, и это одна из нескольких причин того, почему я предпочитаю название Register Resolve Release.

Паттерн Three Calls утверждает, что для вызова каждого метода должна существовать единственная строка кода. Тем не менее, в зависимости от обстоятельств, некоторые методы могут вызываться более одного раза.

В однопоточном приложении таком, как настольное приложение, утилита командной строки или пакетное задание, каждый метод обычно вызывается только один раз, что проиллюстрировано на рисунке 3-12.

Рисунок 3-12: В однопоточном приложении каждый метод будет вызываться только один раз. За конфигурацией контейнера непосредственно следует формирование диаграммы объектов приложения, которая выполняет реальную работу. После окончания работы перед выходом из приложения вызывается метод Release.

В приложениях, основанных на запросах, таких, как веб-сайт, веб-сервис или получатель асинхронного сообщения, Composition Root формирует диаграмму объектов для каждого входящего запроса. В этом типе приложения, как это проиллюстрировано на рисунке 3-13, метод Register вызывается только один раз, тогда как методы Resolve и Release вызываются попарно для каждого запроса – потенциально большее число раз.

Рисунок 3-13: В приложениях, основанных на запросах, метод Register вызывается только один раз, тогда как методы Resolve и Release вызываются много раз – по одному на каждый запрос

Важно отметить, что вам нужно сконфигурировать контейнер только один раз. Контейнер – это совместно используемый экземпляр, применяемый для преобразования составных запросов, но конфигурация должна оставаться стабильной и завершенной.

Динамическая картина Register Resolve Release – это почти инверсия статического представления – сравните рисунок 3-11 и рисунок 3-13. В статическом представлении мы допускаем составные строки кода, которые вызывают метод Register, а в динамическом представлении этот блок кода должен вызываться строго один раз. С другой стороны, статическим правилом является то, что вы должны иметь только одну строку кода, которая вызывает Resolve и Release, но во время выполнения они могут вызываться много раз.

Это может казаться сложным и трудным, но как демонстрирует следующий пример, этого всего лишь вызовы трех методов.

Пример: использование Register Resolve Release

В данном примере вы будете реализовывать Composition Root шаблонного приложения из раздела "Расширение шаблонного приложения" с помощью DI-контейнера Castle Windsor. Это тот же самый контейнер, который вы использовали в примере раздела "Composition Root", поэтому этот пример нужно читать как продолжение предыдущего.

Точкой входа в приложение является метод Application_Start, и поскольку это приложение является веб-сайтом, фаза Register изолирована от фаз Resolve и Release, потому что вы должны сконфигурировать контейнер всего лишь раз. Код остается таким же, как и в предыдущем примере, но я хочу немного сместить фокус:

protected void Application_Start()
{
	MvcApplication.RegisterRoutes(RouteTable.Routes);

	var container = new WindsorContainer();
	container.Install(new CommerceWindsorInstaller());

	var controllerFactory = new WindsorControllerFactory(container);

	ControllerBuilder.Current.SetControllerFactory(controllerFactory);
}

Согласно паттерну Register Resolve Release вызов первого метода, который вы совершаете для экземпляра container, должен быть вызовом элементарного Register. В данном случае метод называется Install и CommerceWindsorInstaller инкапсулирует индивидуальные регистрации в единственном классе. Следующий листинг демонстрирует реализацию CommerceWindsorInstaller.

Листинг 3-3: Инкапсуляция составных регистраций
public class CommerceWindsorInstaller : IWindsorInstaller
{
	public void Install(IWindsorContainer container, IConfigurationStore store)
	{
		container.Register(AllTypes
			.FromAssemblyContaining<HomeController>()
			.BasedOn<IController>()
			.Configure(r => r.LifeStyle.PerWebRequest));

		container.Register(AllTypes
			.FromAssemblyContaining<BasketService>()
			.Where(t => t.Name.EndsWith("Service"))
			.WithService
			.FirstInterface());

		container.Register(AllTypes
			.FromAssemblyContaining<BasketDiscountPolicy>()
			.Where(t => t.Name.EndsWith("Policy"))
			.WithService
			.Select((t, b) => new[] { t.BaseType }));

		string connectionString = ConfigurationManager
			.ConnectionStrings["CommerceObjectContext"]
			.ConnectionString;

		container.Register(AllTypes
			.FromAssemblyContaining<SqlProductRepository>()
			.Where(t => t.Name.StartsWith("Sql"))
			.WithService
			.Select((t, b) => new[] { t.BaseType })
			.Configure(r => r.LifeStyle.PerWebRequest
				.DependsOn((new
				{
					connString = connectionString
				}))));
	}
}

Строка 5, 10, 16, 26: Вызовы Register

CommerceWindsorInstaller кажется сложным, но важно отметить, что он инкапсулирует четыре вызова метода Register и что это единственный способ, с помощью которого он взаимодействует с контейнером. Остальная часть кода сейчас не важна. В нем используются соглашения для конфигурации контейнера. Более подробно об API механизма автоматической регистрации Castle Windsor вы можете прочитать в разделе "Конфигурирование контейнера" (глава "Castle Windsor").

Поскольку шаблонное приложение является веб-сайтом, Resolve и Release должны быть реализованы в единой паре. Для каждого HTTP-запроса вы должны преобразовать диаграмму объектов, которая будет обрабатывать этот запрос, а когда это закончится, вы должны выпустить его снова. Вы делаете это из класса под названием WindsorControllerFactory, который наследуется от DefaultControllerFactory, относящегося к ASP.NET MVC – более подробно о шве ASP.NET MVC вы можете прочитать в разделе "Построение ASP.NET MVC приложений".

ASP.NET MVC фреймворк вызывает метод GetControllerInstance для того, чтобы преобразовать IControllers и метод ReleaseController при обработке запроса. Ниже приведены подходящие для нас методы вызова методов Resolve и Release:

protected override IController GetControllerInstance(
	RequestContext requestContext, Type controllerType)
{
	var controller = this.container.Resolve(controllerType);
	return (IController)controller;
}

public override void ReleaseController(IController controller)
{
	this.container.Release(controller);
}

В методе GetControllerInstance вы передаете аргумент controllerType в метод Resolve и возвращаете результирующую диаграмму объектов. При обработке запроса ASP.NET MVC фреймворк вызывает метод ReleaseController с экземпляром IController, созданным до этого с помощью метода GetControllerInstance, и вы можете передать этот экземпляр controller в метод Release.

Обратите внимание на то, что это единственное появление методов Resolve и Release во всей базе кода приложения.

Этот пример погружается несколько глубже предыдущего примера, который демонстрировал паттерн Composition Root, но в сущности это тот же самый код. Паттерн Composition Root указывает, где вам следует формировать диаграммы объектов, тогда как Register Resolve Release имеет дело с тем, как использовать DI-контейнер в пределах Composition Root.

В следующей главе я сделаю обзор большего количества DI-паттернов, но перед тем, как это сделать, я хочу сделать небольшой крюк и обсудить то, как DI-контейнеры приспосабливаются к всеобщей .NET экосистеме.