Как стать автором
Обновить

О классах Program и Startup — инициализация ASP.NET приложения. Часть II: IWebHostBuilder и Startup

Время на прочтение72 мин
Количество просмотров10K

Введение

Это - продолжение статьи, первая часть которой была опубликована ранее. В той части был рассмотрен процесс инициализации, общий для любого приложения .NET Core на базе шаблона Generic Host. А в этой части будет рассмотрена инициализация, специфическая именно для веб-приложения. Именно в нее входят вызовы методов Startup-класса.

Если вы не читали первую часть, то рекомендую в нее заглянуть, по двум причинам. Во-первых, процесс инициализации, специфической именно для веб-приложения, существенно опирается на механизмы, рассмотренные в первой части: методы интерфейса построителя веб-приложения IWebHostBuilder в основном реализуются через вызовы методов интерфейса построителя IHostBuilder, и процесс инициализации проходит, в целом, через те же стадии, общие для любого базирующегося на Generic Host приложения .NET Core. А во-вторых, там объяснено, для чего часть информации убрана под спойлеры, и какую информацию под какими спойлером можно увидеть (и решить - а стоит ли ее смотреть).

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

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

И прежде всего - краткое содержание статьи:

TL;DR
  1. Конфигурирование веб-приложения выполняется одним из методов расширения для интерфейса построителя IHostBuilder: ConfigureWebHost(базовый) или ConfigureWebHostDefaults(дополнительно устанавливает настройки по умолчанию).

  2. Конфигурирование выполняется внутри передаваемого в один из этих методов делегата, принимающего параметр построителя веб-приложения типа IWebHostBuilder - того же самого типа, который используется для конфигурирвания в шаблоне Web Host. Это позволяет использовать для конфигурирования в шаблоне Generic Host те же методы, что и в шаблоне Web Host. Этот делегат выполняется синхронно в рамках выполнения используемого метода расширения.

  3. Для завершения конфигурирования веб-приложения и запуска его на выполнение указанные выше методы регистрируют для помещения в контейнер сервисов внутренний класс GenericWebHostedService в качестве реализации сервиса (интерфейса) IHostedService. Благодаря этому, в соответствии с общей логикой работы Generic Host, при запуске приложения будет произведен асинхонный вызов метода StartAsync указанного класса.

  4. Для реализации интерфейса IWebHostBuilder создается объект построителя веб-приложения класса GenericWebHostBuilder (внутреннего), который реализует все методы указанного интерфейса, кроме метода Build.

  5. Конструктор класса GenericWebHostBuilder инициализует внутреннюю конфигурацию построителя веб-приложения и помещает в очереди построителя три делегата - по одному для этапов создания конфигурации построителя, окончательной конфигурации приложения и конфигурирования списка регистрации серверов. Эти делегаты вополняют следующие функции: добавление внутренней конфигурации построителя веб-приложения в конфигурацию построителя (первый делегат), обеспечение работы механизма конфигурирования на основе стартовых сборок (все три делегата) и добавление в список регистрации сервисов ряда описателей сервисов, необходимых для работы веб-приложения (третий делегат).

  6. Класс GenericWebHostBuilder реализует методы интерфейса построителя веб-приложения IWebHostBuilder двумя способами. Методы GetSetting и UseSetting работают с внутренней конфигурацией класса (объектом, реализующим IConfiguration), которая впоследствие добавляется в конфигурацию построителя на этапе ее создания. Методы ConfigureAppConfiguration и ConfigureServices, которые принимают делегаты, производящие конфигурирование, соответственно, конфигурации приложения и списка регистрации сервисов для создания контейнера сервисов, помещают в соответствующие очереди построителя (IHostBuilder) переданные им делегаты, с добавлением кода, который производит необходимое преобразование параметров.

  7. В связи с сильным различием в логике создания контейнера сервисов и запуска приложения между старым шаблоном Web Host и новым Generic Host, была изменена реализация ряда методов расширения для интерфейса построителя веб-приложения IWebHostBuilder. Эти методы проверяют, реализует ли класс, реализующий IWebHostBuilder, также дополнительные интерфейсы (в статье они названы интерфейсами перенаправления). Если интерфейс перенаправления реализован (что свидетельствует об использовании шаблона Generic Host), то метод расширения вызывает соответствующий (на практике - одноименный) метод интерфейса перенаправления, а не использует логику, предназначенную для шаблона Web Host. В частности, именно так устроен широко используемый метод указания Startup-класса UseStartup: он вызывает одноименный метод класса GenericWebHostBuilder. Этот метод помещает в очередь этапа конфигурирования списка регистраций сервисов построителя (IHostBuilder) делегат, вызывающий внутренний метод класса GenericWebHostBuilder с тем же именем UseStartup (но с другими параметрами).

  8. Внутренний метод UseStartup класса GenericWebHostBuilder создает делегаты для вызова методов Startup-класса с именами, соответствующими соглашению о вызове этих методов. Всего этих методов может быть от одного до трех: с базовыми именами ConfigureServices, ConfigureContainer (необязательные) и Configure (обязательный). Этот метод выполняется на этапе конфигурирования списка регистраций сервисов. Делегат для вызова метода ConfigureServices (если он есть) Startup-класса вызывается напрямую, делегат для вызова ConfigureContainer(если таковой есть) помещается в очередь построителя (IHostBuilder) для вызова на этапе конфигурирования контейнера-построителя, а делегат для вызова метода Configure передается в код, осуществляющий запуск веб-приложения через механизм параметров (Options).

  9. Конструктор Startup-класса может использовать внедрение зависимостей только из весьма усеченного суррогатного контейнера сервисов (в этом положение в шаблоне Generic Host отличается от такового в Web Host), но при вызове метода Configure в него в качестве параметров могут передаваться любые сервисы из полноценного контейнера сервисов.

  10. Построение конвейера Middleware производится на стадии запуска веб-приложения. Сначала в конвейер добавляются обработчики, которые создаются сервисами типа IStartupFilter, зарегистрированными в контейнере сервисов (в порядке, обратном порядку их добавления в список регистрации сервисов), а в конце, после них всех - обработчики, добавляемые методом Configure Startup-класса

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

Лирическое отступление: дом, который построит Джек

Пока я продирался сквозь код инициализации веб-приложения ASP.NET Core, через его многочисленные вложенные лямбда-функции, мне постоянно лез стишок про дом, который построил Джек, но немного переиначенный, повернутый в будущее, примерно так: "Вот пес без хвоста, который будет гонять и трепать кота, который будет пугать птицу синицу, которая часто будет воровать пшеницу, которая будет в темном чулане храниться, в доме, который построит Джек". Потому что этот код часто выглядит так: поместить в некую очередь, которая будет выполнена на стадии построения, делегат, который что-то сделает и поместит в другую, выполняемую после этой, очередь другой делегат, который тоже сделает что-то другое и поместит в третью, выполняемую этой, очередь ещё один делегат...

Методы расширения IHostBuilder для конфигурирования специфических веб-приложения настроек

В общих чертах конфигурирование настроек, специфичных для Web-приложения, при использовании шаблона Generic Host выглядит следующим образом. Оно производится одним из методов расширения для интерфейса IHostBuilder - ConfigureWebHostDefaults (конфигурирование с настройками по умолчанию, определен в классе GenericHostBuilderExtensions) или ConfigureWebHost (без настроек по умолчанию, определен в классе GenericHostWebHostBuilderExtensions, этот класс, как и предыдущий, принадлежит пространству имен Microsoft.Extensions.Hosting). В том шаблоне, из которого делается свежий веб-проект, используется первый метод. Но при желании можно использовать и второй, если позаботиться о самостоятельном конфигурировании той части настроек по умолчанию, которые нужны в данном конкретном веб-приложении.

Оба этих метода принимают единственный параметр-делегат. Этот делегат также имеет один параметр - ссылку на интерфейс IWebHostBuilder, построитель веб-приложения. И весь процесс конфигурирования настроек, специфичных для Web-приложения, задается разработчиком внутри этого делегата. Возвращают эти методы, как обычно, тот же IHostBuilder, с которым они работают, для объединения вызовов в цепочку. Код конфигурирования настроек Web-приложения внутри делегата практически полностью аналогичен такому же коду в шаблоне Web Host (в котором интерфейс IWebHostBuilder играет ту же роль, что и IHostBuilder в шаблоне Generic Host).

Лирическое отступление: о совместимости

Такое решение имеет очевидные достоинства совместимости: оно не только позволяет сравнительно легко переделывать разработанные на ASP.NET Web-приложения со старого шаблона Web Host на новый - Generic Host, но и дает возможность повторного использования в шаблоне Generic Host кода методов конфигурирования, разработанных для шаблона Web Host, и, в частности - поддерживать оба эти шаблона в рамках одного фреймворка. И, как и у всего на свете, у этого решения есть обратная сторона: использование делегата для конфигурирования, что усложняет чтение и понимание кода. Причем, если при конфигурировании IHost использование делегатов часто было необходимостью, обусловленной выбранной архитектурой - эти делегаты планировались для отложенного выполнения - то в данном случае такой прямой необходимости нет: переданный в указанные методы делегат вызывается только внутри них и завершается до их завершения. И особенно мне не нравится выбранное разработчиками ASP.NET решение представить делегат в виде лямбда-функции: это, хоть и выглядит стильно, модно,моложежно но плохо практически для всех вариантов использования. Потому что, если все конфигурирование специфических для веб-приложения компонентов производится внутри Startup-класса, то такая, переусложненная форма указания Startup-класса сильно избыточна: я бы для этого случая предпочел бы, вместо ConfigureWebHostDefaults, метод расширения для IHost с одним дополнительны параметром - типом Startup-класса, так как все равно реальную работу выполняет внутренний метод, получающий управление через цепочку вызовов, именно с такой сигнатурой. А если для конфигурирования специфичных для веб-приложения компонентов вызывается некоторое количество методов IWebHostBuilder, то лямбда-функция быстро становится слишком сложной для восприятия. Я бы для такого случая добавил бы в класс Program отдельный метод с параметром IWebHostBuilder, и перенес бы весь этот код туда, чтобы не загромождать код фигурными скобками и непонятно где кончающимися вложенными друг в друга лямбда-функциями. И в качестве параметра в ConfigureWebHost/ConfigureWebHostDefaults передал бы просто имя этого метода. Однако нужна нам эта совместимость или нет и как написать шаблон - нас не спросили: нам просто дали шаблон приложения ASP.NET Core, где уже сделано именно так. Поэтому используем. Или - не используем.

Схема работы метода ConfigureWebHost и связанных с ним компонентов программы представлена на рис.1

Условные обозначения на схемах

Как уже говорилось в первой части, рисунки-схемы не являются формализованными диаграммами, а служат только для иллюстрации. Поэтому объекты на них изображены с некоторой степнью вольности, без строгой формализации. Особенно - в данной части: обозреваемый в ней код является слишком объемным, и в одну схему не вмещается. Но, тем не менее, специфичческий смысл у разных форм графических элементов присутствует. Прямоугольные элементы со сплошниыми границами обозначают блоки кода - как реализации методов классов, так и делегаты. Прямоугольныики со скругленными границами обозначают объекты. Штриховые прямоугольники со скругленными углами - новое по сравнению с первой частью обозначение - выделяют отдельные интерфейсы у класса, реализующего несколько интерфейсов. Элементы с штриховыми границами (кроме прямоугольников со скругленными углами)- элементы данных: свойства объектов и данные, передаваемые методам. Маленькие кружки на выносных линиях обозначают методы и свойства интерфейсов и классов. Сплошные линии со стрелками - направления, в которых идет выполнение кода. Штриховые линии со стрелками - передача данных в методы/свойства (а передаваемые в методы в качестве данных делегаты обозначены прямоугольниками в штриховых кругах). Штрих-пунктирные линии с пустотелыми стрелками на конце обозначают создание экземпляров объектов.

Метод расширения ConfigureWebHost работает следующим образом (см. верхнюю часть рис.1). Сначала он создает экземпляр класса построителя веб-приложения GenericWebHostBuilder (изображен в средней части рис.1), реализующий интерфейс IWebHostHostBuilder. После этого он выполняет переданный в него параметр-делегат (configure), аргументом этого делегата как раз является только что созданный экземпляр построителя веб-приложения. Внутри этого делегата выполняется код, аналогичный коду инициализации для шаблона Web Host. Этот код, как правило вызывает собственные методы интерфейса IWebHostHostBuilder и методы его расширения. И, наконец, метод ConfigureWebHost добавляет в очередь делегатов конфигурирования списка регистраций сервисов построителя (IHostBuilder) реализацию интерфейса IHostedService через класс GenericWebHostedService, который и обеспечивает окончательную инициализацию и запуск на выполнение веб-приложения. Эти действия производится в методе StartAsync этого класса, который вызывается через упомянутый интерфейс стандартной реализацией Internal.Host окружения (IHost) при запуске приложения после его создания методом IHost.StartAsync (подробнее об этом см. в конце предыдущей части).

Метод ConfigureWebHostDefaults работает через метод ConfigureWebHost: он вызывает этот метод, но заставляет его перед выполнением переданного в ConfigureWebHostDefaults делегата выполнить установку значений для веб-приложения по умолчанию с помощью метода ConfigureWebDefaults. Список этих значений - см. документацию.

детали реализации: метод расширения ConfigureWebHostDefaults для интерфейса IHostBuilder

Установка значений по умолчанию производится статическим методом ConfigureWebDefaults класса Microsoft.AspNetCore.WebHost. Этот метод - универсальный, он используется и для шаблона Generic Host, и для шаблона Web Host. Для установки значений по умолчанию в него передается в качестве аргумента ссылка на построитель веб-приложения IWebHostBuilder. Организация выполнения ConfigureWebDefaults делается способом, дающим понять, что разработчики знают про функциональное программирование: в ConfigureWebHost передается в качестве аргумента делегат, который сначала выполняет ConfigureWebHost, а затем - делегат, переданный в ConfigureWebDefaults. В ФП такое называется, в принципе, композицией, ну а в C#, куда удобные примитивы для операциями над функциями не завезли, приходится это выполнять своими силами. Впрочем, объединяемые делегаты на функции тоже не сильно похожи: они ничего не возвращают, а меняют переданный им по ссылке параметр. Иммутабельность? Не слышали.

