這是該系列的第三篇文章:探索 .NET 6:
- Part 1 - 探索 .NET 6 中的 ConfigurationManager
- Part 2 - 比較 WebApplicationBuilder 和 Generic Host
- Part 3 - 探索 WebApplicationBuilder 背后的代碼(當前文章)
- Part 4 - 使用 WebApplication 構建中間件管道
- Part 5 - 使用 WebApplicationBuilder 支持 EF Core 遷移
- Part 6 - 在 .NET 6 中使用 WebApplicationFactory 支持集成測試
- Part 7 - 用於 .NET 6 中 ASP.NET Core的分析器
- Part 8 - 使用源代碼生成器提高日志記錄性能
- Part 9 - 源代碼生成器更新:增量生成器
- Part 10 - .NET 6 中新的依賴關系注入功能
- Part 11 - [CallerArgumentExpression] and throw helpers
- Part 12 - 將基於 .NET 5 啟動版本的應用升級到 .NET 6
在我之前的文章中,我將新的WebApplication
與通用主機進行了比較。在這篇文章中,我將介紹WebApplicationBuilder
背后的代碼,以了解它如何實現更干凈,最小的托管API,同時仍然提供與通用主機相同的功能。
WebApplication 和 WebApplicationBuilder:引導 ASP.NET Core 應用程序的新方法
.NET 6 引入了一種全新的方法來"引導"ASP.NET Core應用程序。與 Program.cs 和 Startup.cs 之間的傳統拆分不同,整個引導代碼都采用 Program.cs,並且比以前版本中 Generic Host 所需的大量 lambda 方法更具過程性:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
WebApplication app = builder.Build();
app.UseStaticFiles();
app.MapGet("/", () => "Hello World!");
app.MapRazorPages();
app.Run();
有各種C#更新使這一切看起來更干凈(頂級語句,隱式使用,推斷的lambda類型等),但也有兩種新類型在起作用:WebApplication
和 WebApplicationBuilder
。在上一篇文章中,我簡要介紹了如何使用 WebApplication
和 WebApplicationBuilder
來配置 ASP.NET Core 應用程序。在這篇文章中,我們將看看它們背后的代碼,看看它們如何實現更簡單的API,同時仍然具有與通用主機相同的靈活性和可配置性。
創建WebApplicationBuilder
示例程序中的第一步是使用 WebApplication
上的靜態方法創建 WebApplicationBuilder
的實例:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
這將實例化 WebApplicationOptions
的新實例,從命令行參數分配 Args 參數,並將選項對象傳遞給 WebApplicationBuilder
構造函數(稍后顯示):
public static WebApplicationBuilder CreateBuilder(string[] args) =>
new(new() { Args = args });
順便說一句,當目標類型不明顯時,目標類型的
new
對於可發現性來說很糟糕。甚至無法猜測第二個new()
在上面的代碼中創建了什么。其中很多都是關於並不總是使用var
的相同論點,但我發現在這種情況下它特別令人討厭。
WebApplicationOptions
提供了一種以編程方式覆蓋某些重要屬性的簡單方法。如果未設置這些,則默認情況下將像在以前的版本中一樣推斷它們。
public class WebApplicationOptions
{
public string[]? Args { get; init; }
public string? EnvironmentName { get; init; }
public string? ApplicationName { get; init; }
public string? ContentRootPath { get; init; }
}
WebApplicationBuilder
構造函數是使最小托管概念正常工作的大量技巧所在:
public sealed class WebApplicationBuilder
{
internal WebApplicationBuilder(WebApplicationOptions options)
{
// .. shown below
}
我還沒有展示該方法的主體,因為這個構造函數中有很多事情要做,還有很多幫助器類型來實現它。我們稍后會回到這些,現在我們將重點介紹WebApplicationBuilder
的公共 API。
WebApplicationBuilder 的公共 API
WebApplicationBuilder
的公共 API 由一堆只讀屬性和一個創建 WebApplication
的單個方法 Build
() 組成。
public sealed class WebApplicationBuilder
{
public IWebHostEnvironment Environment { get; }
public IServiceCollection Services { get; }
public ConfigurationManager Configuration { get; }
public ILoggingBuilder Logging { get; }
public ConfigureWebHostBuilder WebHost { get; }
public ConfigureHostBuilder Host { get; }
public WebApplication Build()
}
如果您熟悉 ASP.NET Core,則其中許多屬性都使用以前版本中的常見類型:
IWebHostEnvironment
:用於檢索環境名稱、ContentRootPath
和類似值。IServiceCollection
:用於向 DI 容器注冊服務。請注意,這是泛型主機用於實現相同目的的ConfigureServices
() 方法的替代方法。ConfigurationManager
:用於添加新配置和檢索配置值。請參閱我在本系列中的第一篇文章,了解此類討論。ILoggingBuilder
:用於注冊其他日志記錄提供程序,就像在通用主機中使用ConfigureLogging()
方法一樣
WebHost
和 Host
屬性很有趣,因為它們公開了新的類型,ConfigureWebHostBuilder
和 ConfigureHostBuilder
。這些類型分別實現 IWebHostBuilder
和 IHostBuilder
,並且主要公開為一種讓你可以將 pre-.NET 6 的擴展方法與新類型一起使用的方法。例如,在上一篇文章中,我展示了如何通過在Host屬性上調用 UseSerilog()
來注冊 Serilog 和 ASP.NET Core集成:
builder.Host.UseSerilog();
公開 IWebHostBuilder
和 IHostBuilder
接口對於允許從 .NET 6 之前的應用程序遷移到新的最小托管 WebApplication
是絕對必要的,但它也被證明是一個挑戰。我們如何協調IHostBuilder
的lambda/callback樣式的配置與WebApplicationBuilder
的命令式樣式配置?這就是 ConfigureHostBuilder
和 ConfigureWebHostBuilder
以及一些內部 IHostBuilder
實現的用武之地:
我們將首先查看公共的 ConfigureHostBuilder
和 ConfigureWebHostBuilder
。
ConfigureHostBuilder: 一個 IHostBuilder 逃生艙口
ConfigureHostBuilder
和 ConfigureWebHostBuilder
已添加為最小托管更新的一部分。他們分別實現了IHostBuilder
和IWebHostBuilder
,但我將在這篇文章中重點介紹ConservationHostBuilder
:
public sealed class ConfigureHostBuilder : IHostBuilder, ISupportsConfigureWebHost
{
// ...
}
ConfigureHostBuilder
實現了 IHostBuilder
,看起來它實現了 ISupportsConfigureWebHost
,但看一下實現結果就會發現這是一個謊言:
IHostBuilder ISupportsConfigureWebHost.ConfigureWebHost(Action<IWebHostBuilder> configure, Action<WebHostBuilderOptions> configureOptions)
{
throw new NotSupportedException($"ConfigureWebHost() is not supported by WebApplicationBuilder.Host. Use the WebApplication returned by WebApplicationBuilder.Build() instead.");
}
這意味着,盡管編譯了以下代碼:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureWebHost(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
這將在運行時引發不支持的異常。這顯然不是理想的,但這是我們為擁有一個很好的命令式API來配置服務等而付出的代價。IHostBuilder.Build()
方法也是如此 - 這會引發一個不支持的異常。
看到這些運行時異常從來都不是一件好事,但是將 ConfigureHostBuilder
視為現有擴展方法(如 UseSerilog
() 方法)的"適配器"而不是"真正的"主機生成器會很有幫助。當您看到如何在以下類型上實現諸如 ConfigureServices()
或 ConfigureAppConfiguration()
之類的方法時,這一點就變得很明顯了:
public sealed class ConfigureHostBuilder : IHostBuilder, ISupportsConfigureWebHost
{
private readonly ConfigurationManager _configuration;
private readonly IServiceCollection _services;
private readonly HostBuilderContext _context;
internal ConfigureHostBuilder(HostBuilderContext context, ConfigurationManager configuration, IServiceCollection services)
{
_configuration = configuration;
_services = services;
_context = context;
}
public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
{
// Run these immediately so that they are observable by the imperative code
configureDelegate(_context, _configuration);
return this;
}
public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
{
// Run these immediately so that they are observable by the imperative code
configureDelegate(_context, _services);
return this;
}
}
例如,ConfigureServices()
方法使用從 WebApplicationBuilder
注入的 IServiceCollection
立即執行提供的 Action<>
。因此,以下兩個調用在功能上是相同的:
// directly registers the MyImplementation type with the IServiceContainer
builder.Services.AddSingleton<MyImplementation>();
// uses the "legacy" ConfigureServices method
builder.Host.ConfigureServices((ctx, services) => services.AddSingleton<MyImplementation>());
后一種方法顯然不值得在正常實踐中使用,但仍然可以使用依賴於此方法的現有代碼(例如擴展方法)。
並非所有傳遞給 ConfigureHostBuilder
中的方法的委托都會立即運行。有些,如UseServiceProviderFactory()
保存在一個列表中,並在稍后調用WebApplicationBuilder.Build()
時執行。
我認為這涵蓋了ConservationHostBuilder
類型,而 ConserveWebHostBuilder
非常相似,充當從以前的API到新的命令式樣式的適配器。現在我們可以回到 WebApplicationBuilder
構造函數。
BootstrapHostBuilder 幫助程序
在我們轉向查看 ConfigureHostBuilder
之前,我們先將查看 WebApplicationBuilder
構造函數。但我們還沒有准備好...首先,我們需要再看一個幫助器類,BootstrapHostBuilder
。
BootstrapHostBuilder
是一個IHostBuilder
內部實現,以供 WebApplicationBuilder
使用。它主要是一個相對簡單的實現,它"記住"它收到的所有 IHostBuilder
調用。例如,ConfigureHostConfiguration()
和 ConfigureServices()
函數如下所示:
internal class BootstrapHostBuilder : IHostBuilder
{
private readonly List<Action<IConfigurationBuilder>> _configureHostActions = new();
private readonly List<Action<HostBuilderContext, IServiceCollection>> _configureServicesActions = new();
public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate)
{
_configureHostActions.Add(configureDelegate);
return this;
}
public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
{
_configureServicesActions.Add(configureDelegate);
return this;
}
// ...
}
與立即執行提供的委托的 ConfigureHostBuilder
相比,BootstrapHostBuilder
將委托"保存"到稍后要執行的列表。這類似於通用主機生成器的工作方式。但請注意,BootstrapHostBuilder
是另一個"不可構建"的IHostBuilder
,因為調用Build()
會引發異常:
public IHost Build()
{
throw new InvalidOperationException();
}
BootstrapHostBuilder
的大部分復雜性在於其 RunDefaultCallbacks(ConfigurationManager, HostBuilder)
方法。這用於以正確的順序應用存儲的委托,我們將在后面看到。
WebApplicationBuilder 構造函數
最后,我們來看看 WebApplicationBuilder 構造函數。這包含很多代碼,所以我將逐個介紹它。
請注意,我已經采取了一些自由來刪除次要代碼(例如,保護子句和測試代碼)
public sealed class WebApplicationBuilder
{
private readonly HostBuilder _hostBuilder = new();
private readonly BootstrapHostBuilder _bootstrapHostBuilder;
private readonly WebApplicationServiceCollection _services = new();
internal WebApplicationBuilder(WebApplicationOptions options)
{
Services = _services;
var args = options.Args;
// ...
}
public IWebHostEnvironment Environment { get; }
public IServiceCollection Services { get; }
public ConfigurationManager Configuration { get; }
public ILoggingBuilder Logging { get; }
public ConfigureWebHostBuilder WebHost { get; }
public ConfigureHostBuilder Host { get; }
}
我們從私有字段和屬性開始。_hostBuilder
是通用主機 HostBuilder
的一個實例,HostBuilder
是支持 WebApplicationBuilder
的"內部"主機。我們還有一個 BootstrapHostBuilder
字段(來自上一節)和一個 WebApplicationServiceCollection
實例,這是一個 IServiceCollection
實現,我現在將對此進行介紹。
WebApplicationBuilder
的作用類似於通用主機的"適配器",_hostBuilder
,提供我在上一篇文章中探索的命令式API,同時保持與通用主機相同的功能。
值得慶幸的是,構造函數中的后續步驟已得到充分記錄:
// Run methods to configure both generic and web host defaults early to populate config from appsettings.json
// environment variables (both DOTNET_ and ASPNETCORE_ prefixed) and other possible default sources to prepopulate
// the correct defaults.
_bootstrapHostBuilder = new BootstrapHostBuilder(Services, _hostBuilder.Properties);
// Don't specify the args here since we want to apply them later so that args
// can override the defaults specified by ConfigureWebHostDefaults
_bootstrapHostBuilder.ConfigureDefaults(args: null);
// We specify the command line here last since we skipped the one in the call to ConfigureDefaults.
// The args can contain both host and application settings so we want to make sure
// we order those configuration providers appropriately without duplicating them
if (args is { Length: > 0 })
{
_bootstrapHostBuilder.ConfigureAppConfiguration(config =>
{
config.AddCommandLine(args);
});
}
創建 BootstrapHostBuilder
的實例后,第一個方法調用是 HostingBuilderExtension.ConfigureDefaults()
。這與調用 Host.CreateDefaultBuilder()
時泛型主機調用的方法完全相同。
請注意,
args
不會傳遞到ConfigureDefaults()
調用中,而是在以后應用。結果是args
在此階段不用於配置主機配置(主機配置確定應用程序名稱和宿主環境等值)。
下一個方法調用是 GenericHostBuilderExtensions.ConfigureWebHostDefaults()
,這與我們在 ASP.NET Core 3.x/5 中使用通用主機時通常調用的擴展方法相同。
_bootstrapHostBuilder.ConfigureWebHostDefaults(webHostBuilder =>
{
// Runs inline.
webHostBuilder.Configure(ConfigureApplication);
// We need to override the application name since the call to Configure will set it to
// be the calling assembly's name.
var applicationName = (Assembly.GetEntryAssembly())?.GetName()?.Name ?? string.Empty;
webHostBuilder.UseSetting(WebHostDefaults.ApplicationKey, applicationName);
});
此方法有效地在 BootstrapHostBuilder
的頂部添加一個 IWebHostBuilder
"適配器",在其上調用 WebHost.ConfigureWebDefaults()
,然后立即運行傳入的 lambda 方法。這將注冊 WebApplicationBuillder.ConfigureApplication()
方法,以便稍后調用,該方法設置了一大堆中間件。我們將在下一篇文章中回到該方法。
配置 Web 主機后,下一個方法將 args
應用於主機配置,確保它們正確覆蓋前面的擴展方法設置的默認值:
// Apply the args to host configuration last since ConfigureWebHostDefaults overrides a host specific setting (the application name).
_bootstrapHostBuilder.ConfigureHostConfiguration(config =>
{
if (args is { Length: > 0 })
{
config.AddCommandLine(args);
}
// Apply the options after the args
options.ApplyHostConfiguration(config);
});
最后,調用 BootstrapHostBuilder.RunDefaultCallbacks()
方法,該方法以正確的順序運行我們迄今為止積累的所有存儲回調,以構建 HostBuilderContext
。然后,使用
HostBuilderContext 最終在 WebApplicationBuilder
上設置其余屬性。
Configuration = new();
// This is the application configuration
var hostContext = _bootstrapHostBuilder.RunDefaultCallbacks(Configuration, _hostBuilder);
// Grab the WebHostBuilderContext from the property bag to use in the ConfigureWebHostBuilder
var webHostContext = (WebHostBuilderContext)hostContext.Properties[typeof(WebHostBuilderContext)];
// Grab the IWebHostEnvironment from the webHostContext. This also matches the instance in the IServiceCollection.
Environment = webHostContext.HostingEnvironment;
Logging = new LoggingBuilder(Services);
Host = new ConfigureHostBuilder(hostContext, Configuration, Services);
WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);
這就是構造函數的所有內容。此時,應用程序配置了所有"托管"默認值 — 配置、日志記錄、DI 服務和環境等。
現在,您可以將所有自己的服務、額外配置或日志記錄添加到 WebApplicationBuilder
:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// add configuration
builder.Configuration.AddJsonFile("sharedsettings.json");
// add services
builder.Services.AddSingleton<MyTestService>();
builder.Services.AddRazorPages();
// add configuration
builder.Logging.AddFile();
// build!
WebApplication app = builder.Build();
一旦完成特定於應用程序的配置后,就可以調用 Build()
創建一個 WebApplication
實例。在本文的最后一部分,我們將介紹 Build()
方法。
使用 WebApplicationBuilder.Build() 構建 WebApplication
Build()
方法不是很長,但有點難以理解,所以我將逐行介紹它:
public WebApplication Build()
{
// Copy the configuration sources into the final IConfigurationBuilder
_hostBuilder.ConfigureHostConfiguration(builder =>
{
foreach (var source in ((IConfigurationBuilder)Configuration).Sources)
{
builder.Sources.Add(source);
}
foreach (var (key, value) in ((IConfigurationBuilder)Configuration).Properties)
{
builder.Properties[key] = value;
}
});
// ...
}
我們要做的第一件事是將 ConfigurationManager
中配置的配置源復制到_hostBuilder
的 ConfigurationBuilder
實現中。調用此方法時,生成器最初為空,因此這將填充由默認生成器擴展方法添加的所有源以及隨后配置的額外源。
請注意,從技術上講,ConfigureHostConfiguration 方法不會立即運行。相反,我們正在注冊一個回調,當我們很快調用_hostBuilder.Build() 時,它將被調用。
接下來,我們對 IServiceCollection
執行類似的操作,將它們從_services
實例復制到_hostBuilder
的集合中。這里的注釋是相當有解釋性的;在這種情況下,_hostBuilder
的服務集合不是完全空的(只是大部分是空的),但是我們將服務中的所有內容添加,然后將服務"重置"到_hostBuilder
的實例中。
// This needs to go here to avoid adding the IHostedService that boots the server twice (the GenericWebHostService).
// Copy the services that were added via WebApplicationBuilder.Services into the final IServiceCollection
_hostBuilder.ConfigureServices((context, services) =>
{
// We've only added services configured by the GenericWebHostBuilder and WebHost.ConfigureWebDefaults
// at this point. HostBuilder news up a new ServiceCollection in HostBuilder.Build() we haven't seen
// until now, so we cannot clear these services even though some are redundant because
// we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder.
foreach (var s in _services)
{
services.Add(s);
}
// Add any services to the user visible service collection so that they are observable
// just in case users capture the Services property. Orchard does this to get a "blueprint"
// of the service collection
// Drop the reference to the existing collection and set the inner collection
// to the new one. This allows code that has references to the service collection to still function.
_services.InnerCollection = services;
});
在下一行中,我們將運行在 ConfigureHostBuilder
屬性中收集的任何回調,如前所述。
如果有,這些回調包括像
ConfigureContainer()
和UseServiceProviderFactory()
這樣的回調,它們通常只在使用第三方 DI 容器時使用。
// Run the other callbacks on the final host builder
Host.RunDeferredCallbacks(_hostBuilder);
最后,我們調用 _hostBuilder.Build()
來構建 Host 實例,並將其傳遞給 WebApplication
的新實例。對 _hostBuilder.Build()
的調用是調用所有已注冊回調的位置。
_builtApplication = new WebApplication(_hostBuilder.Build());
最后,我們有一點家務。為了保持所有內容的一致性,將清除 ConfigurationManager
實例,並將其鏈接到存儲在 WebApplication
中的配置。此外,WebApplicationBuilder
上的 IServiceCollection
被標記為只讀,因此在調用 WebApplicationBuilder
后嘗試添加服務將引發 InvalidOperationException
。最后,返回 WebApplication
。
// Make builder.Configuration match the final configuration. To do that
// we clear the sources and add the built configuration as a source
((IConfigurationBuilder)Configuration).Sources.Clear();
Configuration.AddConfiguration(_builtApplication.Configuration);
// Mark the service collection as read-only to prevent future modifications
_services.IsReadOnly = true;
return _builtApplication;
對於 WebApplicationBuilder
來說,這幾乎就是這樣,但我們仍然沒有執行 ConservationApplication()
回調。在下一篇文章中,我們將查看 WebApplication
類型背后的代碼,我們將看到 ConfigureApplication()
最終被調用的位置。
總結
在這篇文章中,我們介紹了新的 WebApplicationBuilder
最小托管API背后的一些代碼。我展示了 ConfigureHostBuilder
和 ConfigureWebHostBuilder
類型如何充當通用主機類型的適配器,以及如何將 BootstrapHostBuilder
用作內部 HostBuilder 的包裝器。僅僅為了創建 WebApplicationBuilder
的實例就有很多令人困惑的代碼,但是我們通過調用Build()
來創建 WebApplication
來結束這篇文章。在下一篇文章中,我們將介紹 WebApplication
背后的代碼。