使用 WebApplication 构建中间件管道
这是该系列的第四篇文章:探索 .NET 6。
在我之前的文章中,我们查看了 WebApplicationBuilder 背后的代码,包括它的一些帮助器类,如 ConservationHostBuilder 和 BootstrapHostBuilder。在文章的最后,我们创建了一个 WebApplicationBuilder 的实例,并调用 Build() 来创建一个 WebApplication。在这篇文章中,我们将介绍 WebApplication 背后的一些代码,并重点介绍中间件和终结点的配置方式。
# WebApplication:简要回顾
这篇文章是上一篇关于 WebApplicationBuilder 的文章,以及 WebApplication 的介绍的延续,所以我建议你从这些帖子开始,如果你还没有读过它们。也就是说,与上一篇文章相比,我不会在这篇文章中深入代码!
正如我在之前文章中所描述的那样,WebApplicationBuilder 是您进行大部分应用程序配置的地方,而 WebApplication 用于三个不同的事情:
- 这是您配置中间件管道的地方,因为它实现了
IApplicationBuilder。 - 这是您使用
MapGet(),MapRazorPages()等配置终结点的地方,因为它实现了IEndpointRouteBuilder。 - 这是您通过调用
Run() 来实际启动应用程序所运行的内容,因为它实现了IHost。
在上一篇文章的末尾,我们看到当您在 WebApplicationBuilder 实例上调用 Build() 时,将创建通用主机 Host 的私有实例,并将其传递给 WebApplication 构造函数。这篇文章就是从这里开始继续,在我们开始研究中间件管道之前,先对 WebApplication 有一点了解。
# WebApplication:围绕三种类型的相对简单的包装器。
与 WebApplicationBuilder 构造函数相比,WebApplication 构造函数相对简单:
public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable
{
private readonly IHost _host;
private readonly List<EndpointDataSource> _dataSources = new();
internal IDictionary<string, object?> Properties => ApplicationBuilder.Properties;
internal static string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder";
internal WebApplication(IHost host)
{
_host = host;
ApplicationBuilder = new ApplicationBuilder(host.Services);
Properties[GlobalEndpointRouteBuilderKey] = this;
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
构造函数和字段初始值设定项主要执行 4 项操作:
- 在
_host字段中保存提供的Host。这与 ASP.NET Core 3.x/5 中使用通用主机时所使用的Host类型相同。 - 创建
EndpointDataSource的新列表。这些用于配置应用程序中的终结点,包括 Razor 页面、控制器、API 终结点和新的"最小" APIs。 - 创建
ApplicationBuilder的新实例。这用于构建中间件管道,并且基本上与自版本 1.0 以来使用的类型相同! - 在
ApplicationBuilder上设置__GlobalEndpointRouteBuilder属性。此属性用于"通信"到我们正在使用新WebApplication的其他中间件,它具有一些不同的默认值,稍后您将看到。
WebApplication 主要将 IHost、IApplicationBuilder 和 IEndpointRouteBuilder 的实现委托给其私有属性,并显式实现它们。例如,对于 IApplicationBuilder,大部分实现都委托给在构造函数中创建的 ApplicationBuilder:
IDictionary<string, object?> IApplicationBuilder.Properties => ApplicationBuilder.Properties;
IServiceProvider IApplicationBuilder.ApplicationServices
{
get => ApplicationBuilder.ApplicationServices;
set => ApplicationBuilder.ApplicationServices = value;
}
IApplicationBuilder IApplicationBuilder.Use(Func<RequestDelegate, RequestDelegate> middleware)
{
ApplicationBuilder.Use(middleware);
return this;
}
2
3
4
5
6
7
8
9
10
11
12
13
有趣的是,当您调用 RunAsync() 时,它会构建中间件管道。
# 启动 WebApplication 并构建中间件管道
启动应用程序的标准方法是在 WebApplication 上调用 app.Run()。这将调用 WebApplication.RunAsync,后者又调用 IHost 扩展方法HostingAbstractionsHostExtensions.RunAsync(),该方法与 3.x/5 中与通用主机一起使用的方法相同。这反过来又调用了 IHost.StartAsync,它启动了我之前写过的复杂的启动交互 (opens new window)。

如上图所示,Host 运行在应用程序中注册的 IHostedService,作为启动序列的一部分。从 ASP.NET Core 3.x 中的"伟大的重新平台化"开始,Web 服务器也作为 IHostedService 运行,所以这是构建中间件管道并开始侦听的时候。
GenericWebHostService 是构建中间件管道的地方,最后传递给 Kestrel 以运行您的应用程序。与通用主机相比,WebApplication 在此过程中没有什么不同,因此,我们将介绍WebApplication 如何设置中间件管道,而不是深入研究(广泛的!)代码。
# WebApplication 的中间件管道
与通用主机相比,WebApplication 的一大区别是 WebApplication 默认设置各种中间件。在上一篇文章中,我展示了 WebApplicationBuilder 调用GenericHostBuilderExtensions.ConfigureWebHostDefaults()。这也通常与通用主机一起使用,并设置了几个默认值:
- 配置 Kestrel
- 添加
HostFiltering中间件 (opens new window) - 如果
ASPNETCORE_FORWARDEDHEADERS_ENABLED环境变量设置为true,则添加ForwardedHeaders中间件 (opens new window)。 - 支持 IIS 集成
除此之外,WebApplicationBuilder 还设置了额外的中间件。这就是我在上一篇文章中提到的配置应用程序方法的用武之地。根据您配置 WebApplication 的方式,ConfigureApplication 会向管道添加其他中间件。
此代码有点复杂,因为它需要处理多个边缘情况。与其尝试过于详细地跟踪代码,不如看一些示例,看看生成的中间件管道是什么样子的。
# 空管道
我们将从最基本(且有些无意义)的应用程序开始,其中我们不会向应用程序添加任何额外的中间件或终结点:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.Run();
2
3
4
此设置是最基本的配置,并生成一个中间件管道,其中包含:
HostFilteringMiddlewareDeveloperExceptionPageMiddleware- 从
WebApplication的ApplicationBuilder构建的"空"中间件。
如下所示:
HostFilteringMiddleware 是由于对 ConfigureWebHostDefaults() 的隐藏调用而添加的,如上一节所述。未添加 ForwardedHeadersMiddleware,因为未添加环境变量。
现在,当您在开发环境中运行时,会自动添加 DeveloperExceptionPage,并将以下内容添加到每个中间件管道的开头:
if (context.HostingEnvironment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
2
3
4
管道中的最后一块中间件是使用直接添加到 Program.cs 中的 WebApplication 的 ApplicationBuilder 的中间件构建的。在此示例中,我们没有添加任何内容,因此我们实质上是在中间件内部添加了一个"空"管道。
显然,仅具有默认中间件的应用程序没有多大用处,因此让我们做一些基本的并向WebApplication 添加一些中间件。
# 具有额外中间件的管道
在此示例中,我们将一些其他中间件添加到管道中:静态文件中间件和 HTTPS 重定向中间件:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
// 添加一些额外的中间件
app.UseHttpsRedirection();
app.UseStaticFiles();
app.Run();
2
3
4
5
6
7
8
通过此设置,我们仍然拥有与上一节中相同的三核中间件,但 WebApplication.ApplicationBuilder 管道现在包含两个额外的中间件
HostFilteringMiddlewareDeveloperExceptionPageMiddlewareWebApplication.ApplicationBuilder管道,包含HttpsRedirectionMiddlewareStaticFilesMiddleware
所以现在我们有一个看起来像这样的东西:

我们仍然没有任何终结点,因此在下一个示例中,我们实现了一个 "Hello World!" 终结点 :
# "Hello World" 管道
在下面的示例中,我们为主页添加了一个终结点,该终结点将文本 "Hello World!" 返回到我在上一个示例中显示的示例中:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
// 添加单个终节点
app.MapGet("/", () => "Hello World!");
app.Run();
2
3
4
5
6
7
8
9
使用 MapGet() 添加终结点会将一个入口添加到 WebApplication 的 EndpointDataSource 集合中,这会导致 ConfigureApplication 自动添加其他中间件:
HostFilteringMiddlewareDeveloperExceptionPageMiddlewareEndpointRoutingMiddleware(又名RoutingMiddleware)WebApplication.ApplicationBuilder管道,包含HttpsRedirectionMiddlewareStaticFilesMiddleware
EndpointMiddleware
请注意,在 Program.cs 中定义的管道开始之前,RoutingMiddleware 会自动添加到中间件管道中,并且 EndpointMiddleware 会自动添加到管道末尾。生成的管道如下所示:

我发现非常有趣的是,WebApplication 对用户隐藏了路由中间件。RoutingMiddleware 和 EndpointMiddleware 密切相关,并且它们施加了严格的排序要求(例如,AuthorizationMiddleware 必须放在它们之间),这使得它们成为新手相对难以掌握的概念。使用 .NET 6 和 WebApplication,用户需要担心的事情更少了!
不过,这并不都是美好的。一些中间件通常假设它将在
UseRouting()之前被调用。例如,ExceptionHandlerMiddleware、RewriterMiddleware、StatusCodePagesMiddleware都必须更新才能处理这种新模式 (opens new window)。
但是,如果您需要 RoutingMiddleware 位于管道中的特定点,该怎么办?例如,也许您有中间件需要在RoutingMiddleware 之前?
# 具有 UseRouting() 的管道
在下一个示例中,我们通过调用 UseRouting() 将 EndpointRoutingMiddleware 显式添加到 WebApplication.ApplicationBuilder 管道中。这更接近于您希望在 .NET 3.x/5 应用中看到的内容,其中 UseRouting() 位于管道的中间。
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
// EndpointRoutingMiddleware 位于管道的中间
app.UseRouting();
app.MapGet("/", () => "Hello World!");
app.Run();
2
3
4
5
6
7
8
9
10
11
如您所料,生成的管道包含与以前相同的所有中间件,但排列顺序略有不同:
HostFilteringMiddlewareDeveloperExceptionPageMiddlewareWebApplication.ApplicationBuilder管道,包含HttpsRedirectionMiddlewareStaticFilesMiddlewareEndpointRoutingMiddleware(RoutingMiddleware)
EndpointMiddleware
这可以被可视化成这样:

在"主"中间件管道和 WebApplication.ApplicationBuilder 管道之间有一个有趣的相互作用 (opens new window),在指定 UseRouting() 时,WebApplicationBuilder 必须小心地保留整体顺序。希望这不会经常被需要,但是当真的需要时,如果您愿意,您仍然应该能够使用 WebApplicationBuilder,而不是被迫回退到通用主机。
最后,让我们看一下如果添加终结点路由的另一端终结点(EndpointMiddleware)会发生什么情况。
# 具有 UseEndpoints() 的管道
如前所述,终结点路由适用于一对中间件,即选择终结点的 EndpointRoutingMiddleware(又名 RoutingMiddleware)和执行终结点的 EndpointMiddleware。EndpointMiddleware 通常通过调用 UseEndpoints 添加到管道中,但使用 WebApplicationBuilder,它会自动添加。让我们看看如果我们也显式添加它会发生什么。
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapGet("/", () => "Hello World!");
// 显式添加终结点中间件,并添加一个新的终结点
app.UseEndpoints(x =>
{
x.MapGet("/ping", () => "pong")
});
app.Run();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
使用上述配置,您最终会得到以下内容:
HostFilteringMiddlewareDeveloperExceptionPageMiddlewareWebApplication.ApplicationBuilder管道,包含HttpsRedirectionMiddlewareStaticFilesMiddlewareEndpointRoutingMiddleware(RoutingMiddleware),和EndpointMiddleware
EndpointMiddleware
如果您仔细阅读上面的列表,您会发现终结点中间件出现了两次!这不是一个错误,而是 WebApplicationBuilder 无法判断 EndpointMiddleware 是否被添加到 WebApplication.ApplicationBuilder 管道中 (opens new window)的结果。幸运的是,"外部"中间件是完全良性的。

您可能还会注意到,我直接在 WebApplication 上以及在 UseEndpoints() 调用中注册了终结点。但是,这些终结点都已注册到 WebApplication 的 _dataSources,这要归功于 WebApplicationBuilder 中属性的一些诡计多端的摆弄 (opens new window)。最终结果是永远不会调用第二个中间件。
这涵盖了与在 WebApplication 中构建中间件管道相关的大多数边缘情况。我认为看到这里所做的更改非常有趣,尽管正如我之前提到的,我希望在 WebApplication 中的中间件和终结点之间有更好的划分。例如,我希望它看起来更像这样:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// I wish the "Endpoints" property was a thing
app.Endpoints.MapGet("/", () => "Hello World!");
app.Endpoints.MapGet("/ping", () => "pong");
app.Run();
2
3
4
5
6
7
8
9
10
11
12
13
但就目前而言,不可否认的是,WebApplication 和 WebApplicationBuilder 提供了比它们包装的通用主机实现更简单的API。希望这样可以更容易地教给新手!
# 总结
在这篇文章中,我们看到了新的"最小托管" WebApplication 如何为您的应用程序构建中间件管道。我们首先查看了 WebAppliation 的构造函数,以了解它的工作原理,然后查看了一些示例管道。对于每个管道,我都显示了您编写的代码以及生成的中间件管道。这显示了 WebApplication 如何自动(有条件地)在管道的开头添加 DeveloperExceptionPageMiddleware,并将管道包装在路由中间件中。总体而言,这可以简化中间件排序问题,但最好了解正在发生的事情。
实现最小托管 API 需要对中间件进行大量更改以适应新模式。在下一篇文章中,我们将介绍 .NET Core 中必须更改的其他一些内容!
作者:Gerry Ge
出处:翻译至 Building a middleware pipeline with WebApplication (opens new window)
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际 (opens new window)」许可协议进行许可。
转载请注明出处