探索 WebApplicationBuilder 背后的代码
这是该系列的第三篇文章:探索 .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
背后的代码。
Any fool can write code that a computer can understand. Good programmers write code that humans can understand. –Martin Fowler