[探索 .NET 6]02 比較 WebApplicationBuilder 和 Host
rt這是『探索 .NET 6』系列的第二篇文章:
-
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 的變化更加明顯。
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.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();
}
// 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 團隊在提供從 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 軟體包 :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.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
翻譯:精緻碼農
- C# 面向物件程式設計之里氏替換原則實戰解析
- C# 面向物件程式設計之開閉原則實戰解析
- C# 面向物件程式設計之單一職責原則實戰解析
- C#單元測試的使用(三)
- .NET基礎知識快速通關(9)
- .NET基礎知識快速通關(6)
- .NET基礎知識快速通關(5)
- 面試寶典之.NET基礎知識快速通關(1)
- 填坑 | .NET 在Docker中訪問MSSQL報錯
- 如何分析.NET Core HttpClient請求異常
- 理解C#泛型原理
- 萬字長文講解:什麼是「抽象」?
- 簡述使用REST API 的最佳實踐
- 如何基於.NET Core構建分散式檔案儲存系統?
- 利用SOS擴充套件庫進入高階.NET6程式的除錯
- 淺議開發者面臨的資訊偏差影響因素
- 【新書速遞】龍芯開源LoongArch版,學會造計算機!
- .NET誕生20週年 .NET 7有什麼新東西?
- 基於.NET 製作一個氣象站 IoT 應用
- [探索 .NET 6]02 比較 WebApplicationBuilder 和 Host