GenericWebHostBuilder - фиктивная (отчасти) реализация построителя веб-приложения IWebHostBuilder

В качестве реализации IWebHostBuilder внутри метода ConfigureWebHost используется фиктивный внутренний класс построителя веб-приложения GenericWebHostBuilder. Фиктивный он - потому что никакое веб-приложение он на самом деле не строит и строить не способен: метод Build интерфейса IWebHostBuilder, который используется в шаблоне Web Host с той же целью, что и одноименный метод интерфейса IHostBuilder в шаблоне Generic Host, в нем просто не реализован: при вызове он выбрасывает исключение. Одна из важных функций этого класса состоит в том, чтобы транслировать вызовы методов конфигурирования, определенных в интерфейсе построителя веб-приложения IWebHostBuilder, в эквивалентные вызовы интерфейса построителя окружения IHostBuilder (на рис.1 он условно изображн внизу). Ряд методов построителя веб-приложения удается реализовать таким образом. Но далеко не всю функциональность построителя веб-приложения IWebHostBuilder удается реализовать так, требуются и другие способы.

Лирическое отступление: класс GenericWebHostBuilder как шедевр костылестроения

В описании выше знатоки теории могли бы углядеть пример применения шаблона проектирования "адаптер", который является общепринятым методом построения костылей. Но вообще-то GenericWebHostBuilder далеко не ограничивается трансляцией вызовов IWebHostBuilder в вызовы IHostBuilder (все это подробно рассмотрено ниже). Так что, в целом, GenericWebHostBuilder и ряд связанных с ним интерфейсов и классов можно смело назвать шедевром костылестроения от разработчиков .NET Core, позволившим впихнуть без существенных жертв старую логику построения веб-приложения на шаблоне Web Host в рамки нового шаблона Generic Host - в том числе и там, где логика работы приложений на этих шаблонах существенно отличается. Я считаю, что работу подобных хорошо реализованных костылей стоит изучать разработчикам - чтобы использовать примененные там приемы при написании своих костылей. Насколько я заметил, теоретики от программирования не очень охотно занимаются анализом того, как надо писать костыли - им больше по душе придумывать и раздавать советы о том, как все делать правильно сразу (ну или переделывать с нуля - это называется умным заграничным словом рефакторинг) - как писать "чистый код", каким принципам следовать при построении архитектуры приложения, какие шаблоны использовать при проектировании... Эти темы и рассматривать проще, и для создания шумихи (AKA hype) и получения известности, позволяющей так или иначе заработать деньги, они лучше подходят. А существующие приложения, которые надо модифицировать, вообще часто рассматриваются (особенно, молодежью, которая выучила эту самую теорию, но пока не знает жизни) как "легаси", на которое не стоит тратить свои ум, силы и время. В то же время реальные программисты костыли в своих приложениях вынуждены делать регулярно, так что хорошее знакомство с удачными методами проектирования и построения костылей, а так же - близко связанными методами модификации архитектуры могло бы существенно облегчить их реальную работу. Однако, насколько мне известно (но я мог что-то упустить, да), книг и статей по описаниям лучших практик костылестроения - паттернам проектирования костылей, методам локальной модификации архитектуры приложения и т.п. - просто нет, и изучать данную тему приходится по таким вот источникам, как исходный код .NET Core Но вернемся к классу GenericWebHostBuilder.

Конструктор класса GenericWebHostBuilder и его делегаты

Конструктор класса построителя веб-приложения GenericWebHostBuilder содержит немалое количество кода, выполняющего несколько функций, в том числе - весьма непростых. Для обеспечения вызовов делегатов, выполняемых на последующих стадиях, и для прочих надобностей в него через параметр типа IHostBuilder передается ссылка на родительский построитель (реально - экземпляр класса HostBuilder), которую конструктор сразу же записывает в частное поле _builder экземпляра GenericWebHostBuilder (на рис.1 не показана). Следующим действием конструктора является создание объекта внутренней конфигурации построителя веб-приложения IWebHostBuilder. Внутренняя конфигурация хранится во внутреннем поле _config экземпляра объекта GenericWebHostBuilder. Она создается исключительно из переменных среды с префиксом ASPCORENET_. Другие возможные источники конфигурации - параметры командной строки, файлы, переменные окружения с другим префиксом - для создания внутренней конфигурации построителя веб-приложения не используются. В процессе выполнения делегата-параметра метода ConfigureWebHost во внутреннюю конфигурацию могут быть добавлены (методом UseSetting) дополнительные параметры. А на стадии построения конфигурации построителя (IHostBuilder) внутренняя конфигурация построителя веб-приложения IWebHostBuilder добавляется в конфигурацию построителя (подробности см. ниже).

детали реализации: создание внутреней конфиграции построителя веб-приложения

Создание конфигурации делается обычным образом: создается экземпляр класса ConfigurationBuilder (реализующий интерфейс IConfigurationBuilder), в него добавляется источник конфигурации на основе переменных среды с указанным префиксом, а затем вызывается метод Build этого интерфейса.

И, наконец, конструктор класса GenericWebHostBuilder добавляет в разные очереди конфигурирования родительского построителя размещения IHostBuilder три делегата (реализованных в виде лямбда-функций). Эти делегаты будут выполнены на соответствующих этапах построения окружения (IHost) в методе IHostBuilder.Build

Первый добавленный делегат предназначен для поддержки механизма конфигурирования веб-приложения на основе стартовых сборок (Startup Assemblies). Добавляется этот делегат в очередь делегатов построения конфигурации построителя. Этот делегат делает две вещи. Во-первых - добавляет внутреннюю конфигурацию построителя веб-приложения IWebHostBuilder в число источников конфигурации построителя

детали реализации: первый делегат конструктора GenericWebHostBuilder

Делается это способом, аналогичным добавлению конфигурации построителя в число источников для конфигурации приложения, описанным ранее: в список источников конфигурации добавляется источник класса ChainedConfigurationSource, содержащий ссылку на объект конфигурации внутренней конфигурации построителя веб-приложения.

Таким образом, именно этот делегат добавляет в конфигурацию построителя (и, как следствие - в конфигурацию приложения) значения, которые читаются из переменных среды с префиксом ASPCORENET_ и значения, задаваемые методом UseSetting построителя приложения IWebHostBuilder (об этом методе см. ниже). Во-вторых, этот делегат вызывает внутренний метод ExecuteHostingStartups класса GenericWebHostBuilder выполняющий вызовы методов конфигурирования в стартовых сборках. Работа этого метода будет подробно описана ниже.

Второй добавляемый делегат - это делегат создания конфигурации приложения через механизм стартовых сборок. Он добавляется в очередь построения конфигурации приложения. Работа этого делегата также будет подробно рассмотрена ниже. Третий добавляемый делегат - это делегат конфигурирования списка регистраций сервисов. Он добавляется в очередь конфигурирования списка регистраций сервисов. Работа этого делегата тоже будет подробно рассмотрена ниже.

Лирическое отступление: третий делегат - замечательно сложный код

Если лямбда-функции, которые являются первыми двумя делегатами, сложны лишь весьма ограниченно - содержат всего по две строки с вызовами методов и легко помещаются на одном экране - то третий делегат являет собой неплохой образец написания сложного кода современными методами ;-) : он содержит целых 56 строк (правда, среди них - немало комментариев, строк с одиночными фигурными скобками и даже просто пустых строк), в нем используется трехкратно (считая сам делегат) вложенные друг в друга лямбда-функции (и получается загадочная картинка "найди на этом рисунке все лямбда-функции"), он использует отложенный вызов исключений через делегаты... Я думаю, что когда я буду писать планируемую мной статью о современных методах создания сложного кода - то есть использование нового синтаксиса, добавленного в язык, функциональное (по форме) программирование и т.п., причем так, чтобы формально соответствовать модным признакам хорошего кода, а что-то вроде банальных и известных уже десятилетиями операторов goto и циклов do на пяти страницах, которые не стеснялись использовать настоящие программисты ещё в 70-е годы - я обязательно постараюсь использовать этот кусок кода как жизненный пример применения таких методов. К сожалению, к очевидному предпочтительному сроку выхода такой статьи, 1 апреля сего года ;-) , я не успел, так что этот план пока что отложен.

Далее будут рассмотрены интерфейсы, реализуемые классом GenericWebHostBuilder. Общая схема реализации интерфейсов изображена на Рис.2.

Рис.2  Интерфейсы, реализованные классом GenericWebHostBuilder
Рис.2 Интерфейсы, реализованные классом GenericWebHostBuilder
Дополнительные обозначения

Для иллюстрации реализации интерфейсов классом GenericWebHostBuilder, кроме обозначений, использованных на Рис.1, потребовались дополнительные обозначения. Прежде всего, это - расшифровки содержимого делегатов, создаваемых методами класса GenericWebHostBuilder: они на рисунке изображены в выносках, оформленных линией "штрих-две точки". Кроме того, использование механизма параметра (которое технически является конфигурирование нескольких описателей сервисов для контейнера серевисов) получило свое обозначение - прямоугольник со срезаннным правым верхним углом.

Рис.2 имеет в целом такую же компоновку как и рис.1. В его верхней части так же изображены блоки кода, выполняющие конфигурирование - вызовы обычных методов и методов расширения интерфейса IWebHostBuilder. (они, как было описано выше, выполняются внутри делегата - параметра метода ConfigureWebHost). При этом на рис.2 (как и на рис.1) те методы расширения IWebHostBuilder, которые в шаблоне Generic Host имеют специфическое поведение (оно будет рассмотрено ниже), подписаны. Еще один такой метод - UseStartup - был изображен на предыдущем рис.1. На этом рисунке его нет, т.к. этот метод и метод Configure, изображенный на этом рисунке являются взаимоисключающими. В средней части рисунка изображена реализация этих методов классом GenericWebHostBuilder. В нижней части рисунка схематически изображена часть класса построителя HostBuilder, с которой взаимодействует класс GenericWebHostBuilder.

Реализация интерефейса IWebHostBuilder

Перенаправляющие методы

Методы построителя веб-приложения IWebHostBuilder, просто перенаправляющие вызовы методам построителя IHostBuilder - это методы ConfigureAppConfiguration и ConfigureServices, причем второй имеет две формы (на рис.2 показана только одна из них), принимающие делегаты с разными сигнатурами. Для предоставления возможности объединения вызовов в цепочку эти методы возвращают ссылку на интерфейс IWebHostBuilder данного построителя веб-приложения (т.е., просто this) Методы ConfigureAppConfiguration и первая форма ConfigureServices принимают в качестве параметра единственный делегат, имеющий два параметра. Первый параметр делегатов для этих методов одинаковый - контекст построения веб-приложения WebHostBuilderContext, а второй параметр различается в соответствии с назначением метода: Для ConfigureAppConfiguration это ссылка на построитель конфигурации IConfigurationBuilder, для ConfigureServices - список регистраций сервисов IServiceCollection. Оба этих метода создают делегаты-обертки (их структура условно показана в выносках) для переданных им параметров-делегатов, так чтобы они соответствовали сигнатурам параметров-делегатов, принимаемых одноименными методами построителя IHostBuilder, и затем вызывают одноименный метод для родительского построителя _builder, передавая в него делегат-обертку в качестве аргумента.

Лирическое отступление: обертки???

Ну, я понимаю, что слово "обертка" выглядит непрофессионально. Профессиональнее было бы назвать это как-то так, чтобы всем было видно, что я в курсе современных модных трендов Computer Science. Но... Если вспомнить функциональное программирование, то можно было бы назвать это композицией. Однако эти делегаты ни разу не являются функциями, и иммутабльностью от них не и пахнет. Так что от всех красот ФП осталось бы только красивое название. Если вспомнить паттерны проектирования, то можно упомянуть "паттерн Адаптер". Но как быть тогда с вызовом метода GetWebHostBuilderContext - который ни разу не прост, и, вообще говоря, его деятельность не исчерпывается простым преобразованием одного типа параметра в другой (подробности - ниже, в соответствующем разделе)? Короче, по такой причине я решил тут не умничать, а назвать этот тип делегата (он еще не раз нам встретится) по-простому: оберткой.

Вторая форма метода ConfigureServices принимает в качестве параметра делегат с единственным параметром - списком регистрации сервисов IServiceCollection. Она создает делегат-обертку с сигнатурой, требуемой первой формой этого метода (на рис.2 не показан), и вызывает эту форму.

детали реализации: структура делегатов-оберток, создаваемых перенаправляющими методами GenericWebHostBuilder

Делегаты-обертки для получения контекста построителя Web-приложения WebHostBuilderContext вызывают частный (но доступный благодаря замыканию) вспомогательный метод GetWebHostBuilderContext (он будет рассмотрен позднее) с аргументом - контекстом построения HostBuilderContext, полученным в качестве первого параметра делегата-обертки, а затем вызывают исходный делегат с аргументами нужного типа: полученным контекстом построителя веб-приложения и вторым параметром делегата-обертки, тип которого, как было написано ранее, зависит от рассматриваемого метода. Созданный во второй форме метода ConfigureServices делегат-обертка принимает два параметра - контекст построителя веб-приложения IWebHostBuilder и список регистраций сервисов IServiceCollection, и вызывает исходный делегат-параметр метода только со вторым из этих параметров, игнорируя первый.

Методы доступа к внутренней конфигурации построителя веб-приложения

Методы GetSetting и UseSetting построителя веб-приложения IWebHostBuilder являются простой оболочкой для внутренней конфигурации построителя веб-приложения (которая, как описано выше, затем станет частью конфигурации построителя окружения IHostBuilder): их вызовы просто читают или устанавливают (в зависимости от метода) свойство по умолчанию интерфейса конфигурации IConfiguration. Для предоставления возможности объединения вызовов в цепочку метод UseSetting возвращает ссылку на интерфейс IWebHostBuilder данного построителя веб-приложения (т.е., просто this)

