Часть 1. ConfigurationManager
Часть 2. WebApplicationBuilder
В предыдущем посте я сравнивал новый WebApplication с универсальным хостом. В этом посте я рассмотрю код, лежащий в основе WebApplicationBuilder, чтобы увидеть, как он обеспечивает более чистый, минимальный API хостинга, при этом обеспечивая ту же функциональность, что и универсальный хост.
WebApplication и WebApplicationBuilder: новый способ начальной загрузки приложений ASP.NET Core
В .NET 6 представлен совершенно новый способ «начальной загрузки» приложения ASP.NET Core. Вместо традиционного разделения между Program.cs и Startup.cs весь код находится в Program.cs и является гораздо более процедурным, чем множество лямбда-методов, которых требовал универсальный хост в предыдущих версиях:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
WebApplication app = builder.Build();
app.UseStaticFiles();
app.MapGet("/", () => "Hello World!");
app.MapRazorPages();
app.Run();
В язык C# добавлены различные обновления, которые делают всё это более чистым (операторы верхнего уровня, неявные директивы using, выведение типов в лямбдах и т. д.). Но также появились два новых типа: WebApplication и WebApplicationBuilder. В предыдущем посте я кратко описал, как использовать WebApplication и WebApplicationBuilder для настройки приложения ASP.NET Core. В этом посте мы рассмотрим их код, чтобы увидеть, как получился более простой API, при сохранении той же гибкости и настраиваемости, что и у универсального хоста.
Создание WebApplicationBuilder
Первым шагом в нашем примере программы является создание экземпляра WebApplicationBuilder с использованием статического метода класса WebApplication:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
Это создаёт новый экземпляр WebApplicationOptions, инициализирует аргументы Args из аргументов командной строки и передаёт объект параметров в конструктор WebApplicationBuilder (показанный кратко ниже):
public static WebApplicationBuilder CreateBuilder(string[] args) =>
new(new() { Args = args });
Кстати, новое сокращённое ключевое слово new, когда целевой тип неочевиден, - просто отстой при разборе кода. Невозможно даже предположить, что создаёт второй new() в приведённом выше коде. Во многом претензии те же самые, как и к повсеместному использованию var, но в данном случае это меня особенно раздражает.
WebApplicationOptions предоставляет простой способ программного переопределения некоторых важных свойств. Если они не установлены, они будут выведены в значения по умолчанию, как и в предыдущих версиях .NET.
public class WebApplicationOptions
{
public string[]? Args { get; init; }
public string? EnvironmentName { get; init; }
public string? ApplicationName { get; init; }
public string? ContentRootPath { get; init; }
}
Конструктор WebApplicationBuilder – это то место, где происходит большая часть магии, чтобы заставить работать концепцию минимального хостинга:
public sealed class WebApplicationBuilder
{
internal WebApplicationBuilder(WebApplicationOptions options)
{
// … показано ниже
}
}
Я ещё не показал тело метода, так как в этом конструкторе много чего происходит и используется много вспомогательных типов. Мы вернёмся к ним через секунду, а пока сосредоточимся на публичном API WebApplicationBuilder.
Публичный API WebApplicationBuilder
Публичный API WebApplicationBuilder состоит из набора свойств, доступных только для чтения, и одного метода Build(), который создаёт WebApplication.
public sealed class WebApplicationBuilder
{
public IWebHostEnvironment Environment { get; }
public IServiceCollection Services { get; }
public ConfigurationManager Configuration { get; }
public ILoggingBuilder Logging { get; }
public ConfigureWebHostBuilder WebHost { get; }
public ConfigureHostBuilder Host { get; }
public WebApplication Build()
}
Если вы знакомы с ASP.NET Core, вы заметите, что многие из этих свойств используют стандартные типы из предыдущих версий:
IWebHostEnvironment– используется для получения имени среды, пути к корню контента и подобных значений.IServiceCollection– используется для регистрации сервисов в контейнере DI. Обратите внимание, что это альтернатива методуConfigureServices(), используемому универсальным хостом для достижения того же результата.ConfigurationManager– используется как для добавления новой конфигурации, так и для получения значений конфигурации. См. первый пост в серии, где обсуждается этот вопрос.ILoggingBuilder– используется для регистрации дополнительных поставщиков журналов, как и в случае с методомConfigureLogging()в универсальном хосте.Свойства
WebHostиHostинтересны тем, что они предоставлены новыми типамиConfigureWebHostBuilderиConfigureHostBuilder. Эти типы реализуютIWebHostBuilderиIHostBuilderсоответственно и в основном представлены как замена для используемых ранее методов расширения.
Например, в предыдущем посте я показал, как можно зарегистрировать интеграцию Serilog с ASP.NET Core, вызвав UseSerilog() на свойстве Host:
builder.Host.UseSerilog();
Раскрытие интерфейсов IWebHostBuilder и IHostBuilder было абсолютно необходимо для обеспечения возможности перехода к новому минимальному API хостинга WebApplication, но это также оказалось проблемой. Как согласовать конфигурацию в виде лямбд/обратных вызовов в IHostBuilder с императивным стилем в WebApplicationBuilder? Вот где в игру вступают ConfigureHostBuilder и ConfigureWebHostBuilder вместе с некоторыми внутренними реализациями IHostBuilder:

