В этой серии статей я собираюсь взглянуть на некоторые из новых функций, которые появились в .NET 6. Про .NET 6 уже написано много контента, в том числе множество постов непосредственно от команд .NET и ASP.NET. Я же собираюсь рассмотреть код некоторых из этих новых функций.
Часть 1. ConfigurationManager
Сравнение WebApplicationBuilder с универсальным Host
В .NET появился новый способ «по умолчанию» для создания приложений, используя WebApplication.CreateBuilder()
. В этом посте сравним этот подход с предыдущими подходами, обсудим, почему было сделано это изменение, и посмотрим, к чему это привело. В следующем посте рассмотрим код WebApplication
и WebApplicationBuilder
, чтобы понять, как они работают.
Создание приложений ASP.NET Core: урок истории
Прежде чем мы рассмотрим .NET 6, я думаю, имеет смысл взглянуть на то, как процесс «начальной загрузки» приложений ASP.NET Core развивался за последние несколько лет, поскольку первоначальный дизайн оказал огромное влияние на то, где мы находимся сегодня. Это станет ещё более очевидным, когда мы рассмотрим код WebApplicationBuilder
в следующем посте!
Даже если мы проигнорируем .NET Core 1.x (который на данный момент совсем не поддерживается), у нас есть три разных парадигмы для настройки приложения ASP.NET Core:
WebHost.CreateDefaultBuilder()
: «оригинальный» подход к настройке приложения ASP.NET Core, начиная с ASP.NET Core 2.x.Host.CreateDefaultBuilder()
: построение ASP.NET Core поверх универсального Host, поддерживающее другие рабочие нагрузки, такие как Worker Service. Подход по умолчанию в .NET Core 3.x и .NET 5.WebApplication.CreateBuilder()
: новинка .NET 6.
Чтобы лучше почувствовать различия, я воспроизвёл типичный «стартовый» код в следующих разделах, который должен сделать смысл изменений в .NET 6 более очевидным.
ASP.NET Core 2.x: WebHost.CreateDefaultBuilder()
В первой версии ASP.NET Core 1.x (если я правильно помню) не было концепции «хоста по умолчанию». Одна из идеологий ASP.NET Core заключалась в том, что всё должно быть «по запросу», т.е., если вам не нужно что-то использовать, вы не должны платить за наличие этого.
На практике это означало, что блок стартового кода содержал много шаблонного кода и множество NuGet пакетов. Чтобы избавить читателей от шока при взгляде на тонну кода только для того, чтобы приложение стартовало, в ASP.NET Core 2.x введён WebHost.CreateDefaultBuilder()
. Он настраивает для вас целую кучу значений по умолчанию, и создаёт IWebHostBuilder
, который строит IWebHost
.
Вот здесь я рассматривал код WebHost.CreateDefaultBuilder() ещё в 2017 и сравнивал его с ASP.NET Core 1.x, если вы вдруг захотите освежить это в памяти.
С самого начала ASP.NET Core отделил начальную загрузку «хоста» от начальной загрузки «приложения». Исторически это проявляется в разделении кода запуска между двумя файлами, традиционно называемыми Program.cs
и Startup.cs
.
В ASP.NET Core 2.1 Program.cs
вызывает WebHost.CreateDefaultBuilder()
, который устанавливает конфигурацию вашего приложения (например, загрузку из appsettings.json
), ведение журнала и настраивает интеграцию Kestrel и/или IIS.
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
Шаблоны по умолчанию также ссылаются на класс Startup
. Этот класс не реализует интерфейс явно. Скорее реализация IWebHostBuilder
просто знает, что нужно искать методы ConfigureServices()
и Configure()
для настройки контейнера внедрения зависимостей и конвейера промежуточного ПО соответственно.
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
// Этот метод вызывается средой исполнения. Используйте этот метод для настройки конвейера HTTP запроса.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
В приведённом выше классе Startup
мы добавили сервисы MVC в контейнер, добавили промежуточное ПО обработки исключений и статических файлов, а затем добавили промежуточное ПО MVC. Промежуточное ПО MVC было единственным реальным практическим способом создания приложений на начальном этапе, обслуживая как представления, отображаемые сервером, так и конечные точки RESTful API.
ASP.NET Core 3.x/5: универсальный HostBuilder
ASP.NET Core 3.x внёс несколько больших изменений в стартовый код приложений. Раньше ASP.NET Core можно было использовать только для проектов веб/HTTP, но в .NET Core 3.x был сделан шаг к поддержке других подходов: длительно работающие «рабочие сервисы» (например, для использования очередей сообщений), gRPC сервисы, службы Windows и многое другое. Цель состояла в том, чтобы иметь общую базовую структуру, которая была создана специально для веб-приложений (конфигурация, ведение журнала, DI), с этими другими типами приложений.
Результатом стало создание «универсального хоста» (в отличие от веб-хоста) и размещение стека ASP.NET Core поверх этой новой платформы. Вместо IWebHostBuilder
появился IHostBuilder
.
Опять же, у меня в то время вышла серия постов на тему этой миграции, если вам интересно!
Это изменение привело к нескольким неизбежным критическим изменениям, но команда ASP.NET сделала всё возможное, чтобы перенаправить весь этот код, написанный для IWebHostBuilder
, на использование IHostBuilder
. Одним из таких обходных путей был метод ConfigureWebHostDefaults()
, используемый по умолчанию в шаблонах Program.cs
:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
};
}
}
Необходимость в ConfigureWebHostDefaults
для регистрации класса Startup
приложений ASP.NET Core демонстрирует одну из проблем, которую решала группа .NET при миграции с IWebHostBuilder
на IHostBuilder
. Startup
неразрывно связан с веб-приложениями, поскольку метод Configure()
предназначен для настройки промежуточного ПО. Но рабочие сервисы и многие другие приложения не имеют промежуточного ПО, поэтому для классов Startup
не имеет смысла быть концепцией уровня «универсального хоста».
Здесь на помощь приходит метод расширения ConfigureWebHostDefaults()
в IHostBuilder
. Этот метод оборачивает IHostBuilder
во внутренний класс GenericWebHostBuilder
и устанавливает все значения по умолчанию, которые WebHost.CreateDefaultBuilder()
выполнял в ASP.NET Core 2.1. GenericWebHostBuilder
действует как адаптер между старым IWebHostBuilder
и новым IHostBuilder
.
Ещё одним большим изменением в ASP.NET Core 3.x стало введение маршрутизации конечных точек. Маршрутизация конечных точек была одной из первых попыток сделать доступными концепции, которые ранее в ASP.NET Core были ограничены частью MVC, в данном случае концепция маршрутизации. Это потребовало некоторого переосмысления конвейера промежуточного ПО, но во многих случаях необходимые изменения были минимальными.
Я ранее написал пост, в котором подробно описал маршрутизацию конечных точек, включая то, как конвертировать ваше промежуточное ПО на использование маршрутизации конечных точек. На русском языке краткое описание промежуточного ПО конечных точек есть здесь.
Несмотря на эти изменения, класс Startup
в ASP.NET Core 3.x выглядел очень похоже на версию 2.x. Пример ниже почти эквивалентен версии 2.x (хотя я использовал Razor Pages вместо MVC).
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
}
// Этот метод вызывается средой исполнения. Используйте этот метод для настройки конвейера HTTP запроса.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
}
ASP.NET Core 5 привнёс относительно немного больших изменений в существующие приложения, так что обновление с 3.x до 5, как правило, состояло всего лишь в изменении целевой платформы и обновлении некоторых NuGet пакетов.
Для .NET 6 это будет по-прежнему актуально, если вы обновляете существующие приложения. Но для новых приложений процесс начальной загрузки по умолчанию полностью изменился...
ASP.NET Core 6: WebApplicationBuilder:
Во всех предыдущих версиях ASP.NET Core конфигурация разделена на 2 файла. В .NET 6 добавлено множество изменений в C#, BCL и ASP.NET Core, и теперь всё может быть в одном файле.
Обратите внимание: никто не заставляет вас использовать этот стиль. Весь код, который я показал в коде ASP.NET Core 3.x/5, по-прежнему работает в .NET 6!
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.MapGet("/", () => "Hello World!");
app.MapRazorPages();
app.Run();
Здесь очень много изменений, вот некоторые из наиболее очевидных:
Операторы верхнего уровня означают отсутствие шаблона
Program.Main()
.Неявные директивы using означают, что операторы
using
не требуются. Я не включал их во фрагменты для предыдущих версий, но для .NET 6 они не нужны вовсе!Нет класса
Startup
– всё в одном файле.
Очевидно, что кода намного меньше, но нужно ли это? Или это сахар ради сахара? И как это работает?
Куда подевался весь код?
В .NET 6 большое внимание уделяется точке зрения «новичков». Если вы новичок в ASP.NET Core, вам нужно очень быстро осмыслить множество концепций. Просто взгляните на содержание моей книги; много что нужно изучить!
Изменения в .NET 6 в значительной степени направлены на устранение «церемонии», связанной с началом работы, и скрытие концепций, которые могут сбивать с толку новичков. Например:
Директивы
using
не обязательны для начала работы. Хотя редакторы кода обычно решают эту проблему, это лишняя головная боль, когда вы только начинаете изучать технологию.Точно так же, пространства имен - ненужная концепция, если вы новичок.
Program.Main()
… почему это так называется? Зачем это нужно? Просто потому, что нужно? Так вот, теперь нет.Конфигурация не разделена между двумя файлами,
Program.cs
иStartup.cs
. Хотя мне нравилось это «разделение ответственности», я не буду тосковать по необходимости объяснять новичкам, зачем нужно это разделение.Если уж мы заговорили о
Startup
, нам больше не нужно объяснять «волшебные» методы, которые вызываются, даже если они явно не реализуют интерфейс.
Кроме того, у нас есть новые типы WebApplication
и WebApplicationBuilder
. Эти типы не были строго необходимыми для достижения вышеупомянутых целей, но они дают нам несколько «более чистую» настройку.
А нам точно нужен новый тип?
Ну, на самом деле нет. Мы можем написать приложение .NET 6, очень похожее на приведённый выше пример, используя вместо этого универсальный хост:
var hostBuilder = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddRazorPages();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.Configure((ctx, app) =>
{
if (ctx.HostingEnvironment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", () => "Hello World!");
endpoints.MapRazorPages();
});
});
});
hostBuilder.Build().Run();
Я думаю, вы согласитесь, что это выглядит намного сложнее, чем версия .NET 6 WebApplication
. У нас есть целая куча вложенных лямбда-выражений, вы должны убедиться, что получаете правильные перегрузки, чтобы, например, получить доступ к конфигурации, и, вообще говоря, он превращает то, что является (в основном) процедурным сценарием начальной загрузки, во что-то более сложное.
Еще одно преимущество WebApplicationBuilder
состоит в том, что асинхронный код стал намного проще. Вы можете просто вызывать асинхронные методы, когда захотите. Надеюсь, это сделает эту серию статей, которые я написал про это в ASP.NET Core 3.x/5, ненужной!
Отличительной особенностью WebApplicationBuilder
и WebApplication
является то, что они по сути эквивалентны описанной выше общей настройке хоста, но делают это с помощью, очевидно, более простого API.
Большая часть конфигурации происходит в WebApplicationBuilder
Для начала рассмотрим WebApplicationBuilder
.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
WebApplicationBuilder отвечает за 4 основные вещи:
Добавление конфигурации с помощью
builder.Configuration
.Добавление сервисов с помощью
builder.Services
.Настройка журнала с помощью
builder.Logging
.Общая конфигурация
IHostBuilder
иIWebHostBuilder
.
Рассмотрим каждую из них по очереди…
WebApplicationBuilder
предоставляет тип ConfigurationManager
для добавления новых источников конфигурации, а также для доступа к значениям конфигурации, как я описал в моём предыдущем посте.
Он также предоставляет доступ к IServiceCollection
напрямую для добавления сервисов в контейнер DI. Поэтому, в то время как с универсальным хостом вам нужно было написать:
var hostBuilder = Host.CreateDefaultBuilder(args);
hostBuilder.ConfigureServices(services =>
{
services.AddRazorPages();
services.AddSingleton<MyThingy>();
})
с WebApplicationBuilder
вы можете написать
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSingleton<MyThingy>();
Аналогично для журнала вместо
var hostBuilder = Host.CreateDefaultBuilder(args);
hostBuilder.ConfigureLogging(builder =>
{
builder.AddFile();
})
вы можете написать:
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddFile();
Это код имеет точно такое же поведение, только в более простом виде. Для тех точек расширения, которые напрямую зависят от IHostBuilder
или IWebHostBuilder
, WebApplicationBuilder
предоставляет свойства Host
и WebHost
соответственно.
Например, настройка Serilog для ASP.NET Core подключается к IHostBuilder
, поэтому в ASP.NET Core 3.x/5 он добавляется с помощью следующего кода:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog() // <-- Add this line
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
С помощью WebApplicationBuilder
вы можете вызвать UseSerilog()
на свойстве Host
, а не в самом построителе:
builder.Host.UseSerilog();
Фактически, WebApplicationBuilder
– это то место, где вы выполняете всю настройку, кроме конвейера промежуточного ПО.
WebApplication – и швец, и жнец…
После того, как вы настроили всё необходимое в WebApplicationBuilder
, вы вызываете Build()
для создания экземпляра WebApplication
:
var app = builder.Build();
WebApplication
интересен тем, что реализует несколько различных интерфейсов:
IApplicationBuilder
– используется для создания конвейера промежуточного ПО.IEndpointRouteBuilder
– используется для добавления конечных точек.
Два последних пункта во многом связаны. В ASP.NET Core 3.x и 5 IEndpointRouteBuilder
используется для добавления конечных точек путем вызова UseEndpoints()
и передачи ему лямбды, например:
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
В этом шаблоне .NET 3.x/5 для новичков в ASP.NET Core есть несколько сложностей:
Построение конвейера промежуточного ПО происходит в функции
Configure()
классаStartup
(вы должны об этом знать, чтобы искать его там).Обязательно вызывайте
app.UseRouting()
передapp.UseEndpoints()
(а также размещайте другое промежуточное ПО в нужном месте).Вы должны использовать лямбда-выражение для настройки конечных точек (несложно для пользователей, знакомых с C#, но может сбивать с толку новичков).
WebApplication
значительно упрощает этот шаблон:
app.UseStaticFiles();
app.MapRazorPages();
Это явно намного проще, хотя я нашёл это немного обманчивым, поскольку различие между промежуточным ПО и конечными точками гораздо менее очевидно, чем в .NET 5.x. Вероятно, это просто дело вкуса, но я думаю, что такой подход размывает концепцию важности порядка настройки (которая относится к промежуточному ПО, но не к конечным точкам).
Что я ещё не показал, так это основные принципы построения WebApplication
и WebApplicationBuilder
. В следующем посте я приоткрою завесу тайны, чтобы мы смогли посмотреть, что на самом деле происходит за кулисами.
Итого
В этом посте я описал, как начальная загрузка приложений ASP.NET Core изменилась с версии 2.x до .NET 6. Я показал новые типы WebApplication
и WebApplicationBuilder
, представленные в .NET 6, и рассказал, почему они были введены, а также о некоторых преимуществах, которые они приносят. Наконец, я рассказал о разных ролях, которые играют эти два класса, и о том, как их API упрощают процесс запуска. В следующем посте я рассмотрю код, лежащий в основе типов, чтобы увидеть, как они работают.
Комментарии (5)
sebasww
13.12.2021 11:10Я правильно понял, что проблема WebApplicationBuilder - это отсутствие возможности проброса DI в конструктор промежуточного ПО ?
mvv-rus
14.12.2021 20:24Никакой проблемы. У класса WebApplicationBuilder есть свойство Host, которое, вопреки своему названию, дает доступ не к IHost или его реализации, а к реализующему интерфейс IHostBuilder классу ConfigureHostBuilder. Так что метод(точнее методы — их два перегруженных) UseServiceProviderFactory для подключения стороннего контейнера вполне доступен, равно как и метод ConfigureContainer для его конфигурирования.
DadeMurphyZC
Спасибо за пост! Интересно было почитать про нововведения .NET 6, например что нет класса Startup, все в одном файле.
mvv-rus
Вот то, что класс Startup не нужен (точнее, из шаблона по умолчанию его убрали) — это, по сути, ни разу не нововведение.
И раньше вполне можно было вместо него использовать сам класс Program — просто перенести в него из Startup.cs методы ConfigureServices и Configure и указать его имя вместо Startup в вызове UseStartup для IWebHostBuilder (а Startup.cs вообще выкинуть).
Примерно так (шаблон Generic Host, в Program.cs нужно еще добавить несколько using):