Интерфейсы перенаправления

Кроме интерфейса построителя веб-приложения IWebHostBuilder класс GenericWebHost реализует ещё два внутренних (то есть, недоступных для внешних программ) интерфейса, которые, исходя из выполняемых ими функций (они рассмотрены ниже) в статье будут назваться интерфейсами перенаправления: ISupportsUseDefaultServiceProvider и ISupportsStartup. Эти же интерфейсы реализует и ещё один класс для шаблона Generic Host, реализующий IWebHostBuilder - HostingStartupWebHostBuilder: класс-перехватчик, используемый в механизме конфигурации на основе стартовых сборок. Об этом классе будет рассказано ниже, при рассмотрении реализации этого механизма. Интерфейсы перенаправления используются в некоторых методах расширения для интерфейса построителя веб-приложения IWebHostBuilder, универсальных для обоих шаблонов построения веб-приложения: и Web Host, и Generic Host. На рис.1 и рис.2 изображены сами эти интерфейсы (рядом с интерфейсом IWebHostBuilder) и два примера их использования - методами расширения UseStartup вместе с UseDefaultServiceProvider на рис.1 и Configure вместе с UseDefaultServiceProvider на рис.2 (методы расширения UseStartup и Configure предназначены для использования в разных ситуациях, совместно они не используются, поэтому они показаны на разных рисунках).

детали реализации: класс, содержащий методы расширения для IWebHostBuilder

Все эти методы расширения определены в классе WebHostBuilderExtensions пространства имен Microsoft.AspNetCore.Hosting.

Нужда в этих интерфейсах возникла по той причине, что в Generic Host по сравнению с Web Host используется совершенно другая логика инициализации для ряда компонентов приложения (а именно - контейнера сервисов и конвейера Middleware). Поэтому рассмотренные ниже методы расширения IWebHostBuilder, связанные с конфигурированием этих компонентов, проверяют свой первый параметр - объект построителя веб-приложения, реализующий интерфейс IWebHostBuilder, реализует ли он соответствующий интерфейс перенаправления. И если реализует (то есть класс, реализующий IWebHostBuilder, является одним из классов из состава шаблона приложения Generic Host) - вызывает одноименный с методом расширения метод этого интерфейса, передавая в него в качестве аргументов остальные свои параметры. В противном случае метод расширения использует свою обычную логику, предназначенную для шаблона Web Host. То есть, здесь имеет место тот самый случай, о котором было упомянуто в первой части статьи: с помощью интерфейса перенаправления эти методы расширения как раз учитывают, какой именно класс реализует интерфейс, и изменяют в соответствии с этим свое поведение.

Методы расширения IWebHostBuilder, использующие интерфейсы перенаправления

Перейдем к рассмотрению тех методов расширения IWebHostBuilder, где они используются. Первый такой интерфейс, ISupportsUseDefaultServiceProvider, используется в методе расширения UseDefaultServiceProvider.

детали реализации: метод расширения UseDefaultServiceProvider для IWebHostBuilder

Этот метод имеет две перегруженных формы, различающиеся типом второго параметра-делегата: Одна из них принимает делегат, содержащий два параметра - контекст построителя веб-приложения WebHostBuilderContext и параметры (options) контейнера сервисов ServiceProviderOptions. (этот класс описан в первой части, при описании фабрики контейнера сервисов по умолчанию DefaultServiceProviderFactory). Именно эта форма реализует всю логику метода UseDefaultServiceProvider. Вторая форма принимает делегат, содержащий только один параметр - параметры (options) контейнера сервисов ServiceProviderOptions. Она просто вызывает первую форму с аргументом в виде делегата-обертки, имеющего нужную сигнатуру (с двумя параметрами), который вызывает внутри себя делегат-параметр метода UseDefaultServiceProvider со своим вторым параметром ServiceProviderOptions в качестве аргумента.

Второй такой интерфейс, ISupportsStartup, используется в методах расширения Configure и UseStartup (ну вот, наконец, мы и до UseStartup добрались). Метод UseStartup имеет несколько форм. Первая из них - обобщенная, она принимает в качестве параметра-типа Startup-класс (и никаких параметров больше не имеет). Но никакого метапрограммирования, как можно было бы подумать, в этой форме метода UseStartup нет: она просто вызывает вторую, необобщенную форму метода UseStartup с аргументом - своим параметром-типом. То есть здесь, как и во многих случаях в .NET Core, обобщенный метод - это просто сокращенная форма для передачи параметра типа System.Type. Вторая форма метода UseStartup принимает один параметр - тип Startup-класса. Работает она в целом по той же общей схеме для использующих интерфейс перенаправления методов расширения, что была описана выше.

детали реализации: метод расширения UseStartup для IWebHostBuilder

То есть, она производит проверку построителя веб-приложения IWebHostBuilder на реализацию интерфейса перенаправления ISupportsStartup (иначе говоря, использование шаблона Generic Host) и вызывает его одноименный метод, если интерфейс реализован; в противном случае выполняет код для шаблона Web Host. Но перед проверкой этот метод дополнительно записывает во внутреннюю конфигурацию построителя веб-приложения IWebHostBuilder под ключом "applicationName" имя сборки, содержащей Startup-класс. Это значение, в частности, впоследствии используется в конструкторе класса WebHostOptions, с которым мы ещё встретимся позднее. Ну, и в самом приложении оно может быть использовано, поскольку, в конце концов, становится доступным через сервис конфигурации (IConfiguration). В любом случае и в любой форме метод UseStartup возвращает ссылку на IWebHostBuilder для объединения методов в цепочку.

Второй метод расширения для IWebHostBuilder, в котором используется внутренний интерфейс перенаправления ISupportsStartup - это метод Configure. Этот метод, как известно, можно использовать для конфигурирования конвейера Midleware веб-приложения вместо метода Configure в Startup-классе, если не требуется функциональность конфигурирования контейнера сервисов, которую тоже обеспечивает Startup-класс. Метод Configure имеет две перегруженные публичные формы и одну частную. Публичные формы имеют обе по два параметра. Первый параметр в обеих формах, как и положено методу расширения, имеет тип расширяемого интерфейса IWebHostBuilder, в качестве второго параметра используются делегаты конфигурирования конвейера Middleware с разными сигнатурами. Обе публичные формы вызывают частную форму (с тремя параметрами, к первым двум добавляется ещё имя приложения) Эта частная форма тоже работает в целом по той же общей схеме для использующих интерфейс перенаправления методов расширения.

детали реализации: метод расширения Configure для IWebHostBuilder

Для первой формы это делегат очереди конфигурирования Middleware с одним параметром - делегатом, имеющим единственный параметр - ссылку на интерфейс создания конвейера Midleware IApplicationBuilder, для второй формы делегат очереди конфигурирования Middleware принимает два параметра: кроме уже упомянутого делегата (он является вторым параметром), ещё и контекст построителя веб-приложения WebHostBuilderContext. Обе публичные формы вызывают частную форму с аргументами: первым - интерфейсом IWebHostBuilder, вторым - делегатом с сигнатурой, соответствующей второй форме (первая форма для этого создает делегат-обертку, которая вызывает делегат-параметр этой формы с одним только вторым параметром обертки) и третьим - именем приложения. В качестве имени используется название сборки, в которой определен параметр-делегат метода Configure. В любом случае и в любой форме метод Configure также возвращает ссылку на IWebInterfaceBuilder для объединения методов в цепочку.

Реализация интерфейсов перенаправления в классе GenericWebHostBuilder

Теперь вернемся к классу GenericWebHostBuilder и рассмотрим, как в нем реализованы методы интерфейсов перенаправления. Все эти методы возвращают интерфейс IWebHostBuilder (ссылку на текущий экземпляр GenericWebHostBuilder, то есть this), для объединения в цепочку

Метод ISupportsUseDefaultServiceProvider.UseDefaultServiceProvider вызывает метод UseServiceProviderFactory (типом контейнера-построителя IServiceCollection в качестве параметра-типа) родительского построителя IHostBuilder - ту его форму, параметром которой является делегат-фабрика контейнера сервисов. Этот делегат возвращает экземпляр класса фабрики контейнера сервисов по умолчанию DefaultServiceProviderFactory, в конструктор которого передан объект параметров контейнера сервисов, полученный как параметр метода UseDefaultServiceProvider.

детали реализации: ISupportsUseDefaultServiceProvider.UseDefaultServiceProvider

Делегат сначала получает контекст построителя веб-приложения WebHostBuilderContext с помощью вспомогательного метода GetWebHostBuilderContext, передавая в него в качестве аргумента параметр делегата - контекст построения HostBuilderContext. затем делегат создает новый объект параметров фабрики контейнера сервисов ServiceProviderOptions. После этого созданный делегат вызывает тот делегат, который был передан в метод UseDefaultServiceProvider с аргументом - вновь созданным объектом ServiceProviderOptions для установки свойств этого объекта. И, наконец, делегат создает и возвращает экземпляр фабрики контейнера сервисов по умолчанию DefaultServiceProviderFactory, в конструктор которого был передан сконфигурированный объект параметров контейнера сервисов ServiceProviderOptions.

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

Следующий метод интерфейса перенаправления, реализованный в классе GenericWebHostBuilder, ISupportsStartup.UseStartup принимает параметр System.Type, являющийся типом Startup-класса, который этот метод регистрирует для использования при инициализации веб-приложения. По существу, все, что делает этот метод - это помещает в очередь конфигурирования списка регистраций сервисов родительского построителя IHostBuilder вызов делегата. Этот делегат на соответствующем этапе вызовет внутренний метод UseStartup (с тремя параметрами), причем сделает это гарантированно однократно, лишь для последнего добавленного Startup-класса. Это нужно, потому что, в принципе, не запрещено вызывать метод расширения UseStartup из нескольких мест с несколькими разными регистрируемыми Startup-классами, а использовать нужно только последний по порядку Startup-класс, зарегистрированный таким образом.

детали реализации: однократный вызов методов Startup-класса

Как реализовано однократное добавление. Метод UseStartup помимо добавления делегата ещё и запоминает в словаре построителя Properties под ключом "UseStartup.StartupType" тип регистрируемого Startup-класса. При следующей регистрации это значение перезаписывается типом следующего зарегистрированного Startup-класса (на рис.2 это не показано). А делегаты, помещенные методом UseStartup в очередь конфигурирования списка регистраций сервисов, сравнивают это значение из словаря с полученным через замыкание значением типа, для который они должны вызвать внутренний метод UseStartup, и вызывают его, только когда эти значения совпадают.

Сам внутренний метод UseStartup довольно сложен. Он будет подробно рассмотрен позднее.

И последний метод интерфейса перенаправления, реализованный в классе GenericWebHostBuilder - это ISupportsStartup.Configure. Он принимает в качестве параметра делегат с двумя параметрами - контекстом построителя веб-приложения WebHostBuilderContext и интерфейсом создания конвейера Midleware IApplicationBuilder, и помещает его в то место, откуда он будет вызван в процессе создания конвейера Midleware.

детали реализации: метод ISupportsStartup.Configure

Далее в рассказе этот делегат, который был передан передается в метод ISupportsStartup.Configure, будет называться исходным или №0. Делается это следующим образом. В очередь конфигурирования списка регистрации сервисов помещается делегат конфигурирования установки значения параметра (option) типа GenericWebHostServiceOptions (№1), который будет выполнен, соответственно, на стадии конфигурирования списка регистрации серверов. В качестве параметров в него, как обычно будут переданы контекст построения HostBuilderContext и ссылка на конфигурируемый список регистраций сервисов IServiceCollection. Этот делегат (№1) конфигурирует в списке регистрации сервисов (одним из вариантов его метода расширения Configure) использование параметра (option) указанного типа с установкой значения (неименованного) этого параметра путем выполнения делегата задания значения (№2). Параметр типа GenericWebHostServiceOptions передается в конструктор класса веб-приложения GenericWebHostedService (через внедрение зависимостей) и используется в методе его запуска StartAsync. Делегат создания значения (№2) выполняется как раз в этот момент - в момент получения значения внутри метода StartAsync через интерфейс параметра (option). При выполнении делегат задания значения (№2) сначала получает контекст построителя веб-приложения WebHostBuilderContext с помощью вспомогательного внутреннего метода GetWebHostBuilderContext класса GenericWebHostBuilder (доступ к этому методу он получает через замыкание), передавая в него в качестве аргумента контекст построителя (полученный также через замыкание как параметр делегата конфигурирования установки значения параметра (№1)). Затем делегат создания значения (№2) записывает в свойство ConfigureApplication (постобработчик стартового кода) конфигурируемого объекта значения (этот объект он получает через свой параметр) делегат вызова исходного делегата (№3), который вызывает исходный делегат (№0) с аргументами - полученным контекстом построителя веб-приложения WebHostBuilderContext (будет получен через замыкание) и интерфейсом создания конвейера Midleware IApplicationBuilder (этот аргумент он получит в качестве своего единственного параметра). Этот делегат (№3) будет вызван в том же методе GenericWebHostedService.StartAsync немного позже, в процессе создания конвейера Midleware . и вызовет исходный делегат (№0). И, кстати, совершенно не факт, что этот делегат будет вызван: если после вызова метода расширения Configure будет идти другой вызов этого метода или метода UseStartup (он точно так же использует делегат для установки того же самого свойства неименованного значения того же самого типа с целью обеспечения вызова метода Configure, см. ниже), то значение этого свойства будет перезаписано другим делегатом, не №3. На рис.2 вся эта сложность не показана: там, чтобы сохранить ясность, показан только приближенный результат - делегат, переданный в Configure, будет записан в свойство ConfigureApplication передаваемого через механизм параметров (Options) экземпляра класса GenericWebHostServiceOptions.

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

Как происходит процесс создания конвейера Middleware, будет рассмотрено ниже.

Лирическое отступление после предыдущих деталей реализации

