這是『探索 .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 應用程式。
-
:配置 ASP.NET Core 應用程式的“原始”方法,截至 ASP.NET Core 2.x。WebHost.CreateDefaultBuilder()
-
:在通用Host.CreateDefaultBuilder()
的基礎上重新建構 ASP.NET Core,支援其他如 Worker 服務的工作負載。.NET Core 3.x 和 .NET 5 中的預設方法。Host
-
:.NET 6 中的新熱點。WebApplication.CreateBuilder()
為了更好地了解這些差異,我在下面幾節中重制了典型的“啟動”代碼,這應該會使 .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
。
在 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 實作了多種接口
WebApplication 實作了多種接口
一旦你在
WebApplicationBuilder
上配置了你需要的一切,你就可以調用
Build()
來建立一個
WebApplication
的執行個體:
var app = builder.Build();
WebApplication
很有趣,因為它實作了多個不同的接口:
-
- 用來啟動和停止 HostIHost
-
- 用于建立中間件管道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
翻譯:精緻碼農