天天看點

深入了解.NET 6 WebApplicationBuilder 和 Host

作者:DotNET技術圈

這是『探索 .NET 6』系列的第二篇文章:

  • 01 揭開 ConfigurationManager 的面紗
  • 02 比較

    WebApplicationBuilder

    Host

在 .NET 中,有一種新的“預設”方法用來建構應用程式,即使用

WebApplication.CreateBuilder()

。在這篇文章中,我将這種方法與以前的方法進行了比較,讨論了為什麼要進行這種改變,并看看其影響。在下一篇文章中,我将看一下

WebApplication

WebApplicationBuilder

背後的代碼,看看它們是如何工作的。

1建構 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()

    :在通用

    Host

    的基礎上重新建構 ASP.NET Core,支援其他如 Worker 服務的工作負載。.NET Core 3.x 和 .NET 5 中的預設方法。
  • WebApplication.CreateBuilder()

    :.NET 6 中的新熱點。

為了更好地了解這些差異,我在下面幾節中重制了典型的“啟動”代碼,這應該會使 .NET 6 的變化更加明顯。

2ASP.NET Core 2.x:WebHost.CreateDefaultBuilder()

在 ASP.NET Core 1.x 的第一個版本中,(如果我記得沒錯的話)沒有“預設” Host 的概念。ASP.NET Core 的理念之一是一切都應該“按需付費”,也就是說,如果你不需要使用它,你就不應該為該功能的存在消費資源。

在實踐中,這意味着“入門”模闆包含了大量的模闆,以及大量的 NuGet 包。為了不看到所有這些代碼就能開始的快速開發,ASP.NET Core 引入了

WebHost.CreateDefaultBuilder()

。這為你設定了一大堆的預設值,建立了一個

IWebHostBuilder

,并建立了一個

IWebHost

從一開始,ASP.NET Core 就将 Host 啟動與應用程式啟動分開。從曆史上看,這表現為将你的啟動代碼分成兩個檔案,傳統上稱為

Program.cs

Startup.cs

深入了解.NET 6 WebApplicationBuilder 和 Host

在 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?}");
 });
 }
}           

在上面的啟動類中,我們将 MVC 服務添加到容器中,添加了異常處理和靜态檔案中間件,然後添加了 MVC 中間件。MVC 中間件是最初建構應用程式的唯一真正實用的方法,它同時滿足了伺服器渲染的視圖和 RESTful API 端點。

3ASP.NET Core 3.x/5:HostBuilder

ASP.NET Core 3.x 給 ASP.NET Core 的啟動代碼帶來了一些重大變化。以前,ASP.NET Core 隻能真正用于 Web/HTTP 工作負載,但在 .NET Core 3.x 中,做出了支援其他方法的舉措:長期運作的“worker services”(例如,用于消費消息隊列)、gRPC 服務、Windows 服務等等。我們的目标是與這些其他類型的應用分享專門為建構 Web 應用(配置、日志、DI)而建立的基礎架構。

結果是建立了一個“通用 Host”(相對于 Web Host 而言),并在此基礎上對 ASP.NET Core 技術棧進行了“重新平台化”。用

IWebHostBuilder

代替了

IHostBuilder

這一變化引起了一些不可避免的破壞性變化,但 ASP.NET 團隊盡力為所有針對

IWebHostBuilder

而不是

IHostBuilder

編寫的代碼提供了指引。其中一個變通方法是

Program.cs

模闆中預設使用的

ConfigureWebHostDefaults()

方法:

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

來注冊 ASP.NET Core 應用程式的

Startup

類,這是 .NET 團隊在提供從

IWebHostBuilder

IHostBuilder

的遷移路徑時面臨的挑戰之一。

Startup

與 Web 應用密不可分,因為

Configure()

方法是配置中間件的。但 worker service 和許多其他應用程式沒有中間件,是以

Startup

類作為一個“通用 Host”級别的概念是沒有意義的。

這就是

IHostBuilder

上的

ConfigureWebHostDefaults()

擴充方法的作用。這個方法将

IHostBuilder

包裹在一個内部類中,即

GenericWebHostBuilder

,并設定

WebHost.CreateDefaultBuilder()

在 ASP.NET Core 2.1 中的所有預設值。

GenericWebHostBuilder

作為舊的

IWebHostBuilder

和新的

IHostBuilder

之間的一個擴充卡。

ASP.NET Core 3.x 的另一個重大變化是引入了端點路由。端點路由是首次嘗試使以前僅限于 ASP.NET Core 的 MVC 部分的路由概念可以通用。這需要對你的中間件管道進行一些重新思考,但在許多情況下,必要的改變是最小的。

盡管有這些變化,ASP.NET Core 3.x 中的

Startup

類看起來與 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 來說,如果你要更新現有的應用程式,也是這樣。但是對于新的應用程式來說,預設的啟動體驗已經完全改變了...

4ASP.NET Core 6:WebApplicationBuilder

所有以前的 ASP.NET Core 版本都将配置分成兩個檔案。在 .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

    語句。
  • 沒有

    Startup

    類--所有東西都在一個檔案中。

這顯然減少了很多代碼,但這有必要嗎?它又是如何工作的呢?

5所有的代碼都去哪兒了