Неплохо, да? Всего десять строк тела метода - и такие глубины, даже, не побоюсь этого слова, бездны фреймворка открываются. Причем, надо учесть, что это я сейчас написал, какие делегаты когда выполняются, и с какой целью это делается: в коде никаких комментариев, естественно нет, в документации такое тоже не описывают. А если ещё учесть, что делегат №3 неплохо замаскирован (равными отступами вокруг знака равенства и стрелки, что, убаюкивая глаз, делает его похожим на сравнение)... Короче, этот кусок кода - ещё один из очевидных кандидатов в примеры, как писать сложный код современными методами.

Конфигурирование на основе стартовых сборок

Однако продолжим дальше рассмотрение процесса конфигурирования настроек веб-приложения. Следующим на очереди у нас будет механизм конфигурирования веб-приложения на основе стартовых сборок (Startup Assemblies). Осуществляют его те самые три делегата, которые конструктор построителя веб-приложения поместил в очереди делегатов для разных этапов построения приложения. Операции, которые выполняются этими делегатами, схематически изображены на рис.3.

Рис.3 Конфигурирование на основе стартовых сборок и другие операции делегатов конструктора.
Рис.3 Конфигурирование на основе стартовых сборок и другие операции делегатов конструктора.
Дополнительные обозначения

Для иллюстрации работы делегатов, помещенных конструктором класса построителя веб-приложения GenericWebHostBuilder в очереди для этапов работы построителя HostBuilder, и в частности - процесса конфигурирования на основе стартовых сборок, потребовалгсь ещё дополнительные обозначения. Во-первых - для этих самых сборок: они изображены на рис. прямоугольниками со скругленными углами, нарисованными двойными линиями (и внутри этих прямоугольников условно нарисованы содержащиеся в сборках классы). Во-вторых, производимые в процессе конфигурирования многочисленные вызовы интерфейсов построителя веб-приложения (IWebHostBuilder) и перенаправления из методов расширения этого интерфейса (они были рассмотрены ранее) условно изображены толстыми черными линиями.

Первый делегат конструктора и метод ExecuteHostingStartups

Первый делегат (изображен в выноске "1st .ctor delegate" на рис.3), как было написано выше, выполняется на этапе создания конфигурации построителя (IHostBuilder). Этот делегат, завершив описанное выше добавление внутренней конфигурации построителя веб-приложения GenericWebHostBuilder к списку для источников конфигурации построителя (в передаваемой ему в качестве параметра внутренней переменной configBuilder в BuildHostConfiguration, что отражено на рис.3), вызывает внутренний метод ExecuteHostingStartups класса GenericWebHostBuilder.

Метод ExecuteHostingStartups производит следующие действия. Во-первых, он создает из объекта внутренней конфигурации объект типизированных параметров веб-приложения, имеющий тип WebHostOptions.

детали реализации: аргумент конструктора WebHostOptions

Именем приложения по умолчанию для конструктора WebHostOptions указывается имя входной сборки

Затем этот метод проверяет, запрещено ли в типизированных параметрах веб-приложения WebHostOptions использование механизма конфигурирования на основе стартовых сборок (установлен флаг PreventHostingStartup), и если так - просто возвращает управление. В противном случае начинается подготовка к конфигурированию на основе стартовых сборок. Первым делом создается список исключений, в который будут заноситься исключения, возникшие в коде конфигурирования в каждой из стартовых сборок и перехваченные в данном методе.

Класс-перехватчик HostingStartupWebHostBuilder - реализация IWebHostBuilder для вызова методов конфигурирования в стартовых сборках

Далее в методе создается объект класса-перехватчика HostingStartupWebHostBuilder (на рис.3 изображен над классом GenericWebHostBuilder) - специальной реализации IWebHostBuilder, которая будет передаваться коду стартовых сборок при вызове методов инициализации. Ссылка на него записывается в поле _hostingStartupWebHostBuilder. Класс-перехватчик реализует также оба интерфейса перенаправления, которые реализует и GenricWebHostBuilder - ISupportsUseDefaultServiceProvider и ISupportsStartup, поэтому методы расширения, проверяющие наличие этих интерфейсов (как специальный путь выполнения для шаблона Generic Host) будут работать точно так же, как и с GenericWebHostBuilder. Внутри себя класс-перехватчик содержит ссылку на родительский объект GenericWebHostBuilder (он получает ее из конструктора), и при вызове большинства своих методов он переадресует вызов в соответствующий метод родительского объекта построителя веб-приложения. И лишь для методов ConfigureAppConfiguration и ConfigureServices (последний существует в двух перегруженных формах) переадресация не производится, вместо этого класс-перехватчик накапливает передаваемые через них делегаты - предназначенные для выполнения на этапах окончательного создания конфигурации приложения и конфигурирования списка регистраций сервисов соответственно - в своих внутренних списках, для вызова их позднее, на соответствующих стадиях построения окружения (IHost). Для вызова этих накопленных делегатов служат специальные перегруженные формы этих же методов ConfigureAppConfiguration и ConfigureServices. Они будут разобраны позднее, при рассмотрении делегатов для соответствующих стадий, которые в соответствующие очереди поместил конструктор класса GenericWebHostBuilder (а именно - второго и третьего делегатов): именно они вызывают накопленные в классе-перехватчике делегаты.

детали реализации: зачем нужен и как реализован класс-перехватчик

Это сделано так, чтобы делегаты в очередях конфигурирования были выполнены в нужном порядке: если бы класс-перехватчик просто переадресовывал бы вызовы этих методов вместо накопления списков (или бы его не было, и конфигурирование на основе стартовых сборок работало бы напрямую с GenericWebHostBuilder), то добавленные этими вызовами делегаты были бы помещены в свои очереди после делегатов, добавленных вызовами из класса Program (напомним, что конфигурирование на основе стартовых сборок производится уже в методе Build, после завершения метода Program.CreateHostBuilder). А это неправильно. Поэтому делегаты сначала накапливаются во внутренних списках класса-перехватчика, а потом выполняются в нужный момент - в тот момент, когда подходит очередь делегатов, помещенных туда конструктором класса GenericWebHostBuilder (то есть, если смотреть из класса Program - изнутри методов ConfigureWebHost/ConfigureWebHostDefaults, до выполнения переданного в них делегата) По неизвестной мне причине списки делегатов реализованы не как объекты обобщенного типа List<>, специализированного нужным типом делегата, а с использованием функциональности комбинирования делегатов: когда делегаты добавляются один к другому с помощью оператора + (или +=), а затем выполняются одним вызовом комбинированного делегата. В причинах такой разницы в реализации я не разбирался. Вторая форма метода ConfigureServices принимает в качестве параметра делегат с единственным параметром - списком регистрации сервисов IServiceCollection. Она создает делегат-обертку с сигнатурой, требуемой первой формой этого метода, и вызывает эту форму. Созданный в этой форме делегат-обертка принимает два параметра - контекст построителя веб-приложения IWebHostBuilder и список регистраций сервисов IServiceCollection, и вызывает исходный делегат-параметр метода только со вторым из этих параметров, игнорируя первый.

После этого метод ExecuteHostingStartups получает из типизированных параметров веб-приложения WebHostOptions список стартовых сборок, которые будут использоваться для конфигурирования

детали реализации: список стартовых сборок

В этот список изначально добавляется сборка с именем самого приложения (по умолчанию это - его стартовая сборка), а также - сборки перечисленные в параметре конфигурации "hostingStartupAssemblies", а затем из него исключаются все сборки, перечисленные в параметре конфигурации "hostingStartupExcludeAssemblies".

Каждая такая сборка загружается, в ней находятся все классы, которые выполняют конфигурирование - это классы, перечисленные в атрибутах HostingStartupAttribute и реализующие интерфейс IHostingStartup. Для каждого такого класса вызывается метод Configure реализованного им интерфейса IHostingStartup с аргументом - ссылкой на экземпляр класса-перехватчика. Процесс обработки стартовых сборок схематически изображен в верхней части рис.3. Все возникшие при этом исключения перехватываются и добавляются в ранее созданный список, после чего перечисление сборок и классов возобновляется. После завершения перечисления метод проверяет, возникли ли исключения, и если да, то создает объединяющий их объект AggregateException, который записывается в поле _hostingStartupErrors объекта построителя веб-приложения GenericWebHostBuilder. Впоследствии это исключение будет обработано третьим делегатом.

Второй делегат конструктора

Второй делегат (изображен в выноске "2nd .ctor delegate" на рис.3) был помещен конструктором класса GenericWebHostBuilder в очередь стадии окончательного создания конфигурации приложения. Он сначала вызывает вспомогательный метод GetWebHostBuilderContext для получения контекста построителя веб-приложения WebHostBuilderContext. Затем он вызывает перегруженную версию метода ConfigureAppConfiguration класса-перехватчика из поля _hostingStartupWebHostBuilder. В качестве аргументов в этот метод передается полученный контекст построителя веб-приложения и ссылка на построитель конфигурации IConfigurationBuilder. Именно эта перегруженная версия ConfigureAppConfiguration - с первым параметром-контекстом построителя веб-приложения - и есть тот метод, который вызывает применение всех накопленных классом-перехватчиком делегатов, переданных через одноименный метод интерфейса IWebHostBuilder. Эти делегаты изменяют содержимое передаваемой им качестве параметра внутренней переменной configBuilder метода Host.ConfigureAppConfiguration (что условно отображено на рис.3), которая затем используется для построения конфигурации приложения.

детали реализации: второй делегат конструктора

Аргументом GetWebHostBuilderContext является контекст построения HostBuilderContext - первый параметр делегата, ну а ссылка на построитель конфигурации - это его второй параметр Ну и, перед тем, как вызвать метод ConfigureAppConfiguration класса-перехватчика, этот делегат проверяет, создан ли вообще объект этого класса: если конфигурирование на основе стартовых сборок запрещено, то этого объекта не будет. Накопленные делегаты метод ConfigureAppConfiguration класса-перехватчика применяет путем вызова комбинированного делегата, в котором скомбинированы все накопленые делегаты, передавая ему в качестве аргументов полученные параметры: контекст построителя веб-приложения и ссылку на построитель конфигурации IConfigurationBuilder.

Третий делегат конструктора - не только конфигурирование на основе стартовых сборок

Третий делегат (изображен в выноске "3rd .ctor delegate" на рис.3), помещенный конструктором класса GenericWebHostBuilder в очередь стадии конфигурирования списков регистрации сервисов, помимо завершения конфигурирования на основе стартовых сборок, выполняет еще ряд важных функций (не зря же в нем больше 50 строк кода). Параметры, которые получает этот делегат - это контекст построения HostBuilderContext и ссылка на конфигурируемый список регистрации сервисов IServiceCollection (внутренняя переменная services метода Host.CreateServiceProvider, см. рис.3). Для начала этот делегат также вызывает вспомогательный метод GetWebHostBuilderContext для получения контекста построителя веб-приложения WebHostBuilderContext. Помимо этого он извлекает (на рис.3 показано штриховой стрелкой) из словаря Properties (на рис.3 эта деталь опущена) контекста построения HostBuilderContext также типизированные параметры веб-приложения - объект типа WebHostOptions, который помещается туда тем же методом GetWebHostBuilderContext. Затем третий делегат конструктора регистрирует в списке регистрации сервисов стандартный набор сервисов для веб-приложения.

детали реализации: регистрация стандартного набора сервисов

А вот списка этих сервисов в дркументации, кажется, нет - пожтому приовожу его здесь. Обязательно регистрируются следующие сервисы: IWebHostEnvironment и AspNetCore.Hosting.IHostingEnvironment - время жизни постоянное (Singleton), реализацией является экземпляр класса HostingEnvironment, создаваемый методом GetWebHostBuilderContext, на него ссылается свойство HostingEnvironment созданного этим методом контекста построителя веб-приложения WebHostBuilderContext (см. описание этого метода). IApplicationLifetime - время жизни постоянное (Singleton), реализация - класс GenericWebHostApplicationLifetime В случаи отсутствия других регистраций регистрируются также следующие сервисы:

  • Diagnostic Listener, Diagnostic Source - время жизни постоянное (Singleton), реализация - экземпляр объекта типа DiagnosticListener, конструктор которого вызван с аргументом-строкой "Microsoft.AspNetCore" (примечание: в процесе написания статьи, в конце марта, реализация была заменена на фабрику, создающую экземпляр этого типа - вероятно, чтобы контейнер сервисов мог контролировать время его жизни и вызвать для него Dispose);

  • IHttpContextFactory - время жизни постоянное (Singleton), реализуются классом DefaultHttpContextFactory;

  • IMiddlewareFactory - время жизни - ограниченная область (Scoped), реализуются классом MiddlewareFactory;

  • IApplicationBuilderFactory - время жизни постоянное (Singleton), реализуются классом ApplicationBuilderFactory;

Кроме того, в списке регистрации сервисов (одним из вариантов его метода расширения Configure) регистрируется использование параметра (option) типа GenericWebHostServiceOptions, с установкой в его значении (неименованном) свойств WebHostOptions и HostingStartupExceptions путем выполнения делегата задания значения. При этом знакомое уже нам свойство ConfigureApplication (в дальнейшем я буду называть его постобработчик стартового кода), в котором находится делегат для конфигурирования конвейера Middleware, не затрагивается.
В свойство WebHostOptions значения заносится ссылка на полученный ранее объект типизированных параметров веб-приложения, а в свойство HostingStartupExceptions - исключение типа AggregateException из поля _hostingStartupErrors класса GenericWebHostBuilder, которое может там появиться в результате выполнения конфигурирования на основе стартовых сборок.
Использоваться значения этих свойств, как и постобработчика стартового кода - свойства ConfigureApplication, будут в методе StartAsync класса веб-сервиса GenericWebHostedService, при запуске веб-приложения. В частности, именно там будут записаны в лог объединенные в AggregateException исключения, возникшие при в процессе конфигурирования на основе стартовых сборок и переданные через свойство HostingStartupExceptions.

