使用 WebApplication 构建中间件管道

3/11/2022 .NET6

这是该系列的第四篇文章:探索 .NET 6

我之前的文章中,我们查看了 WebApplicationBuilder 背后的代码,包括它的一些帮助器类,如 ConservationHostBuilderBootstrapHostBuilder。在文章的最后,我们创建了一个 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;
    }
    // ...
}
1
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 主要将 IHostIApplicationBuilderIEndpointRouteBuilder 的实现委托给其私有属性,并显式实现它们。例如,对于 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;
}
1
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)

Sequence diagram for Host.StartAsync()

如上图所示,Host 运行在应用程序中注册的 IHostedService,作为启动序列的一部分。从 ASP.NET Core 3.x 中的"伟大的重新平台化"开始,Web 服务器也作为 IHostedService 运行,所以这是构建中间件管道并开始侦听的时候。

GenericWebHostService 是构建中间件管道的地方,最后传递给 Kestrel 以运行您的应用程序。与通用主机相比,WebApplication 在此过程中没有什么不同,因此,我们将介绍WebApplication 如何设置中间件管道,而不是深入研究(广泛的!)代码。

# WebApplication 的中间件管道

与通用主机相比,WebApplication 的一大区别是 WebApplication 默认设置各种中间件。在上一篇文章中,我展示了 WebApplicationBuilder 调用GenericHostBuilderExtensions.ConfigureWebHostDefaults()。这也通常与通用主机一起使用,并设置了几个默认值:

除此之外,WebApplicationBuilder 还设置了额外的中间件。这就是我在上一篇文章中提到的配置应用程序方法的用武之地。根据您配置 WebApplication 的方式,ConfigureApplication 会向管道添加其他中间件。

此代码有点复杂,因为它需要处理多个边缘情况。与其尝试过于详细地跟踪代码,不如看一些示例,看看生成的中间件管道是什么样子的。

# 空管道

我们将从最基本(且有些无意义)的应用程序开始,其中我们不会向应用程序添加任何额外的中间件或终结点:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();

app.Run();
1
2
3
4

此设置是最基本的配置,并生成一个中间件管道,其中包含:

  • HostFilteringMiddleware
  • DeveloperExceptionPageMiddleware
  • WebApplicationApplicationBuilder 构建的"空"中间件。

如下所示:

The WebApplication adds the DeveloperExceptionPageMiddleware by default

HostFilteringMiddleware 是由于对 ConfigureWebHostDefaults() 的隐藏调用而添加的,如上一节所述。未添加 ForwardedHeadersMiddleware,因为未添加环境变量。

现在,当您在开发环境中运行时,会自动添加 DeveloperExceptionPage,并将以下内容添加到每个中间件管道的开头:

if (context.HostingEnvironment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
1
2
3
4

管道中的最后一块中间件是使用直接添加到 Program.cs 中的 WebApplicationApplicationBuilder 的中间件构建的。在此示例中,我们没有添加任何内容,因此我们实质上是在中间件内部添加了一个"空"管道。

显然,仅具有默认中间件的应用程序没有多大用处,因此让我们做一些基本的并向WebApplication 添加一些中间件。

# 具有额外中间件的管道

在此示例中,我们将一些其他中间件添加到管道中:静态文件中间件和 HTTPS 重定向中间件:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();

// 添加一些额外的中间件
app.UseHttpsRedirection();
app.UseStaticFiles();

app.Run();
1
2
3
4
5
6
7
8

通过此设置,我们仍然拥有与上一节中相同的三核中间件,但 WebApplication.ApplicationBuilder 管道现在包含两个额外的中间件

  • HostFilteringMiddleware
  • DeveloperExceptionPageMiddleware
  • WebApplication.ApplicationBuilder 管道,包含
    • HttpsRedirectionMiddleware
    • StaticFilesMiddleware

所以现在我们有一个看起来像这样的东西:

The WebApplication.ApplicationBuilder pipeline includes the middleware added directly to the WebApplication

我们仍然没有任何终结点,因此在下一个示例中,我们实现了一个 "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();
1
2
3
4
5
6
7
8
9

使用 MapGet() 添加终结点会将一个入口添加到 WebApplicationEndpointDataSource 集合中,这会导致 ConfigureApplication 自动添加其他中间件:

  • HostFilteringMiddleware

  • DeveloperExceptionPageMiddleware

  • EndpointRoutingMiddleware (又名 RoutingMiddleware

  • WebApplication.ApplicationBuilder 管道,包含

    • HttpsRedirectionMiddleware
    • StaticFilesMiddleware
  • EndpointMiddleware

请注意,在 Program.cs 中定义的管道开始之前,RoutingMiddleware 会自动添加到中间件管道中,并且 EndpointMiddleware 会自动添加到管道末尾。生成的管道如下所示:

The WebApplication has added the the RoutingMiddleware and EndpointMiddleware to the pipeline

我发现非常有趣的是,WebApplication 对用户隐藏了路由中间件。RoutingMiddlewareEndpointMiddleware 密切相关,并且它们施加了严格的排序要求(例如,AuthorizationMiddleware 必须放在它们之间),这使得它们成为新手相对难以掌握的概念。使用 .NET 6 和 WebApplication,用户需要担心的事情更少了!

不过,这并不都是美好的。一些中间件通常假设它将在 UseRouting() 之前被调用。例如,ExceptionHandlerMiddlewareRewriterMiddlewareStatusCodePagesMiddleware 都必须更新才能处理这种新模式 (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();
1
2
3
4
5
6
7
8
9
10
11

如您所料,生成的管道包含与以前相同的所有中间件,但排列顺序略有不同:

  • HostFilteringMiddleware

  • DeveloperExceptionPageMiddleware

  • WebApplication.ApplicationBuilder 管道,包含

    • HttpsRedirectionMiddleware
    • StaticFilesMiddleware
    • EndpointRoutingMiddlewareRoutingMiddleware
  • EndpointMiddleware

这可以被可视化成这样:

The middleware pipeline contains the RoutingMiddleware in the WebApplication.ApplicationBuilder

在"主"中间件管道和 WebApplication.ApplicationBuilder 管道之间有一个有趣的相互作用 (opens new window),在指定 UseRouting() 时,WebApplicationBuilder 必须小心地保留整体顺序。希望这不会经常被需要,但是当真的需要时,如果您愿意,您仍然应该能够使用 WebApplicationBuilder,而不是被迫回退到通用主机。

最后,让我们看一下如果添加终结点路由的另一端终结点(EndpointMiddleware)会发生什么情况。

# 具有 UseEndpoints() 的管道

如前所述,终结点路由适用于一对中间件,即选择终结点的 EndpointRoutingMiddleware(又名 RoutingMiddleware)和执行终结点的 EndpointMiddlewareEndpointMiddleware 通常通过调用 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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

使用上述配置,您最终会得到以下内容:

  • HostFilteringMiddleware

  • DeveloperExceptionPageMiddleware

  • WebApplication.ApplicationBuilder 管道,包含

    • HttpsRedirectionMiddleware
    • StaticFilesMiddleware
    • EndpointRoutingMiddlewareRoutingMiddleware),和
    • EndpointMiddleware
  • EndpointMiddleware

如果您仔细阅读上面的列表,您会发现终结点中间件出现了两次!这不是一个错误,而是 WebApplicationBuilder 无法判断 EndpointMiddleware 是否被添加到 WebApplication.ApplicationBuilder 管道中 (opens new window)的结果。幸运的是,"外部"中间件是完全良性的。

The middleware pipeline contains two instances of the EndpointMiddleware

您可能还会注意到,我直接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();
1
2
3
4
5
6
7
8
9
10
11
12
13

但就目前而言,不可否认的是,WebApplicationWebApplicationBuilder 提供了比它们包装的通用主机实现更简单的API。希望这样可以更容易地教给新手!

# 总结

在这篇文章中,我们看到了新的"最小托管" WebApplication 如何为您的应用程序构建中间件管道。我们首先查看了 WebAppliation 的构造函数,以了解它的工作原理,然后查看了一些示例管道。对于每个管道,我都显示了您编写的代码以及生成的中间件管道。这显示了 WebApplication 如何自动(有条件地)在管道的开头添加 DeveloperExceptionPageMiddleware,并将管道包装在路由中间件中。总体而言,这可以简化中间件排序问题,但最好了解正在发生的事情。

实现最小托管 API 需要对中间件进行大量更改以适应新模式。在下一篇文章中,我们将介绍 .NET Core 中必须更改的其他一些内容!




作者:Gerry Ge

出处:翻译至 Building a middleware pipeline with WebApplication (opens new window)

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际 (opens new window)」许可协议进行许可。

转载请注明出处

Last Updated: 3/16/2022, 8:52:03 AM