.NET 6 的一大重點是“新人”的視角。作為 ASP.NET Core 的初學者,有一大堆的概念需要你快速了解。隻要看看我的書的目錄就知道了;有很多東西需要你去了解!

.NET 6 的變化主要集中在消除與入門相關的“儀式”,以及隐藏那些可能讓新人感到困惑的概念。比如說:

  • using

    語句在入門時是不必要的。盡管工具化通常使這些在實踐中成為一個非問題,但當你開始學習時,它們顯然是一個不必要的概念。
  • 與此類似,

    namespace

    在你入門時也是一個不必要的概念。
  • Program.Main()

    ...為什麼叫這個名字?為什麼我需要它?因為你需要。隻是現在你不需要了。
  • 配置沒有被分割在兩個檔案中,

    Program.cs

    Startup.cs

    。雖然我喜歡這種“關注點分離”,但這要向新來者解釋為什麼這種分割方式。
  • 當我們談論

    Startup

    時,我們不再需要解釋“魔術”方法,這些方法可以被調用,盡管它們沒有明确地實作一個接口。

此外,我們還有新的

WebApplication

WebApplicationBuilder

類型。這些類型對于實作上述目标并不是嚴格必要的,但它們确實在某種程度上使配置體驗更加幹淨。

6我們真的需要一個新的類型嗎

嗯,不,我們不需要。我們可以用通用 Host 來編寫一個與上面的例子非常相似的 .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

版本要複雜得多。我們有一大堆嵌套的 lambda,它将一個(大部分)程式性的啟動腳本變成了更複雜的東西。

WebApplicationBuilder

的另一個好處是,啟動時的異步代碼要簡單得多。你可以在你喜歡的時候調用異步方法。

關于

WebApplicationBuilder

WebApplication

的巧妙之處在于,它們基本上等同于上述的通用 Host 的設定,但它們用了一個更簡單的 API 來實作。

7大多數配置在 WebApplicationBuilder 中

讓我們先來看看

WebApplicationBuilder

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();           

WebApplicationBuilder

主要負責 4 項工作:

  • 使用

    builder.Configuration

    添加配置。
  • 使用

    builder.Services

    添加服務
  • 使用

    builder.Logging

    配置日志
  • 配置

    IHostBuilder

    IWebHostBuilder

依次來看...

WebApplicationBuilder

暴露了

ConfigurationManager

類型,用于添加新的配置源,以及通路配置值,正如我在之前的文章中所描述的。

它還直接暴露了一個

IServiceCollection

,用于向 DI 容器添加服務。是以,在通用 Host 中,你必須做的是:

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

屬性。

例如,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

,你可以在

Host

屬性上調用

UseSerilog()

builder.Host.UseSerilog();           

事實上,

WebApplicationBuilder

是你做所有配置的地方,除了中間件管道。

8

WebApplication 實作了多種接口

一旦你在

WebApplicationBuilder

上配置了你需要的一切,你就可以調用

Build()

來建立一個

WebApplication

的執行個體:

var app = builder.Build();           

WebApplication

很有趣,因為它實作了多個不同的接口:

  • IHost

    - 用來啟動和停止 Host
  • IApplicationBuilder

    - 用于建立中間件管道
  • IEndpointRouteBuilder

    - 用于添加路由端點

後面這兩點是非常相關的。在 ASP.NET Core 3.x 和 5 中,

IEndpointRouteBuilder

用于通過調用

UseEndpoints()

并向其傳遞一個 lambda 來添加端點,例如:

public void Configure(IApplicationBuilder app)
{
 app.UseStaticFiles();
 app.UseRouting();
 app.UseEndpoints(endpoints =>
 {
 endpoints.MapRazorPages();
 });
}           

對于剛接觸 ASP.NET Core 的人來說,這種 .NET 3.x/5 模式有一些複雜:

  • 中間件管道的建構發生在

    Startup

    Configure()

    函數中(你必須知道去看那裡)。
  • 你必須確定在

    app.UseEndpoints()

    之前調用

    app.UseRouting()

    (以及将其他中間件放在正确的位置)。
  • 你必須使用 lambda 來配置端點(對于熟悉 C# 的使用者來說并不複雜,但對于新人來說可能會感到困惑)。

WebApplication

大大簡化了這種模式:

app.UseStaticFiles();
app.MapRazorPages();           

這顯然要簡單得多,盡管我發現它有點令人困惑,因為中間件和端點之間的差別遠沒有 .NET 5.x 等中那麼清晰。這可能隻是個人看法不同,但我認為這混淆了“順序很重要”的資訊(這适用于中間件,但一般不适用端點)。

我還沒有展示的是

WebApplication

WebApplicationBuilder

是如何建構的。在下一篇文章中,我将揭開幕布,讓我們看到幕後的真實情況。

9總結

在這篇文章中,我描述了 ASP.NET Core 應用程式的啟動從 2.x 版本一直到 .NET 6 的變化。我展示了 .NET 6 中引入的新的

WebApplication

WebApplicationBuilder

類型,讨論了它們被引入的原因,以及它們帶來的一些優勢。最後,我讨論了這兩個類所扮演的不同角色,以及它們的 API 如何使啟動體驗更簡單。在下一篇文章中,我将看一下這些類型背後的一些代碼,看看它們是如何工作的。

原文:bit.ly/3fDZlS9

作者:Andrew Lock

翻譯:精緻碼農

繼續閱讀