Лирическое отступление: параметр, которого нет

Точнее - пока ещё нет. На момент выполнения конструктора класса GenericWebHostBuilder поле _hostingStartupErrors ещё никакого исключения содержать не может - но делегат может сослаться на это поле. Исключение там может появиться в процессе конфигурирования на основе стартовых сборок, выполняемом позже, на стадии построения конфигурации построителя. Ну, а делегат, когда будет выполняеться, на еще более поздней стадии, скопирует ссылку на это исключение в нужное свойство нужного объекта GenericWebHostServiceOptions.HostingStartupExceptions Не правда ли, ловко получилось все запутать?

После этого третий делегат занимается завершением конфигурирования на основе стартовых сборок - он вызывает перегруженную версию метода ConfigureServices класса-перехватчика из поля _hostingStartupWebHostBuilder (если класс-перехватчик вообще был создан). В качестве аргументов в этот метод передается полученный контекст построителя веб-приложения WebHostBuilderContext и ссылка на список регистраций сервисов IServiceCollection. Именно эта, перегруженная версия ConfigureServices - с первым параметром-контекстом построителя веб-приложения - и есть тот метод, который вызывает применение всех накопленных классом-перехватчиком делегатов, переданных через одноименный метод интерфейса IWebHostBuilder.

детали реализации: вызов накопленных делегатов для ConfigureServices в классе-перехватчике

Накопленные делегаты метод ConfigureServices класса-перехватчика применяет путем вызова комбинированного делегата, в котором скомбинированы все накопленые делегаты, передавая ему в качестве аргументов полученные параметры: контекст построителя веб-приложения и ссылку на список регистраций сервисов IServiceCollection.

И последнее действие, которое выполняет третий делегат - это использование для конфигурирования Startup-класса, указанного через имя содержащей его сборки. Имя сборки задается свойством StartupAssembly в объекте типизированных параметров веб-приложения WebHostOptions (это свойство берется из значения конфигурации построителя для ключа "startupAssembly", и его значение может быть задано перегруженной формой метода расширения UseStartup для построителя веб-приложения IWebHostBuilder, принимающей параметр-строку - имя сборки). При этом конкретное имя класса в сборке определяется по соглашению, аналогичному соглашению об именах методов Startup-класса: используется класс с именем Startup с суффиксом - именем окружения, например - StartupDevelopment, а при отсутствии такого класса - класс с именем Startup.

детали реализации: поиск Startup-класса в сборке по имени для запуска основанного на соглашениях

За нахождение нужного класса отвечает метод FindStartupType статического класса StartupLoader (который в целом отвечает за работу со Startup-классом, с ним мы ещё встретимся позднее). Этот метод загружает указанную сборку и ищет через содержащуюся в данных отражения информацию класс с подходящим именем (сначала - с суффиксом окружения, затем - без суффикса). Первоначально он проверяет корневое пространство имен и пространство имен с именем сборки (сначала - для имени суффиксом). Если тип с подходящим именем найден - возвращается именно он (в том числе в этом случае может быть возвращен тип с именем Startup без суффикса, даже в сборке в других пространствах имен есть тип с именем, содержащим суффикс окружения). Иначе метод FindStartupType строит список типов с подходящими именами из всех пространств имен (сначала - имена с суффиксом, затем - без) и возвращает первый найденный в списке тип. Если тип найти не удалось, то выбрасывается исключение InvalidOperationException.

Затем для найденного типа вызывается внутренний метод UseStartup (с тремя параметрами), аналогично тому, как это делается в случае Startup-класса, указанного явно. Работа этого метода будет рассмотрена чуть позже.

Лирическое отступление: тут ошибка?

И, похоже, в этом месте в коде ASP.NET содержится (причем - до сих пор) логическая ошибка: в отличие от делегата, помещаемого в очередь стадии конфигурирования списка регистрации сервисов IServiceCollection методом интерфейса перенаправления ISupportsStartup.UseStartup, в этом месте однократность вызова внутреннего метода UseStartup не гарантируется: третий делегат, помещенный конструктором, не проверяет, что в эту же очередь был после него помещен методом ISupportsStartup.UseStartup делегат вызова внутреннего метода UseStartup. Проверить это было бы несложно по содержимому словаря построителя (или, что то же самое - словаря контекста построителя - это один и тот же объект) Properties под ключом "UseStartup.StartupType": если значения под этим ключом есть (а ISupportsStartup.UseStartup именно туда записывает тип Startup-класса для избежания повторного вызова одноименного внутреннего метода), то в очереди уже находится, как минимум, один делегат вызова внутреннего метода UseStartup и вызывать этот метод здесь будет неправильным (его вызов был перекрыт последующим методом UseStartup).

К сожалению, у меня пока не нашлось времени, чтобы подготовить исправление как полодено: создать код, визуализующий эту ошибку, поправить ее (это элементарно - надо просто добавить проверку наличия значения под ключом UseStartup.StartupType), и, главное - настроить и выполнить сборку библиотеки вместе со всеми тестами, чтобы убедиться, что при исправлении ничего не сломано.

Если в типизированных параметрах веб-приложения WebHostOptions установлен флаг CaptureStartupErrors (в конфигурации построителя ему соответствует ключ "captureStartupErrors"), то все исключения, возникающие в процессе нахождения Startup-класса, указанного через имя содержащей его сборки, перехватываются, и их обработка откладывается на этап запуска веб-приложения.

детали реализации: реализация захвата исключения для его последующего выброса вместо метода Configure Startup-класса

Для этого в списке регистрации сервисов регистрируется (одним из вариантов его метода расширения Configure) сервис установки неименованного значения параметра (option) типа GenericWebHostServiceOptions, записывающий в постобработчик стартового кода - свойство ConfigureApplication делегат, повторно выбрасывающий это исключение. Этот делегат будет вызван в методе вместо в методе StartAsync класса веб-сервиса GenericWebHostedService при запуске веб-приложения вместо делегата, вызывающего метод Configure Startup-класса. Таким образом, исключение будет повторно выброшено в методе запуска веб-приложения для обработки его там (например - для отображения на странице веб-сайта), что как раз и подразумевает семантика флага CaptureStartupErrors

Внутренний метод GetWebHostBuilderContext класса GenericWebHostBuilder

Прежде чем двигаться дальше, стоит рассмотреть подробнее внутренний метод GetWebHostBuilderContext, о котором упоминалось ранее. Данный метод принимает один параметр - контекст построения HostBuilderContext. Этот метод предназначен, прежде всего, как следует из его названия, для создания объекта контекста построителя веб-приложения WebHostBuilderContext. Создается этот объект данным методом один раз - если он ещё не был создан, и ссылка на него сохраняется в словаре Properties контекста построения HostBuilderContext под ключом равным объекту-типу (типа System.Type) для этого класса. При последующих обращениях к методу GetWebHostBuilderContext этот объект извлекается из словаря-кэша, при необходимости - немного корректируется (подробности см. ниже) и возвращается вызвавшему коду.

детали реализации: метод GetWebHostBuilderContext

Для начала стоит сделать небольшое примечание для облегчения дальнейшего понимания: словарь Properties контекста построения и одноименное свойство построителя IHostBuilder реализованного классом HostBuilder - это один и тот же объект. Поэтому не надо удивляться встречающимся иногда (например, в рассматриваемом куске кода) случаям, когда объект заносится в словарь контекста построения, а доступ к нему производится через интерфейс построителя IHostBuilder или наоборот. Теперь можно продолжить рассказ о подробностях реализации и использовании метода GetWebHostBuilderContext. Прежде всего, отметим, что метод GetWebHostBuilderContext создает экземпляр объекта WebHostBuilderContext и копирует в его свойство Configuration ссылку на конфигурацию из контекста построения HostBuilderContext - параметра метода только один раз - припервом вызове. А так как первый вызов GetWebHostBuilderContext (он производится из второго делегата конструктора) происходит на стадии создания конфигурации приложения, то в объект WebHostBuilderContext первоначально передается ссылка на конфигурацию построителя: свойство Configuration в этот момент содержит именно ее, однако значение этого свойства будет заменено на конфигурацию приложения позже, после окончания построения этой конфигурации. Затем этот метод создает новый экземпляр типа HostingEnvironment и сохраняет ссылку на созданный экземпляр в свойстве HostingEnvironment созданного ранее объекта WebHostBuilderContext, после чего - вызывает метод расширения Initialize (определен в классе HostingEnvironmentExtensions) для экземпляра типа HostingEnvironment для его инициализации. Метод Initialize вызывается с двумя аргументами: строкой-путем к содержимому приложения (из свойства ContentRootPath окружения размещения (хоста) IHostEnvironment, на которое ссылается поле HostingEnvironment контекста построения HostBuilderContext) и объектом типизированных параметров веб-приложения WebHostOptions, о котором будет рассказано чуть позднее и который на этот момент уже создан. Данный метод копирует свойство ContentRootPath из переданного ему строкового параметра, свойства ApplicationName и EnvironmentName (последнее - если не null, иначе используется значение по умолчанию "Production") - из второго параметра, объекта типизированных параметров WebHostOptions, свойство WebRootPath также берется из того же параметра, но, если этот путь - относительный, преобразуется в абсолютный, считая его путем относительно ContentRootPath. Файловыми провайдерами ContentRootPathProvider и WebRootPathProvider становятся объекты типа PhysicalFileProvider с базовыми путями ContentRootPath и WebRootPath соответственно.

Но создание объекта контекста построителя веб-приложения WebHostBuilderContext - не единственная функция метода GetWebHostBuilderContext: попутно он, также при первом вызове, создает объект типизированных параметров веб-приложения WebHostOptions и точно так же сохраняет в словаре Properties контекста построения HostBuilderContext ссылку на него под ключом равным объекту-типу (типа System.Type) для этого класса. Этот объект может использоваться другим кодом в классе GenericWebHostBuilder, в частности, как мы видели выше, его использует третий делегат из конструктора этого класса.

детали реализации: конструктор WebHostOptions

Конструктор WebHostOptions принимает два параметра: конфигурацию (IConfiguration), из которой строится этот объект типизированных параметров? и имя приложения по умолчанию. В качестве аргумента-конфигурации в конструктор WebHostOptions передается ссылка на конфигурацию из контекста построения HostBuilderContext (свойство Configuration). Как уже было сказано выше эта ссылка указывает в момент вызова конструктора WebHostOptions на конфигурацию построителя. Именем приложения по умолчанию для конструктора WebHostOptions указывается имя входной сборки. Свойства объекта WebHostOptions заполняются на основе содержимого конфигурации, переданной первым параметром Если имя приложения в конфигурации не указано, то вместо него используется имя приложения по умолчанию из второго параметра. Список свойств объекта WebHostOptions достаточно велик (их более 10), поэтому я позволю себе не перечислять их и те имена значений конфигурации, из которых они берутся.

Если же данный вызов GetWebHostBuilderContext - не первый, и объект контекста построителя веб-приложения WebHostBuilderContext уже создан и находится в словаре, то метод GetWebHostBuilderContext заменяет в этом объекте ссылку на конфигурацию (IConfiguration) в свойстве Configuration на содержимое одноименного свойства контекста построения HostBuilderContext и возвращает вызывающему коду существующий объект. Ссылка на конфигурацию корректируется, потому что в процессе построения конфигурации содержимое свойства Configuration контекста построения изменяется (см. описание этапов построения конфигурации в первой части: ссылка на конфигурацию построителя заменяется на ссылку на конфигурацию приложения).

Внутренний метод UseStartup класса GenericWebHostBuilder

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

Имена и сигнатуры методов Startup-класса

Хотя в руководствах обычно говорится о двух методах, имеющих базовое (почему оно базовое - будет объяснено ниже) название ConfigureServices и Configure, но в Startup-классе может также быть определен и третий метод конфигурирования, с базовым названием ConfigureContainer: это - обобщенный метод, который можно использовать для конфигурирования контейнера-построителя, с типом которого совпадает параметр-тип этого метода. Слово "базовое" тут использовано неспроста: реальные имена методов Startup-класса могут не совпадать с базовыми, а иметь дополнительный компонент - название окружения (Development, Production и т.д.), внедренный в базовое название после части "Configure" (например, ConfigureProductionServices или ConfigureDevelopment). Более того, при выборе используемой реализации метода такие имена имеют приоритет над именами в виде базовых названий. Далее для краткости я не буду упоминать про возможность внедрения компонента названия окружения, а буду называть методы по их базовым названиям или, когда нужно подчеркнуть, что имя может не совпадать с базовым названием - методами типа "базовое название" (соответственно, типа ConfigureServices, типа ConfigureContainer или типа Configure).

История имен и сигнатур методов Startup-класса явно была непростой. Если посмотреть на то, как этот класс используется в шаблоне Web Host, то там можно увидеть три варианта. Первый вариант - Startup-класс реализует интерфейс IStartup, имеющий методы ConfigureServices и Configure с жестко заданными именами и сигнатурами (то есть никаких внедрений имени окружения не предусмотрено), причем в сигнатуре метода ConfigureServices указано, что этот метод возвращает интерфейс контейнера сервисов IServiceProvider, то есть - что он самостоятельно создает контейнер сервисов. В таком случае методы Startup-класса вызываются именно через интерфейс IStartup. В противном случае Startup-класс реализует т.н. запуск, основанный на соглашениях (Convention Based Startup), при которых код ASP.NET Core отыскивает в Startup-классе методы с именами, соответствующими соглашению и вызывает их. Этот противный случай распадается, однако, на два варианта: метод типа ConfigureServices, т.е. конфигурирующий список регистраций сервисов, может как самостоятельно создавать контейнер сервисов - тогда он возвращает IServiceProvider, так и оставлять создание контейнера сервисов на долю фреймворка ASP.NET Core - тогда тип возврата этого метода будет void.

Но в шаблоне Generic Host из всех этих вариантов реализации Startup-класса допускается только самый последний: когда метод ConfigureServices не возвращает значение (созданный контейнер сервисов): в рамках данного шаблона создание контейнера сервисов происходит исключительно механизмами фреймворка, а не Startup-классом.

