Сравнение WebApplicationBuilder с Generic Host

  • Михаил
  • 8 мин. на прочтение
  • 93
  • 27 Jun 2022
  • 27 Jun 2022

Существует новый способ создания приложений в .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 , поддерживающая другие рабочие нагрузки, такие как рабочие службы. Подход по умолчанию в .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 представила WebHost.CreateDefaultBuilder(). Это устанавливает для вас целый ряд значений по умолчанию, создавая IWebHostBuilderи создавая файл IWebHost.

С самого начала ASP.NET Core разделил начальную загрузку "хоста" от начальной загрузки "приложения". Исторически это проявляется в разделении кода запуска между двумя файлами, традиционно называемыми Program.cs и Startup.cs .

Разница в области конфигурации для программы и запуска. Программа касается конфигурации инфраструктуры, которая обычно остается стабильной на протяжении всего срока действия проекта. Напротив, вы часто будете изменять автозагрузку, чтобы добавить новые функции и обновить поведение приложения. Взято из моей книги ASP.NET Core в действии, второе издание.
В 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();
   }
   // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
   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. Раньше 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классы как концепцию уровня «общего хоста».

Вот тут-то и пригодится метод расширения onConfigureWebHostDefaults() . Этот метод оборачивает . во внутренний класс и устанавливает все значения по умолчанию, которые были в ASP.NET Core 2.1. действует как адаптер между старым и новым .IHostBuilderIHostBuilderGenericWebHostBuilderWebHost.CreateDefaultBuilder()GenericWebHostBuilderIWebHostBuilderIHostBuilder

Еще одним большим изменением в ASP.NET Core 3.x стало введение маршрутизации конечных точек . Маршрутизация конечных точек была одной из первых попыток сделать доступными концепции, которые ранее ограничивались частью MVC ASP.NET Core, в данном случае — концепцией маршрутизации . Это потребовало некоторого переосмысления вашего конвейера промежуточного программного обеспечения, но во многих случаях необходимые изменения были минимальными.

Несмотря на эти изменения, 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();
   }
   // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
   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 разделяли конфигурацию на два файла. В .NET 6 множество изменений в C#, BCL и ASP.NET Core означает, что теперь все может быть в одном файле.

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При начале работы эти утверждения необязательны. Хотя на практике инструменты обычно делают их не проблемой, они явно ненужны, когда вы начинаете.
  • Аналогично этому, namespaceэто ненужная концепция, когда вы только начинаете.
  • 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();

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

Еще одним преимуществом WebApplicationBuilderявляется то, что asyncкод во время запуска намного проще. Вы можете просто вызывать asyncметоды, когда захотите.

Самое интересное в 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();

Это имеет точно такое же поведение, только с более простым в использовании API. Для тех точек расширения, которые зависят от IHostBuilderили IWebHostBuilderнапрямую, WebApplicationBuilderпредоставляются свойства Hostи WebHostсоответственно.

Например, интеграция ASP.NET Core Serilog подключается к 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 интересен, поскольку реализует несколько разных интерфейсов:

  • IHost- используется для запуска и остановки хоста
  • 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 упрощают запуск. В следующем посте я рассмотрю часть кода этих типов, чтобы понять, как они работают.