[探索 .NET 6]02 比較 WebApplicationBuilder 和 Host

語言: CN / TW / HK

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

在 .NET 中,有一種新的“預設”方法用來構建應用程式,即使用 WebApplication.CreateBuilder() 。在這篇文章中,我將這種方法與以前的方法進行了比較,討論了為什麼要進行這種改變,並看看其影響。在下一篇文章中,我將看一下 WebApplicationWebApplicationBuilder 背後的程式碼,看看它們是如何工作的。

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 的變化更加明顯。

2 ASP.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.csStartup.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();
}

// 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 端點。

3 ASP.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 團隊在提供從 IWebHostBuilderIHostBuilder 的遷移路徑時面臨的挑戰之一。 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 軟體包 :tada:。

對於 .NET 6 來說,如果你要升級現有的應用程式,也是這樣。但是對於新的應用程式來說,預設的啟動體驗已經完全改變了...

4 ASP.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.csStartup.cs 。雖然我喜歡這種“關注點分離”,但這要向新來者解釋為什麼這種分割方式。

  • 當我們談論 Startup 時,我們不再需要解釋“魔術”方法,這些方法可以被呼叫,儘管它們沒有明確地實現一個介面。

此外,我們還有新的 WebApplicationWebApplicationBuilder 型別。這些型別對於實現上述目標並不是嚴格必要的,但它們確實在某種程度上使配置體驗更加乾淨。

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 的另一個好處是,啟動時的非同步程式碼要簡單得多。你可以在你喜歡的時候呼叫非同步方法。

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

7 大多數配置在  WebApplicationBuilder 

讓我們先來看看 WebApplicationBuilder

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

WebApplicationBuilder 主要負責 4 項工作:

  • 使用 builder.Configuration 新增配置。

  • 使用 builder.Services 新增服務

  • 使用 builder.Logging 配置日誌

  • 配置 IHostBuilderIWebHostBuilder

依次來看...

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 中。對於那些直接依賴 IHostBuilderIWebHostBuilder 的擴充套件點, WebApplicationBuilder 分別暴露了 HostWebHost 屬性。

例如,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 模式有一些複雜:

  • 中介軟體管道的構建發生在 StartupConfigure() 函式中(你必須知道去看那裡)。

  • 你必須確保在 app.UseEndpoints() 之前呼叫 app.UseRouting() (以及將其他中介軟體放在正確的位置)。

  • 你必須使用 lambda 來配置端點(對於熟悉 C# 的使用者來說並不複雜,但對於新人來說可能會感到困惑)。

WebApplication 大大簡化了這種模式:

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

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

我還沒有展示的是 WebApplicationWebApplicationBuilder 是如何構建的。在下一篇文章中,我將揭開幕布,讓我們看到幕後的真實情況。

9 總結

在這篇文章中,我描述了 ASP.NET Core 應用程式的啟動從 2.x 版本一直到 .NET 6 的變化。我展示了 .NET 6 中引入的新的 WebApplicationWebApplicationBuilder 型別,討論了它們被引入的原因,以及它們帶來的一些優勢。最後,我討論了這兩個類所扮演的不同角色,以及它們的 API 如何使啟動體驗更簡單。在下一篇文章中,我將看一下這些型別背後的一些程式碼,看看它們是如何工作的。

原文:bit.ly/3fDZlS9

作者:Andrew Lock

翻譯:精緻碼農