Списки параметров метода ConfigureServices и обобщенного метода ConfigureContainer<> жестко заданы. Метод ConfigureServices должен принимать один параметр - ссылку на список регистрации сервисов IServiceCollection, подлежащий конфигурированию. Обобщенный метод ConfigureContainer<> должен также принимать один параметр - ссылку на контейнер-построитель, подлежащий конфигурированию. Тип этого параметра задается параметром-типом этого обобщенного метода. Оба этих метода не должны возвращать значения (тип возврата - void). Метод Configure тоже не должен возвращать значение, но его список параметров задан не жестко: он должен принимать, по крайней мере, один параметр - интерфейс создания конвейера Midleware IApplicationBuilder, но кроме этого параметра метод Configure может принимать и дополнительные параметры (как правило - интерфейсных типов): значения этих параметров будут получены перед вызовом метода Configure из контейнера сервисов и переданы как аргументы в метод Configure.

Лирическое отступление: это модно

Да-да, это - то самое, модное-стильное-молодежное внедрение зависимостей (в данном случае - в метод), которое так любят нынешние теоретики от программирования. А подробности, через какое место эти самые зависимости внедряются на практике, теоретиков обычно не интересуют. Но так как этот текст - не плод труда теоретика, то в нем эти подробности - как внедряются зависимости в метод Configure - рассмотрены будут обязательно.

Поддержка запуска, основанного на соглашениях для Startup-класса. Класс StartupLoader

Однако прежде чем перейти непосредственно к рассмотрению работы внутреннего метода UseStartup класса GenericWebHostBuilder, имеет смысл подробнее рассмотреть те вспомогательные методы и классы, которые он использует: эти методы и классы устроены достаточно сложно, а логика их работы во многом не очевидна.

Для реализации запуска, основанного на соглашениях (Convention Based Startup), предназначены методы внутреннего статического класса StartupLoader (этот класс используется в обоих шаблонах - и Web Host, и Generic Host) С одним из методов этого класса - FindStartupType, отыскивающим в сборке Startup-класс в соответствии с соглашениями об именах - мы уже встречались, когда рассматривали как устроено конфигурирование Startup-класса, указанного через имя содержащей его сборки (это было в "деталях реализации"). В данном разделе нам понадобится рассмотреть другие методы этого класса.

Лирическое отступление: но кое-что удастся пропустить

Но с самыми большими и интересными частями этого класса - основным методом LoadMethods и внутренним вложенным классом ConfigureServicesDelegateBuilder, реализующим основную часть функциональности этого метода - нам, к счастью, иметь дела не придется: это - части кода, предназначенные исключительно для шаблона Web Host. На долю Generic Host остаются только небольшие вспомогательные методы.

Для поиска методов Startup-класса внутренний метод UseStartup использует три метода класса StartupLoader - по одному для каждого из методов Startup-класса. Названия этих методов: FindConfigureServicesDelegate - для ConfigureServices, FindConfigureContainerDelegate - для ConfigureContainer и FindConfigureDelegate - для Configure. Все эти методы возвращают объект соответствующего вспомогательного класса получения делегата, через который вызывается найденный метод Startup-класса. Для каждого из методов Starup-класса существует свой вспомогательный класс получения делегата. Эти классы подробно описаны ниже. Все эти классы имеют метод Build для создания делегата вызова соответсвующего метода Startup-класса, для этого в метод Build нужно передать параметр - экземпляр Startup-класса, для которого будет вызываться метод. Кроме того, класс StartupLoader содержит еще один метод с длиннющим именем HasConfigureServicesIServiceProviderDelegate, который тоже вызывается из метода UpdateServices класса GenericWebHostBuilder. Этот метод возвращает true, если метод ConfigureServices Startup-класса возвращает интерфейс IServiceProvider (то есть, самостоятельно создает контейнер сервисов). Он используется для контроля пригодности Startup-класса для шаблона Generic Host (подробности см. ниже).

детали реализации: поиск методов Startup-класса

