使用 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
此设置是最基本的配置,并生成一个中间件管道,其中包含:
HostFilteringMiddleware
DeveloperExceptionPageMiddleware
- 从
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
管道现在包含两个额外的中间件
HostFilteringMiddleware
DeveloperExceptionPageMiddleware
WebApplication.ApplicationBuilder
管道,包含HttpsRedirectionMiddleware
StaticFilesMiddleware
所以现在我们有一个看起来像这样的东西:
我们仍然没有任何终结点,因此在下一个示例中,我们实现了一个 "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
自动添加其他中间件:
HostFilteringMiddleware
DeveloperExceptionPageMiddleware
EndpointRoutingMiddleware
(又名RoutingMiddleware
)WebApplication.ApplicationBuilder
管道,包含HttpsRedirectionMiddleware
StaticFilesMiddleware
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
如您所料,生成的管道包含与以前相同的所有中间件,但排列顺序略有不同:
HostFilteringMiddleware
DeveloperExceptionPageMiddleware
WebApplication.ApplicationBuilder
管道,包含HttpsRedirectionMiddleware
StaticFilesMiddleware
EndpointRoutingMiddleware
(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
使用上述配置,您最终会得到以下内容:
HostFilteringMiddleware
DeveloperExceptionPageMiddleware
WebApplication.ApplicationBuilder
管道,包含HttpsRedirectionMiddleware
StaticFilesMiddleware
EndpointRoutingMiddleware
(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)」许可协议进行许可。
转载请注明出处