Мы начнём с рассмотрения публичных ConfigureHostBuilder и ConfigureWebHostBuilder.
ConfigureHostBuilder
ConfigureHostBuilder и ConfigureWebHostBuilder были добавлены как часть обновлений для минимального хостинга. Они реализуют IHostBuilder и IWebHostBuilder соответственно. В этом посте мы подробно рассмотрим ConfigureHostBuilder:
public sealed class ConfigureHostBuilder : IHostBuilder, ISupportsConfigureWebHost
{
// ...
}
ConfigureHostBuilder реализует IHostBuilder, и похоже, что он реализует ISupportsConfigureWebHost, но взгляд на реализацию показывает, что это не так:
IHostBuilder ISupportsConfigureWebHost.ConfigureWebHost(Action<IWebHostBuilder> configure, Action<WebHostBuilderOptions> configureOptions)
{
throw new NotSupportedException($"ConfigureWebHost() не поддерживается WebApplicationBuilder.Host. Используйте вместо этого WebApplication, возвращаемый WebApplicationBuilder.Build().");
}
Это означает, что хотя следующий код и компилируется
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureWebHost(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
он выбрасывает исключение NotSupportedException во время выполнения. Это явно не идеально, но это цена, которую мы платим за наличие хорошего императивного API для настройки сервисов и т. д. То же самое верно и для метода IHostBuilder.Build() – он выбросит NotSupportedException.
Видеть исключения времени выполнения никогда не доставляет радости, но это помогает думать о ConfigureHostBuilder как об «адаптере» для существующих методов расширения (таких как метод UseSerilog()), а не как о «реальном построителе» хоста. Это становится очевидным, когда вы видите, как такие методы, как ConfigureServices() или ConfigureAppConfiguration(), реализованы для этих типов:
public sealed class ConfigureHostBuilder : IHostBuilder, ISupportsConfigureWebHost
{
private readonly ConfigurationManager _configuration;
private readonly IServiceCollection _services;
private readonly HostBuilderContext _context;
internal ConfigureHostBuilder(HostBuilderContext context, ConfigurationManager configuration, IServiceCollection services)
{
_configuration = configuration;
_services = services;
_context = context;
}
public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
{
// Выполнить делегат немедленно, чтобы контекст и конфигурация были доступны в императивном коде
configureDelegate(_context, _configuration);
return this;
}
public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
{
// Выполнить делегат немедленно, чтобы контекст и конфигурация были доступны в императивном коде configureDelegate(_context, _services);
return this;
}
}
Например, метод ConfigureServices() немедленно выполняет предоставленный делегат Action<>, используя внедрённую коллекцию IServiceCollection из WebApplicationBuilder. Таким образом, следующие два вызова функционально идентичны:
// прямая регистрация типа MyImplementation в IServiceContainer
builder.Services.AddSingleton<MyImplementation>();
// «старый» метод ConfigureServices
builder.Host.ConfigureServices((ctx, services) => services.AddSingleton<MyImplementation>());
Последний подход явно не стоит использовать в обычной практике, но существующий код, который полагается на этот метод (например, методы расширения), все ещё можно использовать.
Не все делегаты, переданные методам в ConfigureHostBuilder, запускаются немедленно. Некоторые из них, например UseServiceProviderFactory(), сохраняются в списке и выполняются позже при вызове WebApplicationBuilder.Build()
Вот примерно и всё про тип ConfigureHostBuilder. ConfigureWebHostBuilder очень похож и действует как адаптер от предыдущего API к новому императивному стилю. Теперь мы можем вернуться к конструктору WebApplicationBuilder.
Вспомогательный класс BootstrapHostBuilder
Прежде чем мы отвлеклись на ConfigureHostBuilder, мы собирались взглянуть на конструктор WebApplicationBuilder. Но мы еще не готовы к этому... сначала нужно взглянуть на ещё один вспомогательный класс, BootstrapHostBuilder.
BootstrapHostBuilder – это внутренняя реализация IHostBuilder, используемая в WebApplicationBuilder. В основном это относительно простая реализация, которая «запоминает» все полученные вызовы IHostBuilder. Например, функции ConfigureHostConfiguration() и ConfigureServices() выглядят следующим образом:
internal class BootstrapHostBuilder : IHostBuilder
{
private readonly List<Action<IConfigurationBuilder>> _configureHostActions = new();
private readonly List<Action<HostBuilderContext, IServiceCollection>> _configureServicesActions = new();
public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate)
{
_configureHostActions.Add(configureDelegate);
return this;
}
public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
{
_configureServicesActions.Add(configureDelegate);
return this;
}
// ...
}
В отличие от ConfigureHostBuilder, который немедленно выполнял переданный делегат, BootstrapHostBuilder «сохраняет» переданные делегаты в список для последующего выполнения. Это похоже на то, как работает универсальный HostBuilder. Но обратите внимание, что BootstrapHostBuilder – это ещё одна «нестроящаяся» реализация IHostBuilder, в которой вызов Build() выдаёт исключение:
public IHost Build()
{
throw new InvalidOperationException();
}
Большая часть сложности BootstrapHostBuilder заключается в его методе RunDefaultCallbacks(ConfigurationManager, HostBuilder). Он используется для применения сохранённых делегатов в правильном порядке, как мы увидим позже.
Конструктор WebApplicationBuilder
Наконец, мы дошли до конструктора WebApplicationBuilder. Он содержит много кода, поэтому я буду рассматривать его по частям.
Обратите внимание, что я позволил себе вольность удалить второстепенные фрагменты кода (например, защитные конструкции и тестовый код).
public sealed class WebApplicationBuilder
{
private readonly HostBuilder _hostBuilder = new();
private readonly BootstrapHostBuilder _bootstrapHostBuilder;
private readonly WebApplicationServiceCollection _services = new();
internal WebApplicationBuilder(WebApplicationOptions options)
{
Services = _services;
var args = options.Args;
// ...
}
public IWebHostEnvironment Environment { get; }
public IServiceCollection Services { get; }
public ConfigurationManager Configuration { get; }
public ILoggingBuilder Logging { get; }
public ConfigureWebHostBuilder WebHost { get; }
public ConfigureHostBuilder Host { get; }
}
Начнем с приватных полей и свойств. _hostBuilder – это экземпляр универсального хоста HostBuilder, который является «внутренним» хостом в WebApplicationBuilder. У нас также есть поле BootstrapHostBuilder (из предыдущего раздела) и экземпляр WebApplicationServiceCollection, являющийся реализацией IServiceCollection, которую я пока не буду рассматривать.
WebApplicationBuilder действует как «адаптер» для универсального хоста _hostBuilder, предоставляя императивный API, который я исследовал в моём предыдущем посте, сохраняя при этом ту же функциональность, что и универсальный хост.
К счастью, следующие шаги в конструкторе хорошо документированы:
// Запуск методов для ранней настройки общих параметров и значений по умолчанию для веб-хоста, чтобы заполнить конфигурацию из appsettings.json,
// переменных среды (с префиксом DOTNET_ и ASPNETCORE_) и других возможных источников по умолчанию для предварительной установки
// правильных значений по умолчанию.
_bootstrapHostBuilder = new BootstrapHostBuilder(Services, _hostBuilder.Properties);
// Не указываем здесь args, поскольку мы хотим применить их позже, чтобы они
// могли перезаписать значения по умолчанию, указанные в ConfigureWebHostDefaults
_bootstrapHostBuilder.ConfigureDefaults(args: null);
// Мы указываем командную строку здесь последней, поскольку мы пропустили её при вызове ConfigureDefaults.
// args могут содержать настройки как хоста, так и приложения, поэтому мы хотим убедиться, что
// мы соблюдаем правильный порядок провайдеров конфигурации без дублирования их
if (args is { Length: > 0 })
{
_bootstrapHostBuilder.ConfigureAppConfiguration(config =>
{
config.AddCommandLine(args);
});
}
После создания экземпляра BootstrapHostBuilder первым вызывается метод расширения HostingBuilderExtension.ConfigureDefaults(). Это тот же метод, который вызывается универсальным хостом при вызове Host.CreateDefaultBuilder().
Обратите внимание, что аргументы не передаются в вызов ConfigureDefaults(), а вместо этого применяются позже. В результате на этом этапе аргументы не используются для настройки конфигурации хоста (конфигурация хоста определяет такие значения, как имя приложения и среда хостинга).
Следующий вызов метода GenericHostBuilderExtensions.ConfigureWebHostDefaults(), который является тем же методом расширения, который мы обычно вызываем при использовании универсального хоста в ASP.NET Core 3.x/5.
_bootstrapHostBuilder.ConfigureWebHostDefaults(webHostBuilder =>
{
webHostBuilder.Configure(ConfigureApplication);
// Нам нужно перезаписать имя приложения, поскольку вызов Configure установит его как имя вызывающей сборки.
var applicationName = (Assembly.GetEntryAssembly())?.GetName()?.Name ?? string.Empty;
webHostBuilder.UseSetting(WebHostDefaults.ApplicationKey, applicationName);
});
Этот метод по сути добавляет «адаптер» IWebHostBuilder поверх BootstrapHostBuilder, вызывает на нём WebHost.ConfigureWebDefaults(), а затем немедленно запускает переданный лямбда-метод. Это регистрирует метод WebApplicationBuillder.ConfigureApplication() для последующего вызова, который устанавливает целую кучу промежуточного ПО. Мы вернёмся к этому методу в следующем посте.
После настройки веб-хоста следующий метод применяет аргументы к конфигурации хоста, гарантируя, что они правильно переопределяют значения по умолчанию, установленные предыдущими методами расширения:
// Применяем args к конфигурации хоста последними, поскольку ConfigureWebHostDefaults перезаписывает специфические для хоста настройки (имя приложения).
_bootstrapHostBuilder.ConfigureHostConfiguration(config =>
{
if (args is { Length: > 0 })
{
config.AddCommandLine(args);
}
// Применяем options после args
options.ApplyHostConfiguration(config);
});
Наконец, вызывается метод BootstrapHostBuilder.RunDefaultCallbacks(), который запускает все сохранённые обратные вызовы, которые мы накопили до сих пор, в правильном порядке, чтобы построить HostBuilderContext. Затем HostBuilderContext используется для окончательной установки остальных свойств в WebApplicationBuilder.
Configuration = new();
// Это конфигурация приложения
var hostContext = _bootstrapHostBuilder.RunDefaultCallbacks(Configuration, _hostBuilder);
// Получаем WebHostBuilderContext из свойств для использования в ConfigureWebHostBuilder
var webHostContext = (WebHostBuilderContext)hostContext.Properties[typeof(WebHostBuilderContext)];
// Получаем IWebHostEnvironment из webHostContext. А также регистрирует экземпляр конфигурации в IServiceCollection.
Environment = webHostContext.HostingEnvironment;
Logging = new LoggingBuilder(Services);
Host = new ConfigureHostBuilder(hostContext, Configuration, Services);
WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);
Services.AddSingleton<IConfiguration>(_ => Configuration);
Это конец конструктора. На этом этапе приложение настроено со всеми настройками по умолчанию для «хостинга»: конфигурация, ведение журнала, службы DI, среда и т. д.
Теперь вы можете добавить все свои собственные сервисы, дополнительную конфигурацию или ведение журнала в WebApplicationBuilder:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// добавляем конфигурацию
builder.Configuration.AddJsonFile("sharedsettings.json");
// добавляем сервисы
builder.Services.AddSingleton<MyTestService>();
builder.Services.AddRazorPages();
// добавляем ведение журнала
builder.Logging.AddFile();
// строим!
WebApplication app = builder.Build();
Закончив настройку конкретного приложения, вы вызываете Build() для создания экземпляра WebApplication. В последнем разделе этого поста мы заглянем внутрь метода Build().
Построение WebApplication в WebApplicationBuilder.Build()
Метод Build() не очень длинный, но его немного сложно понять, поэтому я рассмотрю его построчно:
public WebApplication Build()
{
// Копируем источники конфигурации в окончательный IConfigurationBuilder
_hostBuilder.ConfigureHostConfiguration(builder =>
{
foreach (var source in ((IConfigurationBuilder)Configuration).Sources)
{
builder.Sources.Add(source);
}
foreach (var (key, value) in ((IConfigurationBuilder)Configuration).Properties)
{
builder.Properties[key] = value;
}
});
// ...
}
Первое, что мы делаем, это копируем источники конфигурации, настроенные в ConfigurationManager, в реализацию ConfigurationBuilder _hostBuilder. Когда вызывается этот метод, построитель изначально пуст, поэтому он заполняет все источники, которые были добавлены как методами расширения построителя по умолчанию, так и дополнительные источники, которые вы настроили впоследствии.
Обратите внимание, что технически метод ConfigureHostConfiguration запускается не сразу. Скорее, мы регистрируем обратный вызов, который будет вызываться, когда мы далее вызовем _hostBuilder.Build().
Затем мы делаем то же самое для IServiceCollection, копируя их из экземпляра _services в коллекцию _hostBuilder. Комментарии здесь достаточно выразительные. В этом случае коллекция сервисов _hostBuilder не всегда пуста, но мы добавляем в неё всё из Services, а затем «сбрасываем» Services в экземпляр _hostBuilder.
// Это нужно сделать здесь, чтобы избежать добавления IHostedService, который дважды загружает сервер (GenericWebHostService).
// Копируем сервисы, которые были добавлены через WebApplicationBuilder.Services, в окончательную коллекцию IServiceCollection
_hostBuilder.ConfigureServices((context, services) =>
{
// На этом этапе мы добавили только сервисы, настроенные GenericWebHostBuilder и WebHost.ConfigureWebDefaults.
// HostBuilder обновляет новую коллекцию ServiceCollection в HostBuilder.Build(), которую мы ещё не видели
// до сих пор, поэтому мы не можем очистить эти сервисы, даже если некоторые из них избыточны, потому что
// мы вызвали ConfigureWebHostDefaults как для _deferredHostBuilder, так и для _hostBuilder.
foreach (var s in _services)
{
services.Add(s);
}
_services.InnerCollection = services;
});
В следующей строке мы запускаем все обратные вызовы, которые мы собрали в свойстве ConfigureHostBuilder, как я показывал выше.
Если они есть, то это обратные вызовы, такие как ConfigureContainer() и UseServiceProviderFactory(), которые обычно используются только в том случае, если вы используете сторонний контейнер DI.
Host.RunDeferredCallbacks(_hostBuilder);
Наконец, мы вызываем _hostBuilder.Build() для создания экземпляра Host и передаём его новому экземпляру WebApplication. Вызов _hostBuilder.Build() – это то место, где вызываются все зарегистрированные обратные вызовы.
_builtApplication = new WebApplication(_hostBuilder.Build());
Теперь нам надо сделать небольшую уборку. Чтобы всё было согласовано, экземпляр ConfigurationManager очищается и связывается с конфигурацией, хранящейся в WebApplication. Кроме того, IServiceCollection в WebApplicationBuilder помечается как доступный только для чтения, поэтому попытка добавить службы после вызова WebApplicationBuilder вызовет исключение InvalidOperationException. Наконец, возвращается построенное веб-приложение.
((IConfigurationBuilder)Configuration).Sources.Clear();
Configuration.AddConfiguration(_builtApplication.Configuration);
_services.IsReadOnly = true;
return _builtApplication;
Это почти всё для WebApplicationBuilder, но мы ещё не выполнили обратный вызов ConfigureApplication(). В следующем посте мы рассмотрим код, лежащий в основе типа WebApplication, и увидим, где, наконец, вызывается ConfigureApplication().
Итого
В этом посте мы рассмотрели часть кода нового API минимального хостинга WebApplicationBuilder. Я показал, как типы ConfigureHostBuilder и ConfigureWebHostBuilder действуют как адаптеры для универсального хоста и как BootstrapHostBuilder используется в качестве оболочки для внутреннего HostBuilder. Было довольно много запутанного кода только для создания экземпляра WebApplicationBuilder, но мы закончили статью, вызвав Build() для создания WebApplication. В следующем посте мы рассмотрим код WebApplication.
Комментарии (3)

mvv-rus
19.12.2021 19:22+1Это создаёт новый экземпляр WebApplicationOptions, инициализирует аргументы Args из аргументов командной строки и передаёт объект параметров в конструктор WebApplicationBuilder (показанный кратко ниже):
public static WebApplicationBuilder CreateBuilder(string[] args) =>
new(new() { Args = args });
Кстати, новое сокращённое ключевое слово new, когда целевой тип неочевиден, — просто отстой при разборе кода. Невозможно даже предположить, что создаёт второй new() в приведённом выше коде. Во многом претензии те же самые, как и к повсеместному использованию var, но в данном случае это меня особенно раздражает.
Ну, почему прям уж так невозможно ;-): просто открываете в GitHub WebApplicationBuilder.cs и смотрите, какие там параметры у конструктора. Первый параметр имеет тип WebApplicationOptions, его публичная часть крайне проста ;-):public class WebApplicationOptions { public string[]? Args { get; init; } // The command line arguments. public string? EnvironmentName { get; init; } // The environment name. public string? ApplicationName { get; init; } // The application name. public string? ContentRootPath { get; init; } // The content root path. public string? WebRootPath { get; init; } // The web root path. ... }
Вот именно этот класс и создает второй new, а сразу после этого его автоматическое свойство Args инициализуется списком параметров коммандой строки.
Правда, там ещё и парочка вложеных классов есть, с видимостью internal, но на них можно не обращать внимание.
И всего-то какие-то лишние пять минут на анализз этого куска надо потратить ;-)
PS Кстати, у конструктора WebApplicationOptions есть и второй параметр, configureDefaults, типа Action, а у статического метода WebApplication.CreateBuilder — перекрытый вариант, принимающий как параметр экземпляр WebApplicationOptions, так что можно создать такой экземпляр со своим configureDefaults, и сделать на его основе WebApplicationBuilder со своими настройками по умолчанию. Круто! ;-)
SBenzenko Автор
19.12.2021 19:29Имелось в виду, конечно, непосредственно из кода это решительно непонятно. Интересно, в IDE хотя бы "провалиться" в этоn new() можно? Или хоть всплывающая подсказка с типом появляется?
korsetlr473
они опять их разъединили ? весь в прошой версии наоборот объединяли в один ....