Все упомянутые методы основаны на внутреннем методе FindMethod класса StartupLoader, который ищет метод с подходящим именем. Этот метод возвращает MethodInfo для найденного метода Startup-класса или null, если метод не найден и при этом он не является требуемым (см. ниже). Если найдено более одного метода с одинаковым именем или не найден требуемый метод, то выбрасывается исключение InvalidOperationException. Параметры метода FindMethod - следующие. Первый - тип Startup-класса, второй - строка-шаблон имени (для подстановки имени окружения с помощью метода String.Format, т.е. содержащая строку "{0}" в месте подстановки), третий - имя окружения, четвертый - тип результата (null - не проверяется), пятый - является ли метод требуемым. Реализованы методы поиска методов Startup-класса следующим образом. Все они в качестве двух аргументов передают в FindMethod свои параметры (у всех этих методов параметры одинаковые): в качестве первого аргумента (тип Startup-класса) - первый параметр, третьего аргумента (имя окружения) - второй параметр. Метод FindConfigureServicesDelegate класса StartupLoader вызывает FindMethod один или два раза, оба раза - со следующими аргументами: шаблон имени (второй аргумент) - "Configure{0}Services", флаг требуемого метода (пятый аргумент) - false. Первый раз FindMethod вызывается с типом возврата (четвертый аргумент) IServiceProvider (т.е., ищется метод ConfigureServices Startup-класса, который создает и возвращает контейнер сервисов). Если первый вызов FindMethod возвращает null (нет такого метода в Startup-классе), то производится второй вызов FindMethod, без проверки типа возврата (четвертый аргумент равен null). И, наконец, FindConfigureServicesDelegate создает и возвращает объект вспомогательного класса ConfigureServicesBuilder, в конструктор которого передается результат последнего вызова FindMethod. Метод FindConfigureContainerDelegate класса StartupLoader вызывает FindMethod со следующими аргументами: шаблон имени (второй аргумент) - "Configure{0}Container", четвертый аргумент - null(без проверки типа возврата), флаг требуемого метода (пятый аргумент) - false. Затем FindConfigureContainerDelegate создает и возвращает объект вспомогательного класса ConfigureContainerBuilder, в конструктор которого передается результат последнего вызова FindMethod. Метод FindConfigureDelegate класса StartupLoader вызывает FindMethod со следующими аргументами: шаблон имени (второй аргумент) - "Configure{0}", четвертый аргумент - null(без проверки типа возврата), флаг требуемого метода (пятый аргумент) - true (метод Configure обязан существовать в Startup-классе. Затем FindConfigureDelegate создает и возвращает объект вспомогательного класса ConfigureBuilder, в конструктор которого передается результат последнего вызова FindMethod. В методе HasConfigureServicesIServiceProviderDelegate класса StartupLoader делается вызов FindMethod с теми же параметрами, что и первый вызов в FindConfigureServicesDelegate. Возвращаемое значение - неравенство результата FindMethod значению null.

Вспомогательные классы для получения делегатов вызова методов Startup-класса

Вспомогательные классы получения делегатов для методов Startup-класса позволяют путем вызова их метода Build создать делегат (возвращается как результат вызова Build), который вызывает найденный в Startup-классе метод конкретного типа (зависящего от класса). Сигнатура возвращаемого делегата зависит от класса. У метода Build всех этих классов есть единственный параметр - ссылка на экземпляр Startup-класса, для вызова которого будет создан делегат. Этих вспомогательных классов, так же как и типов методов Startup-класса, - три: ConfigureServicesBuilder - для метода типа ConfigureServices, ConfigureContainerBuilder - для ConfigureContainer и ConfigureBuilder - для Configure. Все эти классы внешне устроены примерно одинаково. Помимо описанного выше метода Build, они имеют конструктор, принимающий значение MethodInfo вызываемого метода (оно сохраняется в общедоступном свойстве только для чтения MethodInfo класса). Дополнительно класс ConfigureContainerBuilder имеет метод GetContainerType без параметров, возвращающий тип контейнера-построителя, которым специализирован тот метод ContainerBuilder, для которого этот вспомогательный класс создан.

детали реализации: вспомогательные классы получения делегатов вызова методов Startup-класса

Метод Build в ConfigureServicesBuilder формально возвращает делегат типа Func<IServiceCollection, IServiceProvider> - так как этот вспомогательный класс используется и в шаблоне Web Host, то он возвращает делегат, формально совместимый с требованиями для этого шаблона. В коде шаблона Generic Host (внутренний метод) возвращаемое этим делегатом значение всегда будет null (т.к. метод сам метод ConfigureServices требуемый для шаблона Generic Host, не должен возвращать значение), и данный делегат фактически используется как Action. Метод Build в ConfigureContainerBuilder возвращает делегат типа Action&lt;object>: ConfigureContainerBuilder является необобщенным классом, у него нет параметра-типа для типа контейнера-построителя, поэтому он работает с контейнером-построителем как с универсальным типом Object. Вся необходимая работа с конкретным типом выполняется кодом внутреннего метода UseStartup (забегая вперед: там используется весьма интересный прием для вызова специализации типом контейнера-построителя, неизвестным во время компиляции, обобщенного метода ConfigureContainer построителя IHostBuilder.) Метод Build в ConfigureBuilder возвращает делегат типа Action&lt;IApplicationBuilder>, с одним параметром, хотя сам метод Configure Startup-класса может иметь дополнительные параметры (их значения в таком случае будут получены(внедрены) из контейнера сервисов).

Делегаты, которые возвращают все перечисленные вспомогательные классы, также устроены единообразно: они вызывают внутренний метод Invoke соответствующего класса, принимающий два параметра - ссылку на Startup-класс и специфический для вспомогательного класса параметр, - с фиксированным первым аргументом - переданной как параметр в метод Build ссылкой на Startup-класс (она хранится в замыкании этого делегата). Второй передаваемый в Invoke аргумент - это параметр (единственный) возвращаемого делегата: его тип - IServiceCollection для класса ConfigureServicesBuilder, Object - для ConfigureContainerBuilder и IApplicationBuilder - для ConfigureBuilder.

Методы Invoke классов ConfigureServicesBuilder и ConfigureContainerBuilder устроены похожим образом, поэтому они будут рассмотрены вместе. Прежде всего, для полноты имеет смысл упомянуть об ранее не упомянутых свойствах этих классов, которые не используются в шаблоне Generic Host, но используются в Web Host. Это - общедоступные для чтения и записи свойства-фильтры, их имена - StartupServiceFilters в ConfigureServicesBuilder и ConfigureContainerFilters в ConfigureContainerBuilder соответственно. Вообще-то все, что касается этих свойств, как неиспользуемых, можно при чтении пропустить. Но для полноты картины я их тут все же опишу - потому что они устроены весьма небезынтересно. Эти свойства содержат делегаты-фильтры - функции с одним параметром, причем типом и параметра, и результата делегата-фильтра является тот же тип делегата, что и тип делегата, возвращаемого методом Build соответствующего класса. Эти делегаты-фильтры предназначены для преобразования переданного им в качестве параметра входного делегата в выходной делегат с той же сигнатурой: они возвращают в виде результата выходной делегат, который (обычно вызывает (но может и не вызывать) переданный им в качестве параметра входной делегат, но при этом он может как изменять аргумент входного делегата перед его вызовом, так и результат, возвращаемый входным делегатом после его вызова. С помощью этих свойств-фильтров в шаблоне Web Host реализуется функциональность фильтров конфигурирования процесса создания контейнера сервисов с интерфейсами IStartupConfigureServicesFilter и IStartupConfigureContainerFilter<>. Но, как уже упоминалось, в шаблоне Generic Host эти свойства-фильтры не используются: их значения по умолчанию - делегаты-фильтры, которые не делают ничего, а просто возвращают переданный им параметр-делегат - так и остаются неизменными. Однако код методов Invoke этих классов, поскольку он - общий для обоих шаблонов - Web Host и Generic Host, вызов делегатов-фильтров, содержащихся в этих свойствах, содержит, поэтому они и были упомянуты. Конкретно, метод Invoke этих классов возвращает значение, получаемое от результата применения делегата-фильтра из свойства (которое в Generic Host, как уже говорилось, содержит делегат, который просто возвращает то, что ему передали на вход) к внутренней функции, определенной внутри метода Invoke (ее имя - Startup в классе ConfigureServicesBuilder или StartupConfigureContainer в классе ConfigureContainerBuilder). А проще говоря, в шаблоне Generic Host просто вызывается эта самая определенная в методе Invoke внутренняя функция. Эта внутренняя функция имеет ту же сигнатуру, что и делегат, возвращаемый методом Build. Она принимает один специфический для вспомогательного класса параметр (IServiceCollection в классе ConfigureServicesBuilder или Object в классе ConfigureContainerBuilder), после чего вызывает внутренний метод InvokeCore своего класса, передавая в него в качестве аргументов ссылку на экземпляр Startup-класса (переданный как параметр в метод Invoke) и свой единственный параметр. Внутренняя функция класса в ConfigureServicesBuilder, в соответствии со своей сигнатурой, также возвращает полученное из InvokeCore значение IServiceProvider (в шаблоне Generic Host оно всегда null и не используется). Метод InvokeCore принимает два параметра - ссылку на Startup-класс и специфический для вспомогательного класса параметр, упомянутый выше. Он подготавливает список аргументов для вызова: массив из одного элемента - своего второго параметра. А затем он вызывает соответствующий метод Startup-класса с помощью MetodInfo.Invoke для экземпляра Startup-класса, полученного как первый параметр, с этим списком аргументов, причем при вызове указывается флаг BindingFlags.DoNotWrapExceptions, предотвращающий оборачивание исключения при вызове, если оно будет выброшено, в исключение TargetInvocationException. Метод InvokeCore класса ConfigureServicesBuilder возвращает значение, полученное из MetodInfo.Invoke, если вызываемый таким образом метод не возвращает значения - что требуется в шаблоне Generic Host - то возвращаемое значение будет null.

Метод Invoke класса ConfigureBuilder устроен по-другому. Во-первых, в этом классе нет свойства-фильтра, а потому вызов метода Configure Startup-класса производится непосредственно из метода Invoke. Во-вторых, метод Invoke получает из информации отражения список параметров метода Configure (он, напомним, может иметь дополнительные параметры), и для всех параметров, кроме имеющего тип IApplicationBuilder, извлекает в качестве значений аргументов для них сервисы того же типа из контейнера сервисов (доступного через свойство ApplicationServices интерфейса IApplicationBuilder). Эти значения затем подставляются в качестве значений соответствующих аргументов для вызова Configure. Тем самым метод Invoke производит то, что у теоретиков называется "внедрением зависимостей". Для аргумента типа IApplicationBuilder подставляется то значение, которое передается в метод Invoke в качестве второго параметра. И затем метод Configure вызывается с помощью MetodInfo.Invoke для экземпляра Startup-класса, полученного как первый параметр, с этим списком аргументов, причем при вызове так же, как и в двух остальных вспомогательных классах, указывается флаг BindingFlags.DoNotWrapExceptions. Если говорить более точно, то в качестве источника аргументов используется не сам этот контейнер сервисов (корневой), а контейнер сервисов ограниченной области, созданный его на основе: эта ограниченная область охватывает получение аргументов и вызов метода Configure Startup-класса и затем удаляется перед завершением делегата. Применение ограниченной области позволяет использовать для вызова метода Configure сервисы со временем жизни ограниченной области (Scoped).

Работа внутреннего метода UseStartup

И вот теперь, вооружившись знаниями о вспомогательных классах и методах, можно перейти непосредственно к рассмотрению того, как работает внутренний метод UseStartup класса GenericWebHostBuilder. Выполняется этот метод, напомним, на стадии конфигурирования списка регистраций сервисов. Схема работы этого метода изображена на рис.4:

Рис.4. Схема работы внутреннего метода UseStatup класса GenericWebHostBuilder.
Рис.4. Схема работы внутреннего метода UseStatup класса GenericWebHostBuilder.
Дополнительные условные обозначения для рис.4

Поскольку работа метода UseStartup активно связана с метаданными и отражеением, потребовалось новое обозначение - для метаданных. Сами метаданные обозначены прямоугольниками с пунктирными границами: обычными прямоугольниками - метаданные методов, прямоугольником со скургленными углами - метаданные класса. А использование метаданных изображено пунктирными соединительными линиями.

Самое первое, что делает этот метод (на рис.4 он изображен внутри своего класса GenericWebHostBuilder, в его правом нижнем углу) - это получает (блок кода 1 на рис.4) контекст построителя веб-приложения WebHostBuilderContext и типизированные параметры веб-приложения - объект типа WebHostOptions.

детали реализации: UseStarup - начальное получеие контекста

Делает он это точно так же, как и третий делегат конструктора: вызывает вспомогательный метод GetWebHostBuilderContext с аргументом - контекстом построения HostBuilderContext, полученным в качестве второго параметра внутреннего метода UseStartup. Вызванный метод возвращает WebHostBuilderContext. Затем внутренний метод UseStartup извлекает объект WebHostOptions из словаря Properties контекста построения HostBuilderContext, который был помещен туда тем же самым методом GetWebHostBuilderContext при его самом первом вызове.

Дальнейшие действия внутренний метод UseStartup выполняет в блоке try, чтобы перехватить возможные исключения при их выполнении. В этом блоке он сначала проверяет (на рис.4 это не показано), что метод ConfigureServices Startup-класса может быть использован для конфигурирования контейнера сервисов в рамках шаблона Generic Host - что он не создает контейнер сервисов самостоятельно, т.е. не возвращает интерфейс IConfigureServices созданного контейнера сервисов.

детали реализации: проверка Startup-класса на правильную реализацию метода ConfigureServices

При этом производятся две проверки: что Startup-класс не реализует интерфейс IStartup (в котором сигнатура метода ConfigureServices указывает на то, что он возвращает IServiceProvider) и, если не реализует (т.е. используется запуск, основанный на соглашениях) - что не возвращает значение IServiceProvider (созданный методом ConfigureServices . Если это условие нарушается, то выбрасывается исключение NotSupportedException. Проверка на то, что метод ConfigureServices не возвращает IServiceProvider при использовании запуска, основанного на соглашениях, производится с помощью метода StartupLoader.HasConfigureServicesIServiceProviderDelegate.

После этого внутренний метод UseStartup создает (блок кода 2 на рис.4) экземпляр Startup-класса, который будет использоваться для конфигурирования. Конструктор Startup-класса, используемого в рамках шаблона Generic Host может содержать параметры-интерфейсы сервисов, но, в отличие от шаблона Web Host, возможный набор этих сервисов крайне ограничен и не может быть расширен разработчиком приложения. В число этих интерфейсов входят: IConfiguration, IWebHostEnvironment, IHostEnvironment и еще два устаревших интерфейса Это ограничение связано с тем, что в шаблоне Generic Host, в отличие от Web Host не создается отдельный контейнер сервисов построителя до создания Startup-класса, поэтому для создания Startup-класса используется суррогатный контейнер сервисов (о нем - см. ниже детали реализации). Созданный экземпляр Startup-класса запоминается в словаре контекста построения HostBuilderContext (на рис.4 не показан).

детали реализации: суррогатный контейнер сервисов и его использование для создания экземпляра Startup-класса

Упомянутые два устаревших интерфейса, доступных в суррогатном котейнере - это Microsoft.Extensions.Hosting.IHostingEnvironment и Microsoft.AspNetCore.Hosting.IHostingEnvironment. Реализацией суррогатного контейнера сервисов IServiceProvider является вложенный класс HostServiceProvider, определенный внутри GenericWebHostBuilder. Эта реализация основана на объекте контекста построителя веб-приложения WebHostBuilderContext, который передается в конструктор этого класса: сервис IConfiguration реализуется объектом, на которое ссылается поле Configuration контекста построителя веб-приложения (а на данной стадии оно ссылается уже на полную конфигурацию приложения), остальные реализованные сервисы - объектом, на который ссылается поле HostingEnvironment контекста построителя веб-приложения. Создание экземпляра Startup-класса производится методом ActivatorUtilities.CreateInstance. В этот метод в качестве аргумента - контейнера сервисов передается ссылка на вновь создаваемый экземпляр класса суррогатного контейнера HostServiceProvider, конструктору которого в качестве аргумента передается полученный ранее контекст построителя веб-приложения WebHostBuilderContext. Созданный экземпляр Startup-класса сохраняется в локальной переменной instance, а в словаре контекста построения HostBuilderContext он запоминается под ключом, равным значению поля _startupKey (это поле инициализуется новым объектом Object при инициализации экземпляра GenericWebHostBuilder).

Лирическое отступление (по поводу деталей реализации): интересно, а зачем так сложно?

Почему вместо ключа _startupKey в поле (изменив его тип) объекта GenericWebHostBuilder нельзя было поместить сам экземпляр Startup-класса - для меня остается загадкой: все равно ведь для доступа к этому экземпляру требуется значение какого-то поля того же самого экземпляра GenericWebHostBuilder. Единственное разумное предположение - чтобы развязать времена жизни классов GenericWebHostBuilder и Startup-класса: т.к. ссылка на последний хранится в контексте построения HostBuilderContext, существующий до конца времени жизни приложения (точнее, контейнера сервисов, как сервис с постоянным (Singleton) временем жизни, то GenericWebHostBuilder можно безбоязненно после инициализации собрать как мусор. Правда, зачем нужно сохранять Startup-класс и почему, если нужно, это нельзя было сделать ссылкой из другого места - это мне так и осталось непонятным.

Затем внутренний метод UseStartup создает (блок кода 3 на рис.4) делегат для вызова метода ConfigureServices Startup-класса (на рис.4 - вложенный в блок 3 блок кода) и вызывает его: тем самым вызывается метод ConfigureServices Startup-класса (если он есть, при отсутствии этого метода делегат просто сразу возвращает управление). Метод ConfigureServices Startup-класса добавляет описатели сервисов в получаемый им как параметр список регистрации сервисов (изображен на рис.4 как элемент данных services в методе HostBuilder.CreateServiceProvider), на основе которого будет построен контейнер сервисов.

детали реализации: вызов метода ConfigureServices Startup-класса

Делегат вызывается с аргументом - ссылкой на список регистрации сервисов IServiceCollection, являющейся третьим параметром внутреннего метода UseStartup. Для создания делегата для вызова метода ConfigureServices внутренний метод UseStartup сначала получает с помощью метода StartupLoader.FindConfigureServicesDelegate (передавая в него в качестве аргументов тип Startup-класса и название текущего окружения - Development, Production и т.д.) экземпляр вспомогательного класса получения делегата ConfigureServicesBuilder. Этот и последущие создаваемые экземпляры вспомогательных классов (все они являются локальными переменными метода) изображены на рис.4 внутри штриховых рамок, подписанных именами локальных переменных во внутреннем методе UseStartup. Затем требуемый делегат создается методом Build вспомогательного класса, в который в качестве аргумента передается созданный ранее экземпляр Startup-класса.

Следующее действие, которое выполняет внутренний метод UseStartup - обеспечение выполнения метода конфигурирования контейнера-построителя ConfigureContainer<> Startup-класса (блок кода 4 на рис.4). На этапе, на котором выполняется внутренний метод UseStartup, запускать метод ConfigureContainer еще рано, его вызов (через делегат) надо поместить в очередь следующей стадии - стадии конфигурирования контейнера-построителя. Чтобы это сделать, внутренний метод UseStartup сначала получает экземпляр вспомогательного класса ConfigureContainerBuilder для получения делегата вызова метода ConfigureContainer. Затем проверяется, что этот метод вообще реализован в Startup-классе.

Если этот метод в Startup-классе реализован, то внутренний метод UseStartup класса GenericWebHostBuilder создает делегат обратного вызова (так он назван в комментариях в коде). Этот делегат обратного вызова будет вызывать метод ConfigureContainer Startup-класса на стадии конфигурирования контейнера-построителя с помощью полученного ранее экземпляра вспомогательного класса ConfigureContainerBuilder.

Затем внутренний метод UseStartup помещает этот делегат обратного вызова в очередь конфигурирования контейнера-построителя родительского построителя IHostBuilder. Для этого вызывается (довольно хитрым способом - через отражение) метод ConfigureContainer<> родительского построителя. Хитрый способ нужен, потому что метод ConfigureContainer<> - обобщенный, но его параметр-тип - тип контейнера-построителя - на момент компиляции неизвестен.

детали реализации: помещение делегата вызова ConfigureContainer в очередь конфигурирования контейнера-построителя

Экземпляр вспомогательного класса получения делегата создается с помощью метода StartupLoader.FindConfigureContainerDelegate. Проверка, что метод ConfigureContainer реализован в Startup-классе, производится путем сравнения свойства MethodInfo вспомогательного класса с null.

Процесс помещения делегата обратного вызова в очередь конфигурирования контейнера-построителя в деталях выглядит следующим образом. Сначала методом GetContainerType полученного ранее экземпляра вспомогательного класса ConfigureContainerBuilder определяется тип контейнера-построителя.

Затем ссылка на этот экземпляр вспомогательного класса запоминается в словаре Properties построителя под ключом - типом класса ConfigureContainerBuilder для последующего использования в делегате обратного вызова, который будет передан в метод ConfigureContainer<> построителя IHostBuilder.

Базой для создания упомянутого делегата обратного вызова, который будет выполняться на этапе конфигурирования контейнера-построителя, служит обобщенный метод-заготовка GenericWebHostBuilder.ConfigureContainer<> (на рис.4 он изображен в упрощенном виде над внутренним методом UseStartup) Этот обобщенный метод-заготовка имеет два параметра: контекст построения HostBuilderContext и ссылку на контейнер-построитель (ее тип - это параметр-тип обобщенного метода). Делает этот метод-заготовка следующее. Сначала из словаря Properties контекста построения - первого параметра метода (напомним, что этот словарь - общий для построителя и для контекста построителя) извлекаются ссылки: на экземпляр вспомогательного класса получения делегата ConfigureContainerBuilder (она была сохранена под ключом - типом класса ConfigureContainerBuilder) и на ранее созданный экземпляр Startup-класса (под ключом, хранящемся во внутреннем поле _startupKey). Затем вызовом метода Build, в который в качестве аргумента передается извлеченный экземпляр Startup-класса, для извлеченного экземпляра вспомогательного класса ConfigureContainerBuilder создается делегат вызова метода ConfigureContainer для извлеченного экземпляра StartUp-класса . И, наконец, созданный делегат вызова метода ConfigureContainer вызывается внутри метода-заготовки делегата обратного вызова.

После сохранения ссылки на экземпляр вспомогательного класса создается делегат обратного вызова и реализуется тот самый "хитрый способ" (с использованием отражения) вызова обобщенного метода IHostBuilder.ConfigureContainer<> построителя с установкой параметров-типов делегата и обобщенного типа равными типу контейнера-построителя во время выполнения. Делегат обратного вызова создается путем специализации обобщенного метода-заготовки GenericWebHostBuilder.ConfigureContainer<> типом контейнера-построителя. Для этого сначала создается (и сохраняется в переменной actionType) тип параметра для вызываемой специализации вызываемого обобщенного метода построителя IHostBuilder.ConfigureContainer<>, он же - тип для делегата обратного вызова (который и будет параметром). Это делается путем специализации обобщенного типа Action<,> при помощи вызова его метода MakeGenericType двумя параметрами-типами: контекстом построения HostBuilderContext и конкретным типом контейнера-построителя, полученным ранее из вспомогательного класса ConfigureContainerBuilder. Затем вызовом метода GetType() находится тип класса (т.е. GenericWebHostBuilder), содержащего выполняющийся в данный момент метод, в этом типе с помощью отражения (вызовом GetMethods() с фильтрацией возвращенных результатов по имени метода) находится ссылка на описатель его обобщенного метода ConfigureContainer<>, потом создается (вызовом MakeGenericType для найденного описателя) специализация этого метода типом контейнера-построителя и, наконец, создается (вызовом CreateDelegate) делегат с ранее созданным типом actionType для этой специализации и текущего (this) экземпляра. Это и будет тот самый делегат обратного вызова. Теперь, при наличии нужного в качестве аргумента делегата, можно вызывать обобщенный метод построителя IHostBuilder.ConfigureContainer<>, специализированный типом контейнера-построителя. Чтобы произвести этот вызов, для типа построителя (IHostBuilder) через отражение (вызовом GetMethods() с фильтрацией возвращенных результатов по имени метода) находится ссылка на описатель вызываемого обобщенного метода ConfigureContainer<> построителя, потом создается (вызовом MakeGenericType для найденного описателя) специализация этого метода типом контейнера-построителя, и, наконец, производится вызов этой специализации ее методом Invoke с указанием делегата обратного вызова как единственного аргумента и с флагом BindingFlags.DoNotWrapExceptions, предотвращающим оборачивание исключения при вызове, если оно будет выброшено, в исключение TargetInvocationException.

Лирическое отступление по поводу предыдущих деталей реализации: интересный прием

Этот прием получения специализации обобщенных методов и классов во время выполнения я раньше не знал. Теперь буду знать - вдруг пригодится.

Последнее, что делает внутренний метод UseStartup перед завершением блока try - это получение экземпляра вспомогательного класса ConfigureBuilder, используемого для создания делегата вызова метода Configure Startup-класса.

детали реализации: получение экземпляра вспомогательного класса ConfigureBuilder

Получение экземпляра вспомогательного класса ConfigureBuilder производится методом StartupLoader.FindConfigureDelegate. Полученный экземпляр сохраняется в локальной переменной configureBuilder.

После этого блок try завершается. Связанный с ним блок catch перехватывает все исключения, но только - при условии, что в типизированных параметрах веб-приложения WebHostOptions установлен флаг CaptureStartupErrors. В этом случае производится захват исключения в локальную переменную startupError.

детали реализации: перехват возможных исключений

Эта переменная имеет тип ExceptionDispatchInfo - чтобы это исключение можно было выбросить позднее, возможно - в другом потоке (это произойдет уже в методе StartAsync класса веб-приложения GenericWebHostedService, подробности см. ниже).

Последнее действие, выполняемое внутренним методом UseStartup, уже вне блока try - планирование запуска делегата-постобработчика в методе StartAsync класса веб-приложения GenericWebHostedService, когда он будет запущен для выполнения методом StartAsync интерфейса IHost (блок кода 5 на рис.4). Делегат-постобработчик может сделать одно из двух действий: либо выбросить повторно исключение, которое возникло во внутреннем методе UseStartup до момента планирования запуска делегата-постобработчика (см. выше описание блоков try и catch, на рис.4 этот путь выполнения не показан), либо, если исключения не было - запустить на выполнение метод Configure Startup-класса, который выполняет конфигурирование конвейера Middleware веб-приложения.

детали реализации:планирование запуска делегата-постобработчика

Тип делегата-постобработчика - Action<IApplicationBuilder>. Делегат-постобработчик, прежде всего, проверяет, есть ли сохраненная информация об исключении в доступной через замыкание локальной переменной startupError (т.е., что эта переменная - не null). Если информация об исключении есть, то это исключение выбрасывается повторно методом startupError.Throw, но уже в новом контексте. В противном случае делегат-постобработчик создает делегат для вызова метода Configure Startup-класса и вызывает его. Делегат для вызова метода Configure Startup-класса создается с помощью метода Build экземпляра вспомогательного класса ConfigureBuilder, хранящегося в доступной через замыкание локальной переменной configureBuilder. В качестве аргумента в метод Build передается ранее созданный экземпляр Startup-класса, находящийся в доступной через замыкание локальной переменной instance. Делегат-постобработчик, как уже описывалось выше, передается в метод GenericWebHostedService.StartAsync через механизм параметров(Option) как значение (неименованное) типа GenericWebHostServiceOptions: с помощью метода расширения Configure<GenericWebHostServiceOptions>, вызываемого для параметра метода UseStartup, который имеет тип ICollectionServices, делегат-постобработчик записывается (делегатом, выполняющим конфигурирование значения) в свойство постобработчика стартового кода ConfigureApplication этого значения. На рис.4 упомянутый элемент конфигурирования параметра типа GenericWebHostServiceOptions (вместе с делегатом в свойстве ConfigureApplication) обозначен прямоугольником с одним срезанным углом. Содержимое делегата изображено в выноске "Delegate in a ConfigureApplication propery" справа. Т.к. элемент конфигурирования параметра в реальности действует путем регистрирации спецального сервиса для помещения в контейнер сервисов (подробности см. ниже при описании передачи значения параметра типа GenericWebHostedService), то на рис.4 отображено, что он помещает запись в список регистрации сервисов, т.е. выполняет действие, похожее на действие метода ConfigureServices Startup-класса.

Создание конвейера Middleware, вызов метода Configure Startup-класса

Последняя тема, которую планируется рассмотреть в этой статье - выполнение метода Configure Startup-класса (или выполняющего ту же роль метода расширения Configure интерфейса построителя веб-приложения IWebHostBuilder, далее оба они будут называться просто метод Configure) и ещё ряд связанных с этим операций. Метод Configure принимает участие в процессе создания конвейера обработки запросов протокола HTTP, часто называемом также конвейером Middleware.

Лирическое отступление: "Шишков, прости: не знаю, как перевести"

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

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

Делегат-постобработчик, в задачу которого входит запуск метода Configure, передается, вместе с другой информацией для этапа запуска веб-сервиса GenericWebHostedService, через неименованное значение параметра(option) типа GenericWebHostServiceOptions, в его свойстве ConfigureApplication. Это значение параметра копируется в конструкторе класса GenericWebHostedService в одно из свойств этого класса, и, таким образом, становится доступным методу StartAsync.

детали реализации: передача значения параметра типа GenericWebHostedService

Интерфейс доступа к этому значению - IOption<GenericWebHostServiceOptions> - является одним из параметров конструктора класса веб-сервиса GenericWebHostedService. Значение этот параметр получает через механизм внедрения зависимостей при получении из контейнера сервисов реализации интерфейса IHost, который как раз реализуется классом GenericWebHostedService. Значение свойства Value этого интерфейса копируется в конструкторе этого класса в свойство Options. Значение параметра создается в момент первого к нему обращения путем вызова всех делегатов, устанавливающих это значение, в том порядке, в котором они были зарегистрированы. Эти делегаты (их может быть несколько) были ранее переданы для регистрации в метод расширения Configure интерфейса списка регистрации сервисов IServiceCollection. Делегат, устанавливающий свойства WebHostOptions и HostingStartupExceptions значения регистрируется только один раз, третьим делегатом из конструктора класса GenericWebHostBuilder, который конструктор помещает в очередь конфигурирования списка регистрации сервисов. А вот делегаты, устанавливающие свойство ConfigureApplication (делегат-постобработчик) могут регистрироваться несколько раз: это может сделать либо внутренний метод UseStartup построителя веб-приложения GenericWebHostBuilder, либо делегат, созданный методом Configure построителя веб-приложения (описание, как это происходит, ищите выше по тексту этой статьи). Но в результате выполнения всех делегатов установки значения параметра, свойство будет содержать только один, последний зарегистрированный, делегат-постобработчик - который и будет использован далее. Таким образом, разрешается возможный конфликт между методом Configure Startup-класса и делегатом, выполняющим ту же функцию, переданным через метод расширения Configure интерфейса IWebHostBuilder.

Кроме метода Configure, в создании конвейера Middleware принимают участие фильтры запуска: объекты, реализующие интерфейс IStartupFilter, зарегистрированные в контейнере сервисов. Последовательность этих фильтров (IEnumerable<IStartupFilter>) помещается конструктором класса веб-сервиса GenericWebHostedService в его свойство StartupFilters.

детали реализации: часть конструктора GenericWebHostedService, связанная с обсуждаемой темой

Конструктор этого класса имеет в качестве параметра последовательность фильтров запуска IEnumerable<IStartupFilter>, поэтому при создании экземпляра этого класса как реализации IHostedService, контейнер сервисов создает все зарегистрированные реализации интерфейса IStartupFilter и передает их в качестве аргумента для соответствующего параметра, а конструктор копирует их в свойство StartupFilters. Еще два параметра конструктора, которые получаются из контейнера сервисов через механизм внедрения зависимостей, и которые используются в описываемом процессе создания конвейера Middleware, - это ссылка на интерфейс фабрики построителя веб-приложения IApplicationBuilderFactory (копируется в свойство ApplicationBuilderFactory) и на интерфейс веб-сервера IServer (копируется в Server) Server

Интерфейс IStartupFilter содержит единственный метод Configure, который добавляет в цепочку создания конвейера делегат конфигурирования конвейера Middleware данного фильтра. У этого метода есть единственный параметр, который принимает ссылку на текущую цепочку создания, и возвращает ссылку новую на цепочку, которая состоит теперь из делегата конфигурирования конвейера, добавленного фильтром, в голове цепочки и следующей за ним прежней цепочки. У делегата конфигурирования конвейера также есть лишь один параметр - интерфейс создания конвейера Middleware IApplicationBuilder - и этот делегат не возвращает никакого значения. При вызове делегата конфигурирования конвейера он конфигурирует свой элемент Middleware, после чего обязан он вызвать следующий делегат в цепочке.

Метод StartAsync класса GenericWebHostedService выполняет следующие действия, касающиеся предмета этой статьи. Сначала он производит создание конвейера обработки запросов (конвейера Middleware). Процесс создания конвейера Middleware выполняется внутри блока try. Сначала метод StartAsync создает цепочку создания конвейера Middleware. Сначала в цепочке идут делегаты конфигурирования из фильтров запуска IStartupFilter, причем - в порядке, обратном порядку регистрации фильтров, а последним элементом в цепочке является делегат-постобработчик, обычно вызывающий метод Configure (который, как было написано выше или определен в Startup-классе, или является делегатом, переданным в метод расширения Configure интерфейса IWebHostBuilder). Затем метод StartAsync получает объект построителя веб-приложения - экземпляр типа, реализующего интерфейс IApplicationBuilder, интерфейс создания конвейера Middleware. После этого метод StartAsync вызывает цепочку создания конвейера Middleware (т.е. ее головной элемент) с аргументом - полученной реализацией IApplicationBuilder. Каждый элемент цепочки, начиная с головного, производит свой этап конфигурирования и вызывает следующий элемент, пока не будет вызван последний. И, наконец, методом Build объекта, реализующего интерфейс IApplicationBuilder, создается объект-конвейер обработки запросов (или конвейер Middleware), имеющий тип RequestDelegate, причем компоненты Middleware в конвейере будут расположены в том порядке, в котором были расположены делегаты конфигурирования в цепочке - от добавленного самого последним зарегистрированным фильтром, до добавленных в методе Configure.

детали реализации: действия метода StartAsync, связанные с обсуждаемой темой

В качестве исходного элемента цепочки используется (локальная переменная configure) тот самый делегат-постобработчик из свойства ConfigureApplication значения параметра типа GenericWebHostServiceOptions (оно, напоминаем, было скопировано в свойство Options текущего класса). После этого вызывается, как описано выше, метод Configure каждого из членов последовательности объектов, реализующих IStartupFilter (из свойства StartupFilters), взятых в обратном порядке. В этот метод в качестве аргумента передается ссылка на предыдущий головной элемент цепочки из переменной configure, а результат вызова записывается обратно в эту же переменную. Таким образом, после выполнения этого процесса переменная configure содержит ссылку на цепочку делегатов конфигурирования конвейера, выстроенных в нужном порядке.

Объект построителя веб-приложения получается вызовом метода CreateBuilder свойства ApplicationBuilderFactory текущего класса, впоследствии он сохраняется в локальной переменной builder. Напоминаем, что свойство ApplicationBuilderFactory содержит ссылку на фабрику построителей веб-приложения IApplicationBuilderFactory, получающую указанным выше методом экземпляр типа, реализующего интерфейс построителя веб-приложения IApplicationBuilder. В качестве аргумента в этот метод передается значение свойства Features свойства Server текущего класса. Свойство Server, напоминаем, содержит ссылку на интерфейс IServer используемого веб-сервера (Kestrel и т.д., в зависимости от конфигурации), т.е. в качестве аргумента передается коллекция (типа IServerFeatureCollection) возможностей (features) используемого веб-сервера. Рассмотрение, что именно представляет собой эта коллекция, находится за рамками данной статьи. По факту в реализации фабрики построителей веб-приложения по умолчанию (класс ApplicationBuilderFactory в пространстве имен Microsoft.AspNetCore.Hosting.Builder) метод CreateBuilder просто создает и возвращает новый экземпляр объекта ApplicationBuilder, в конструктор которого передаются два аргумента: контейнер сервисов приложения (фабрика построителей получает к нему доступ через внедрение зависимостей в конструкторе) и полученная в качестве параметра коллекция возможностей (features) веб-сервера.

В случае возникновения исключения в вышеупомянутом блоке try оно перехватывается, и, если типизированных параметрах веб-приложения WebHostOptions (переданных через одноименное свойство параметра типа GenericWebHostServiceOptions) установлен флаг CaptureStartupErrors, то вместо сконфигурированного конвейера Middleware для обработки запросов создается суррогатный конвейер, показывающий в ответ на запросы к веб-серверу информацию об исключении (подробности о нем я опускаю). В частности, именно так обрабатывается ранее перехваченное на предыдущих этапах исключение, которое делегат-постобработчик повторно выбрасывает при наличии флага CaptureStartupErrors заново, вместо вызова метода Configure Startup-класса. Если же флаг CaptureStartupErrors не установлен, то исключение просто выбрасывается заново.

Заключение

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

Теги:
Хабы:
Всего голосов 5: ↑4 и ↓1+3
Комментарии3

Публикации

Истории

Работа

Ближайшие события

Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург