第3章 使用中间件管道处理请求(ASP.NET Core in Action, 2nd Edition)

发布时间 2023-04-10 11:47:12作者: 码农小白修炼记

本章包括

  • 什么是中间件
  • 使用中间件服务静态文件
  • 使用中间件添加功能
  • 组合中间件以形成管道
  • 使用中间件处理异常和错误

在上一章中,您对完整的ASP.NET Core应用程序进行了一次短暂的参观,以了解组件如何结合在一起创建web应用程序。在本章中,我们将重点关注一个小部分:中间件管道。

在ASP.NET Core中,中间件是处理HTTP请求或响应的C#类或函数。它们链接在一起,一个中间件的输出作为下一个中间件输入,形成一个管道。

中间件管道是配置中最重要的部分之一,用于定义应用程序的行为方式及其响应请求的方式。了解如何构建和组合中间件是向应用程序添加功能的关键。

在本章中,您将了解什么是中间件以及如何使用它来创建管道。您将看到如何将多个中间件组件链接在一起,每个组件添加一个独立的功能。本章中的示例仅限于使用现有的中间件组件,并展示如何以正确的方式为应用程序安排它们。在第19章中,您将学习如何构建自己的中间件组件,并将它们合并到管道中。

我们将首先了解中间件的概念,您可以使用它实现的所有功能,以及中间件组件通常如何映射到“横切关注点”。这些是应用程序跨多个不同层的功能。日志记录、错误处理和安全性是典型的横切关注点,应用程序的许多不同部分都需要这些关注点。因为所有请求都通过中间件管道,所以它是配置和处理此功能的首选位置。

注意:关注点是指基于功能划分系统的一部分,部分关注点「横切」程序代码中的数个模块,即在多个模块中都有出现,它们即被称作「横切关注点」,这样说好像还是特别抽象?那我们举个例子:日志功能就是横切关注点的一个典型案例。日志功能往往横跨系统中的每个业务模块,即“横切”所有需要日志功能的类和方法体。所以我们说日志成为了横切整个系统对象结构的关注点 —— 也就叫做横切关注点啦。

在第3.2节中,我将解释如何将单个中间件组件组合到管道中。你将从一个只显示保留页面的web应用程序开始。从那里,您将学习如何构建一个简单的静态文件服务器,该服务器从磁盘上的文件夹中返回请求的文件。

接下来,您将转到包含多个中间件的更复杂的管道。在本例中,您将探讨中间件管道中排序的重要性,并了解当管道包含多个中间件时如何处理请求。

在第3.3节中,您将学习如何使用中间件来处理任何应用程序的一个重要方面:错误处理。对于所有应用程序来说,错误就是现实存在,因此在构建应用程序时,务必考虑到这些错误。除了确保应用程序在抛出异常或发生错误时不会中断外,还必须以用户友好的方式通知应用程序的用户发生了什么问题。

您可以用几种不同的方式处理错误,但它们是典型的横切关注点之一,中间件可以提供所需的功能。在第3.3节中,我将展示如何使用Microsoft提供的中间件处理异常和错误。特别是,您将了解三个不同的组件:

  • DeveloperExceptionPageMiddleware——在构建应用程序时提供快速错误反馈
  • ExceptionHandlerMiddleware——在生产中提供用户友好的通用错误页面
  • StatusCodePagesMiddleware——将原始错误状态代码转换为用户友好的错误页

通过组合这些中间件,您可以确保应用程序中发生的任何错误不会泄露安全细节,也不会破坏应用程序。最重要的是,他们将让用户更好地摆脱错误,给他们尽可能友好的体验。

在本章中,您将看不到如何构建自己的中间件,相反,您将看到,使用ASP.NET Core提供的组件可以做很多事情。一旦您了解了中间件管道及其行为,就更容易理解何时以及为什么需要定制中间件。考虑到这一点,让我们开始吧!

3.1 什么是中间件?

中间件一词在软件开发和IT中的各种上下文中都有使用,但它不是一个特别描述性的词——什么是中间件?

在ASP.NET Core中,中间件是可以处理HTTP请求或响应的C#类。中间件可以

  • 通过生成HTTP响应来处理传入的HTTP请求
  • 处理传入的HTTP请求,修改它,并将其传递给另一个中间件
  • 处理传出的HTTP响应,修改它,并将其传递给另一个中间件或ASP.NET Core web服务器

您可以在自己的应用程序中以多种方式使用中间件。例如,一个日志中间件可能会注意到请求何时到达,然后将其传递给另一个中间件。同时,图像调整中间件组件可能会发现具有指定大小的图像的传入请求,生成请求的图像,并将其发送回用户,而无需传递。

大多数ASP.NET Core应用程序中最重要的中间件是EndpointMiddleware类。这个类通常生成所有HTML页面和API响应(用于Web API应用程序),是本书大部分内容的重点。与图像调整中间件一样,它通常接收请求,生成响应,然后将其发送回用户,如图3.1所示。

图3.1 中间件管道示例。每个中间件处理请求并将其传递给管道中的下一个中间件。中间件生成响应后,会通过管道将响应传回。当它到达ASP.NET Core web服务器时,响应被发送到用户的浏览器。

定义:一个中间件可以调用另一个中间件,而另一个又可以调用其他中间件,依此类推,这种安排被称为管道。您可以将中间件的每一部分视为管道的一部分——当您连接所有部分时,一个请求将通过一部分流到下一部分。

中间件最常见的用例之一是应用程序的横切关注点。无论请求中的特定路径或请求的资源如何,应用程序的这些方面都需要针对每个请求发生。其中包括以下内容

  • 记录每个请求
  • 向响应中添加标准安全标头
  • 将请求与相关用户关联
  • 设置当前请求的语言

在每个示例中,中间件都会接收一个请求,对其进行修改,然后将该请求传递给管道中的下一个中间件。后续中间件可以使用早期中间件添加的细节以某种方式处理请求。例如,在图3.2中,身份验证中间件将请求与用户相关联。授权中间件使用此详细信息来验证用户是否有权向应用程序发出特定请求。

图3.2中间件组件修改请求以便稍后在管道中使用的示例。中间件也可以缩短管道,在请求到达稍后的中间件之前返回响应。

如果用户有权限,授权中间件将把请求传递给端点中间件,让它生成响应。如果用户没有权限,授权中间件可以缩短管道,直接生成响应。它在端点中间件看到请求之前将响应返回到先前的中间件。

从中可以得到的一个关键点是管道是双向的。请求在一个方向上通过管道,直到一个中间件生成响应,此时响应通过管道返回,第二次通过每个中间件,直到返回到第一个中间件。最后,第一个/最后一个中间件将把响应传回ASP.NET Core web服务器。

HttpContext对象
我在第2章中提到了HttpContext,它也在幕后。ASP.NET Core web服务器为每个请求构造一个HttpContext,ASP.NET Core应用程序将其用作单个请求的一种存储盒。特定于此特定请求和后续响应的任何内容都可以与之关联并存储在其中。这可能包括请求的属性、特定于请求的服务、已加载的数据或发生的错误。web服务器用原始HTTP请求的详细信息和其他配置详细信息填充初始HttpContext,并将其传递给应用程序的其余部分。
所有中间件都可以访问请求的HttpContext。例如,它可以使用它来确定请求是否包含任何用户凭据,识别请求试图访问的页面,以及获取任何已发布的数据。然后,它可以使用这些细节来确定如何处理请求。
应用程序处理完请求后,将使用适当的响应更新HttpContext,并通过中间件管道将其返回到web服务器。ASP.NET Core web服务器然后将表示转换为原始HTTP响应,并将其发送回反向代理,反向代理将其转发给用户的浏览器。

正如您在第2章中看到的,您在代码中定义中间件管道,作为Startup中初始应用程序配置的一部分。您可以根据自己的需要定制中间件管道。简单的应用程序可能只需要一个短管道,而具有多种功能的大型应用程序可能需要更多的中间件。中间件是应用程序中有趣的重要行为来源。最终,中间件管道负责响应它接收到的任何HTTP请求。

请求作为HttpContext对象传递到中间件管道。正如您在第2章中看到的,ASP.NET Core web服务器从传入的请求构建HttpContext对象,该对象在中间件管道中上下传递。当您使用现有的中间件构建管道时,这是一个很少需要处理的细节。但是,正如您将在本章的最后一节中看到的,它的幕后存在提供了对中间件管道施加额外控制的途径。

您还可以将中间件管道视为一系列同心组件,类似于传统的matryoshka(俄罗斯)玩偶,如图3.3所示。请求通过管道“前进”,深入中间件堆栈,直到返回响应。然后,响应通过中间件返回,以与请求相反的顺序传递它们。

图3.3 您还可以将中间件视为一系列嵌套组件,其中请求被发送到中间件的更深处,响应从中间件中重新出现。每个中间件都可以在将响应传递给下一个中间件之前执行逻辑,并且可以在创建响应之后,在返回堆栈的过程中执行逻辑。

中间件与HTTP模块和HTTP处理程序
在ASP.NET的早期版本中,没有使用中间件管道的概念。相反,您有HTTP模块和HTTP处理程序。
HTTP处理程序是响应请求并生成响应的进程。例如,ASP.NET页面处理程序响应.aspx页面的请求运行。或者,您可以编写一个自定义处理程序,在请求图像时返回调整大小的图像。
HTTP模块处理应用程序的横切问题,如安全性、日志记录或会话管理。当服务器接收到请求时,它们会响应请求所经过的生命周期事件而运行。事件的示例包括BeginRequest、AcquireRequestState和PostAcquireRequest State。
这种方法是可行的,但有时很难推断哪些模块将在哪个点运行。实现模块需要相对详细地了解每个生命周期事件的请求状态。
中间件管道使理解应用程序更加简单。管道完全在代码中定义,指定了哪些组件应该运行以及按什么顺序运行。在幕后,ASP.NET Core中的中间件管道是一个简单的方法调用链,每个中间件函数调用管道中的下一个。

这几乎就是中间件概念的全部内容。在下一节中,我将讨论如何组合中间件组件来创建应用程序,以及如何使用中间件来分离应用程序的关注点。

3.2 在管道中组合中间件

一般来说,每个中间件组件都有一个主要关注点。它只处理请求的一个方面。日志中间件只处理请求的日志记录,身份验证中间件只负责识别当前用户,静态文件中间件只负责返回静态文件。

这些关注点中的每一个都是高度集中的,这使得组件本身很小,易于推理。它还增加了应用程序的灵活性;添加静态文件中间件并不意味着您必须进行图像调整行为或身份验证。这些特性中的每一个都是一个额外的中间件。

要构建一个完整的应用程序,您需要将多个中间件组件组合到一个管道中,如前一节所示。每个中间件都可以访问原始请求,以及管道中先前中间件所做的任何更改。一旦生成了响应,每个中间件都可以在将响应发送给用户之前检查和/或修改通过管道返回的响应。这允许您从小型、集中的组件构建复杂的应用程序行为。

在本节的其余部分中,您将看到如何通过将小组件组合在一起来创建中间件管道。使用标准的中间件组件,您将学会创建一个保持页面,并从磁盘上的文件夹中提供静态文件。最后,您将再次查看您在第2章中构建的默认中间件管道,并对其进行分解,以了解其构建的原因。

3.2.1 简单管道场景1:等待页面

对于您的第一个应用程序和第一个中间件管道,您将学习如何创建由保留页面组成的应用程序。这在您首次设置应用程序时非常有用,以确保它处理请求时不会出错。

提示:记住,您可以在GitHub存储库中查看本书的应用程序代码https://github.com/andrewlock/asp-dot-net-core-in-action-2e.

在前面的章节中,我已经提到ASP.NET Core框架由许多小型的、独立的库组成。通常通过引用应用程序的.csproj项目文件中的包并在Startup类的Configure方法中配置中间件来添加中间件。微软提供了许多标准的ASP.NET Core中间件组件供您选择,您也可以使用NuGet和GitHub的第三方组件,或者您可以构建自己的自定义中间件。

注:我将在第19章讨论构建定制中间件。

不幸的是,没有一个明确的可用中间件列表,但您可以在主ASP.NET Core GitHub存储库中查看作为ASP.NET Core一部分的所有中间件的源代码(https://github.com/ASP.NET/ASP.NET Core). 您可以在src/middleware文件夹中找到大多数中间件,尽管有些中间件在其他文件夹中,它是更大功能的一部分。例如,身份验证和授权中间件可以在src/Security文件夹中找到。或者,打开搜索https://nuget.org您通常可以找到具有所需功能的中间件。

在本节中,您将看到如何创建一个最简单的中间件管道,仅由WelcomePageMiddleware组成。WelcomePageMiddleware旨在在您首次开发应用程序时快速提供示例页面,如图3.4所示。您不会在生产应用程序中使用它,因为您无法自定义输出,但它是一个独立的中间件组件,您可以使用它来确保应用程序正确运行。

图3.4 欢迎页面中间件响应。在任何路径上对应用程序的每个请求都将返回相同的欢迎页面响应。

提示:WelcomePageMiddleware是基础ASP.NET Core框架的一部分,因此您不需要添加对任何其他NuGet包的引用。

尽管这个应用程序很简单,但当应用程序接收到HTTP请求时,会发生与您之前看到的完全相同的过程,如图3.5所示。

图3.5 WelcomePageMiddleware处理请求。请求从反向代理传递到ASP.NET Core web服务器,最后传递到中间件管道,后者生成HTML响应。

与所有ASP.NET Core应用程序一样,将中间件添加到IApplicationBuilder对象,可以在Configure方法的Startup中定义中间件管道。要创建由单个中间件组件组成的第一个中间件管道,只需要一个方法调用。

清单3.1 欢迎页面中间件管道的启动

using Microsoft.ASP.NET Core.Builder; 

namespace CreatingAHoldingPage
{
    //对于这个基本应用程序,Startup类非常简单。
    public class Startup
    {
        //Configure方法用于定义中间件管道
        public void Configure(IApplicationBuilder app)
        {

            //管道中唯一的中间件 
            app.UseWelcomePage();
        }
    }
}

如您所见,此应用程序的Startup类非常简单。应用程序没有配置和服务,因此Startup没有构造函数或ConfigureServices方法。唯一需要的方法是Configure,在其中调用UseWelcomePage。

您通过调用IApplicationBuilder上的方法在ASP.NET Core中构建中间件管道,但此接口没有定义UseWelcomePage本身这样的方法。相反,这些是扩展方法。

使用扩展方法可以有效地向IApplicationBuilder类添加功能,同时保持它们的实现与IApplicationBuilder隔离。在后台,这些方法通常调用另一个扩展方法来将中间件添加到管道中。例如,在幕后,UseWelcomePage方法使用

UseMiddleware<WelcomePageMiddleware>();

这种为每个中间件创建一个扩展方法并使用Use开始方法名称的惯例旨在提高将中间件添加到应用程序时的可发现性。ASP.NET Core包含许多中间件作为核心框架的一部分,因此您可以在Visual Studio和其他IDE中使用IntelliSense查看所有可用的中间件,如图3.6所示。

图3.6 IntelliSense使查看所有可添加到中间件管道中的中间件变得容易。

调用UseWelcomePage方法将WelcomePageMiddleware添加为管道中的下一个中间件。尽管这里只使用了一个中间件组件,但重要的是要记住,在Configure中调用IApplicationBuilder的顺序定义了中间件在管道中运行的顺序。

警告:在向管道中添加中间件时,请务必小心,并考虑其运行顺序。组件只能访问由中间件创建的数据,该中间件在管道中位于组件之前。

这是最基本的应用程序,无论您导航到哪个URL,都会返回相同的响应,但这表明使用中间件定义应用程序行为是多么容易。现在,我们将使事情变得更有趣,并在您向不同路径发出请求时返回不同的响应。

3.2.2 简单管道场景2:处理静态文件

在本节中,我将向您展示如何创建可用于完整应用程序的最简单的中间件管道之一:静态文件应用程序。

大多数web应用程序,包括那些具有动态内容的应用程序,都使用静态文件提供大量页面。图像、JavaScript和CSS样式表通常在开发期间保存到磁盘,并在请求时提供,通常作为完整HTML页面请求的一部分。

现在,您将使用StaticFileMiddleware创建一个应用程序,该应用程序仅在请求时提供wwwroot文件夹中的静态文件,如图3.7所示。当您使用/moon.jpg路径请求文件时,它将被加载并作为请求的响应返回。

图3.7 使用静态文件中间件服务静态图像文件

如果用户请求的文件不存在于wwwroot文件夹中,例如missing.jpg,则静态文件中间件不会提供文件。相反,一个404 HTTP错误代码响应将被发送到用户的浏览器,浏览器将显示默认的“文件未找到”页面,如图3.8所示。

图3.8 当文件不存在时,将404返回到浏览器。wwwroot文件夹中不存在请求的文件,因此ASP.NET Core应用程序返回了404响应。浏览器(在本例中为Microsoft Edge)将向用户显示默认的“找不到文件”错误。

注意:此页面的外观取决于您的浏览器。在某些浏览器中,例如Internet Explorer(IE),您可能会看到一个完全空白的页面。

为该应用程序构建中间件管道很容易。它由一个中间件StaticFileMiddleware组成,如下面的列表所示。您不需要任何服务,因此只需在ConfigurewithUseStaticFiles中配置中间件管道即可。

清单3.2 静态文件中间件管道的启动

using Microsoft.ASP.NET Core.Builder;
namespace CreatingAStaticFileWebsite
{
    public class Startup    //对于这个基本的静态文件应用程序,Startup类非常简单。
    {
        public void Configure(IApplicationBuilder app)    //Configure方法用于定义中间件管道
        {
            app.UseStaticFiles();    //管道中唯一的中间件
        }
    }

提示:记住,您可以在GitHub存储库中查看本书的应用程序代码https://github.com/andrewlock/asp-dot-net-core-in-action-2e.

当应用程序接收到请求时,ASP.NET Core web服务器处理该请求并将其传递给中间件管道。StaticFileMiddleware接收请求并确定是否可以处理它。如果请求的文件存在,中间件将处理请求并将该文件作为响应返回,如图3.9所示。

图3.9 StaticFileMiddleware处理文件请求。中间件检查wwwroot文件夹以查看请求的moon.jpg文件是否存在。该文件存在,因此中间件检索该文件并将其作为响应返回给web服务器,最终返回给浏览器。

如果该文件不存在,则请求有效地通过静态文件中间件而不发生变化。但是等等,你只添加了一个中间件,对吗?如果没有另一个中间件,您肯定不能将请求传递给下一个中间件吗?

ASP.NET Core自动在管道末端添加一个“虚拟”中间件。如果调用该中间件,它总是返回404响应。

提示:请记住,如果没有中间件为请求生成响应,管道将自动向浏览器返回一个简单的404错误响应。

HTTP响应状态代码
每个HTTP响应都包含一个状态代码,以及描述状态代码的原因短语(可选)。状态码是HTTP协议的基础,是指示常见结果的标准化方式。例如,200响应表示请求已成功响应,而404响应表示找不到请求的资源。
状态代码总是三位数长,并根据第一位数分为五个不同的类别:
1xx——信息。不经常使用,提供了一个通用的确认。
2xx——成功。请求已成功处理和处理。
3xx——重定向。例如,浏览器必须遵循提供的链接,以允许用户登录。
4xx——客户端错误。请求有问题。例如,请求发送了无效数据或用户无权执行请求。
5xx——服务器错误。服务器上出现问题,导致请求失败。
这些状态代码通常驱动用户浏览器的行为。例如,浏览器将通过重定向到所提供的新链接并发出第二个请求来自动处理301响应,而无需用户交互。
4xx和5xx类中存在错误代码。常见的代码包括:找不到文件时的404响应,客户端发送无效数据(例如,无效电子邮件地址)时的400错误,以及服务器上发生错误时的500错误。错误代码的HTTP响应可能包含或不包含响应主体,该响应主体是客户端接收响应时要显示的内容。

这个基本的ASP.NET Core应用程序允许您轻松地查看ASP.NET Core中间件管道的行为,特别是静态文件中间件,但您的应用程序不太可能像这样简单。静态文件更有可能成为中间件管道的一部分。在下一节中,您将看到如何结合多个中间件,就像我们看一个简单的RazorPages应用程序一样。

3.2.3 简单管道场景3:Razor Pages应用程序

至此,您应该对中间件管道有一个很好的理解,因为它定义了应用程序的行为。在本节中,您将看到如何组合几个标准中间件组件以形成管道。与之前一样,这是在Configure方法的Startup中通过向IApplicationBuilder对象添加中间件来执行的。

首先,您将创建一个在典型的ASP.NET Core Razor Pages模板中找到的基本中间件管道,然后通过添加中间件来扩展它。导航到应用程序主页时的输出如图3.10所示——与第2章所示的示例应用程序相同。

图3.10 一个简单的Razor Pages应用程序。该应用程序只使用四个中间件:路由中间件用于选择要运行的RazorPage,端点中间件用于从RazorPage生成HTML,静态文件中间件用于服务CSS文件,以及异常处理中间件用于捕获任何错误。

创建这个应用程序只需要四个中间件:路由中间件来选择要执行的RazorPage,端点中间件来从RazorPage生成HTML,静态文件中间件来服务wwwroot文件夹中的CSS和图像文件,以及异常处理器中间件来处理可能发生的任何错误。

应用程序的中间件管道的配置一如既往地出现在Configure方法的Startup中,如下所示。除了中间件配置之外,此列表还显示了ConfigureServices中对AddRazorPages()的调用,这是使用RazorPage时所必需的。您将在第10章中了解有关服务配置的更多信息。

清单3.3 RazorPages应用程序的基本中间件管道

public class Startup
{
    public void ConfigureServices(IServiceCollection services
    {
        services.AddRazorPages();
    }
    public void Configure(IApplicationBuilder app)
    {
        app.UseExceptionHandler("/Error"); 
        app.UseStaticFiles(); 
        app.UseRouting(); 
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

您现在应该很熟悉向IApplicationBuilder添加中间件以形成管道,但本例中有几点值得注意。首先,所有添加中间件的方法都从Use开始。正如我前面提到的,这得益于使用扩展方法来扩展IApplicationBuilder功能的惯例;通过在方法前面加上Use,它们应该更容易发现。

关于此列表的另一个重要点是Configure方法中Use方法的顺序。将中间件添加到IApplicationBuilder对象的顺序就是它们添加到管道的顺序。这将创建一条类似于图3.11所示的管线。

图3.11清单3.3中示例应用程序的中间件管道。将中间件添加到IApplicationBuilder的顺序定义了管道中中间件的顺序。

首先调用异常处理程序中间件,然后将请求传递给静态文件中间件。如果请求对应于文件,则静态文件处理程序将生成响应;否则它将把请求传递给路由中间件。路由中间件根据请求URL选择RazorPage,端点中间件执行所选的RazorPage。如果没有RazorPage可以处理请求的URL,自动虚拟中间件将返回404响应。

注意:在ASP.NET Core的1.x和2.x版本中,路由和端点中间件被合并为一个“MVC”中间件。将路由的责任与执行分开,可以在路由和端点中间件之间插入中间件。我将在第5章中进一步讨论路由。

当您有两个中间件都在侦听同一路径时,排序的影响最明显。例如,示例管道中的端点中间件当前通过生成如图3.10所示的HTML响应来响应对应用程序主页的请求(使用“/”路径)。图3.12显示了如果您重新引入之前看到的中间件WelcomePageMiddleware,并将其配置为也响应“/”的路径会发生什么。

图3.12 欢迎页面中间件响应。欢迎页面中间件位于端点中间件之前,因此对主页的请求返回欢迎页面中间件,而不是RazorPages响应。

正如您在第3.2.1节中看到的,WelcomePageMiddleware被设计为返回一个固定的HTML响应,因此您不会在生产应用程序中使用它,但它很好地说明了这一点。在下面的列表中,它被添加到中间件管道的开头,并被配置为只响应“/”路径。

清单3.4 向管道中添加WelcomePageMiddleware

public class Startup
{
    public void ConfigureServices(IServiceCollection services
    {
        services.AddRazorPages();
    }
    public void Configure(IApplicationBuilder app)
    {
        app.UseWelcomePage("/");     //WelcomePageMiddleware处理对“/”路径的所有请求,并返回一个示例HTML响应。
        app.UseExceptionHandler("/Error"); 
        app.UseStaticFiles(); 
        app.UseRouting(); 
        app.UseEndpoints(endpoints =>    //对“/”的请求永远不会到达端点中间件。
        {
            endpoints.MapRazorPages();
        });
    }
}

尽管您知道端点中间件也可以处理“/”路径,但WelcomePageMiddleware在管道中较早,因此当它接收到对“/”的请求时,它会返回一个响应,从而使管道短路,如图3.13所示。管道中的其他中间件都没有为请求运行,因此没有一个中间件有机会生成响应。

图3.13 应用程序处理“/”路径请求的概述。欢迎页面中间件是中间件管道中的第一个,因此它在任何其他中间件之前接收请求。它生成HTML响应,缩短了管道。没有其他中间件为请求运行。

如果在调用UseEndpoints之后将WelcomePageMiddleware移动到管道的末尾,则会出现相反的情况。任何对“/”的请求都将由端点中间件处理,您永远不会看到欢迎页面。

提示:在将中间件添加到Configure方法时,应始终考虑中间件的顺序。在管道中较早添加的中间件将在稍后添加中间件之前运行(并可能返回响应)。

到目前为止所示的所有示例都试图处理传入请求并生成响应,但重要的是要记住中间件管道是双向的。每个中间件组件都有机会处理传入请求和传出响应。中间件的顺序对于创建或修改传出响应的组件来说是最重要的。

在前面的示例中,我在应用程序的中间件管道的开头包含ExceptionHandlerMiddleware,但它似乎没有做任何事情。错误处理中间件的特点是在传入请求到达管道时忽略它,而是检查传出响应,只在发生错误时修改它。在下一节中,我将详细介绍可用于应用程序的错误处理中间件的类型以及何时使用它们。

3.3 使用中间件处理错误

在开发应用程序时,错误是现实。即使您编写了完美的代码,只要您发布并部署应用程序,无论是意外还是有意,用户都会找到破解它的方法!重要的是,您的应用程序可以优雅地处理这些错误,为用户提供适当的响应,并且不会导致整个应用程序失败。

ASP.NET Core的设计理念是每个功能都可以选择。因此,由于错误处理是一项功能,您需要在应用程序中显式启用它。应用程序中可能会出现许多不同类型的错误,处理这些错误的方法也多种多样,但在本节中,我将重点介绍两种:异常和错误状态代码。

当您发现意外情况时,通常会发生异常。毫无疑问,你以前会遇到过一个典型的(非常令人沮丧的)异常,那就是NullReferenceException,当你试图访问一个尚未初始化的对象时,会抛出该异常。如果中间件组件中发生异常,它会在管道中传播,如图3.14所示。如果管道不处理该异常,web服务器将向用户返回500状态代码。

图3.14 端点中间件中的异常通过管道传播。如果该异常在管道的早期未被中间件捕获,则会向用户的浏览器发送一个500“服务器错误”状态代码。

在某些情况下,错误不会导致异常。相反,中间件可能会生成错误状态代码。其中一种情况是未处理请求的路径。在这种情况下,管道将返回一个404错误,这将导致向用户显示一个通用的、不友好的页面,如图3.8所示。虽然这种行为“正确”,但它并不能为应用程序的用户提供良好的体验。

错误处理中间件试图通过在应用程序将响应返回给用户之前修改响应来解决这些问题。通常,错误处理中间件要么返回发生错误的详细信息,要么向用户返回一个通用但友好的HTML页面。您应该始终将错误处理中间件放置在中间件管道的早期,以确保它能够捕获后续中间件中生成的任何错误,如图3.15所示。不能拦截管道中早于错误处理中间件的中间件生成的任何响应。

图3.15 错误处理中间件应放置在管道的早期,以捕获原始状态代码错误。在第一种情况下,错误处理中间件位于图像调整中间件之前,因此它可以用用户友好的错误页面替换原始状态代码错误。在第二种情况下,错误处理中间件放置在图像调整中间件之后,因此无法修改原始错误状态代码。

本节的其余部分显示了可在应用程序中使用的几种类型的错误处理中间件。它们是作为基础ASP.NET Core框架的一部分提供的,因此您不需要引用任何额外的NuGet包来使用它们。

3.3.1 查看开发中的异常:DeveloperExceptionPage

在开发应用程序时,当应用程序中发生错误时,通常需要访问尽可能多的信息。因此,Microsoft提供了DeveloperExceptionPageMiddleware,可以使用

app.UseDeveloperExceptionPage();

当一个异常被抛出并通过管道传播到这个中间件时,它将被捕获。然后,中间件生成一个友好的HTML页面,并返回一个500状态代码给用户,如图3.16所示。该页面包含有关请求和异常的各种详细信息,包括异常堆栈跟踪、异常发生行的源代码以及请求的详细信息,如已发送的任何cookie或标头。

图3.16 开发人员异常页面显示了在请求过程中发生异常时的详细信息。默认情况下,会显示导致异常的代码中的位置、源代码行本身以及堆栈跟踪。您还可以单击“查询”、“Cookie”、“标头”或“路由”按钮,以显示有关导致异常的请求的详细信息。

在发生错误时提供这些详细信息对于调试问题非常有用,但如果使用不当,它们也会带来安全风险。您不应该向用户返回非必要的应用程序详细信息,因此在开发应用程序时,您应该只使用DeveloperExceptionPage。线索就在名字里!

警告:在生产中运行时,切勿使用开发人员异常页面。这样做存在安全风险,因为它可能会公开泄露应用程序代码的详细信息,使您成为攻击者的目标。

如果开发人员异常页面不适合生产使用,您应该使用什么?幸运的是,还有一个通用错误处理中间件可以在生产中使用。这是您已经看到并使用过的:ExceptionHandlerMiddleware。

3.3.2 处理生产中的异常:ExceptionHandlerMiddleware

开发人员异常页面在开发应用程序时很方便,但您不应该在生产中使用它,因为它会将有关应用程序的信息泄露给潜在攻击者。不过,您仍然希望捕捉错误;否则,用户将看到不友好的错误页面或空白页面,具体取决于他们使用的浏览器。

可以使用ExceptionHandlerMiddleware解决此问题。如果应用程序中发生错误,用户将看到一个与应用程序其他部分一致的自定义错误页面,但该页面仅提供有关错误的必要详细信息。例如,自定义错误页面(如图3.17所示)可以通过使用相同的标题,显示当前登录的用户,并向用户显示适当的消息,而不是异常的全部细节,保持应用程序的外观一致。

图3.17 ExceptionHandlerMiddleware创建的自定义错误页面。通过重用页眉和页脚等元素,自定义错误页面可以保持与应用程序其余部分相同的外观。更重要的是,您可以轻松控制向用户显示的错误详细信息。

如果您要查看几乎所有ASP.NET Core应用程序的Configure方法,您几乎肯定会发现与ExceptionHandlerMiddleware结合使用的开发人员异常页面与清单3.5所示的方式类似。

//只有在开发模式下运行时才能使用开发人员异常页面。 } else { app.UseExceptionHandler("/Error"); //在生产中,ExceptionHandlerMiddleware将添加到管道中。 } // additional middleware configuration }

除了演示如何将ExceptionHandlerMiddleware添加到中间件管道之外,此列表还显示了在应用程序启动时根据环境配置不同的中间件管道是完全可以接受的。您还可以根据其他值(例如从配置加载的设置)更改管道。

注:您将在第11章中看到如何使用配置值来定制中间件管道。

在将ExceptionHandlerMiddleware添加到应用程序时,通常会提供将向用户显示的自定义错误页面的路径。在清单3.5的示例中,您使用了“/error”的错误处理路径:

app.UseExceptionHandler("/Error");

ExceptionHandlerMiddleware将在捕获异常后调用此路径,以生成最终响应。动态生成响应的能力是ExceptionHandlerMiddleware的一个关键特性,它允许您重新执行中间件管道,以便生成发送给用户的响应。

图3.18显示了ExceptionHandlerMiddleware处理异常时发生的情况。它显示了当向“/”路径发出请求时Index.html Razor Page生成异常时的事件流。最终的响应返回错误状态代码,但也提供HTML响应以使用“/error”路径显示给用户。

图3.18 ExceptionHandlerMiddleware处理异常以生成HTML响应。对/path的请求会生成异常,由中间件处理。使用/Error路径重新执行管道以生成HTML响应。

注意:除了将ExceptionHandler-Middleware添加到应用程序中并配置有效的错误处理路径以允许重新执行管道之外,您不需要做任何其他事情。中间件将捕捉异常并为您重新执行管道。后续的中间件会将重新执行视为一个新的请求,但管道中的先前中间件不会察觉到任何异常情况。

ExceptionHandlerMiddleware之后中间件管道中发生异常时的事件序列如下:

  1. 中间件引发异常。
  2. ExceptionHandlerMiddleware捕获异常。
  3. 清除已定义的任何部分响应。
  4. 中间件使用提供的错误处理路径覆盖请求路径。
  5. 中间件将请求发送回管道,就像原始请求是针对错误处理路径一样。
  6. 中间件管道正常生成新的响应。
  7. 当响应返回ExceptionHandlerMiddleware时,它将状态代码修改为500错误,并继续通过管道将响应传递到web服务器。

重新执行管道带来的主要优点是能够将错误消息集成到正常的站点布局中,如图3.17所示。当发生错误时,当然可以返回一个固定的响应,但您无法在菜单栏中动态生成链接或在菜单中显示当前用户的名称。通过重新执行管道,您可以确保应用程序的所有动态区域都正确集成,就像该页面是站点的标准页面一样。

重新执行中间件管道是保持web应用程序中错误页面一致性的一种好方法,但有一些陷阱需要注意。首先,如果响应尚未发送到客户端,中间件只能修改管道下游生成的响应。例如,如果ASP.NET Core向客户端发送静态文件时发生错误,这可能是一个问题。在这种情况下,如果已经开始发送字节,错误处理中间件将无法运行,因为它无法重置响应。一般来说,在这个问题上你可以做的不多,但这是需要注意的。

当错误处理路径在重新执行管道期间抛出错误时,会出现更常见的问题。想象一下,在页面顶部生成菜单的代码中有一个bug:

  1. 当用户到达您的主页时,用于生成菜单栏的代码抛出异常。
  2. 异常在中间件管道中传播。
  3. 到达时,ExceptionHandlerMiddleware将捕获它,并使用错误处理路径重新执行管道。
  4. 当错误页面执行时,它会尝试为应用程序生成菜单栏,这会再次引发异常。
  5. 异常在中间件管道中传播。
  6. ExceptionHandlerMiddleware已经尝试拦截请求,因此它会让错误一直传播到中间件管道的顶部。
  7. web服务器返回一个原始的500错误,就好像根本没有错误处理中间件一样。

由于这个问题,通常最好使错误处理页面尽可能简单,以减少发生错误的可能性。

警告:如果错误处理路径生成错误,则用户将看到一般浏览器错误。通常情况下,最好使用一个始终有效的静态错误页面,而不是一个可能引发更多错误的动态页面。

ExceptionHandlerMiddleware和DeveloperExceptionPageMiddleware非常适合捕捉应用程序中的异常,但异常并不是您会遇到的唯一错误。在某些情况下,中间件管道将在响应中返回HTTP错误状态代码。处理异常和错误状态代码以提供一致的用户体验非常重要。

3.3.3 处理其他错误:StatusCodePagesMiddleware

应用程序可以返回大量HTTP状态代码,这些代码指示某种错误状态。您已经看到,当发生异常且未处理时,会发送500“服务器错误”,而当任何中间件未处理URL时,会发出404个找不到文件”错误。404错误尤其常见,通常发生在用户输入无效URL时。

提示:除了指示完全未处理的URL外,404错误通常用于指示未找到特定的请求对象。例如,如果不存在ID为23的产品,则对该产品的详细信息的请求可能会返回404。

如果您不处理这些状态代码,用户将看到一个通用错误页面,如图3.19所示,这可能会让许多人感到困惑,认为您的应用程序已损坏。更好的方法是处理这些错误代码并返回一个与应用程序其余部分保持一致的错误页面,或者至少不会使应用程序看起来损坏。

图3.19 一般浏览器错误页面。如果中间件管道无法处理请求,它将向用户返回404错误。该消息对用户的用处有限,可能会让许多人感到困惑或认为您的web应用程序已损坏。

Microsoft提供StatusCodePagesMiddleware来处理此用例。与所有错误处理中间件一样,您应该尽早将其添加到中间件管道中,因为它只处理后期中间件组件生成的错误。

您可以在应用程序中以多种不同的方式使用中间件。最简单的方法是使用

app.UseStatusCodePages();

使用此方法,中间件将拦截任何HTTP状态代码以4xx或5xx开头且没有响应主体的响应。对于最简单的情况,如果不提供任何附加配置,中间件将添加一个纯文本响应主体,指示响应的类型和名称,如图3.20所示。

图3.20 404错误的状态代码错误页面。您通常不会在生产中使用此版本的中间件,因为它不能提供良好的用户体验,但它表明错误代码被正确拦截。

这一点可以说比默认消息更糟糕,但它是为用户提供更一致体验的起点。

在生产中使用StatusCodePagesMiddleware的一种更典型的方法是在捕获错误时重新执行管道,使用与ExceptionHandlerMiddleware类似的技术。这允许您拥有适合应用程序其余部分的动态错误页面。要使用此技术,请使用以下扩展方法替换对UseStatusCodePages的调用。

app.UseStatusCodePagesWithReExecute("/{0}");

此扩展方法将StatusCodePagesMiddleware配置为在找到4xx或5xx响应代码时,使用提供的错误处理路径重新执行管道。这类似于ExceptionHandlerMiddleware重新执行管道的方式,如图3.21所示。

图3.21 StatusCodePagesMiddleware正在重新执行管道以生成404响应的HTML主体。对/path的请求返回404响应,该响应由状态代码中间件处理。使用/404路径重新执行管道以生成HTML响应。

请注意,错误处理路径"/{0}"包含格式字符串标记{0}。当路径被重新执行时,中间件将用状态代码号替换此令牌。例如,404错误将重新执行/404路径。路径的处理程序(通常是RazorPage)可以访问状态代码,并且可以根据状态代码定制响应。您可以选择任何错误处理路径,只要您的应用程序知道如何处理它。

注:您将在第5章中了解路由如何将请求路径映射到Razor Pages。

使用这种方法,您可以为不同的错误代码创建不同的错误页面,如图3.22所示的404特定错误页面。这种技术确保您的错误页面与应用程序的其他部分(包括任何动态生成的内容)保持一致,同时还允许您针对常见错误定制消息。

图3.22 缺少文件的错误状态代码页。当检测到错误代码(在本例中为404错误)时,将重新执行中间件管道以生成响应。这允许网页的动态部分在错误页面上保持一致。

警告:如前所述,在重新执行管道时,必须小心错误处理路径不会生成任何错误。

您可以将StatusCodePagesMiddleware与其他异常处理中间件结合使用,方法是将两者都添加到管道中。StatusCodePagesMiddleware仅在未写入响应正文时修改响应。因此,如果另一个组件(如ExceptionHandlerMiddleware)返回消息体和错误代码,则不会对其进行修改。

注意:StatusCodePagesMiddleware具有额外的重载,允许您在发生错误时执行自定义中间件,而不是重新执行RazorPages路径。

在开发任何web应用程序时,错误处理都是必不可少的;错误发生时,您需要优雅地处理它们。但根据您的应用程序,您可能并不总是希望错误处理中间件生成HTML页面。

3.3.4 错误处理中间件和Web API

ASP.NET Core不仅适用于创建面向用户的web应用程序,还适用于创建HTTP服务,当运行客户端单页应用程序时,可以从其他服务器应用程序、移动应用程序或用户浏览器访问这些服务。在所有这些情况下,您可能不会向客户端返回HTML,而是返回XML或JSON。

在这种情况下,如果发生错误,您可能不想返回一个大的HTML页面,说:“哎呀,出了问题。”将HTML页面返回到期望JSON的应用程序可能很容易意外地破坏它。相反,HTTP 500状态代码和描述错误的JSON主体对消费应用程序更有用。幸运的是,ASP.NET Core允许您在创建WebAPI控制器时完全这样做。

注:我在第4章中讨论了MVC和WebAPI控制器。我在第9章中详细讨论了WebAPI和错误处理。

这让我们暂时结束了ASP.NET Core中的中间件。您已经了解了如何使用和组合中间件来形成管道,以及如何处理应用程序中的错误。当您开始构建第一个ASP.NET Core应用程序时,这将为您带来很大的帮助。稍后,您将学习如何构建自己的定制中间件,以及如何在中间件管道上执行复杂操作,例如响应于特定请求将其分开处理。

在下一章中,您将更深入地了解Razor Pages,以及它们如何用于构建网站。您还将了解MVC设计模式,它与ASP.NET Core中RazorPages的关系,以及何时选择一种方法而不是另一种方法。

总结

  • 中间件的作用类似于ASP.NET中的HTTP模块和处理程序,但更易于推理。
  • 中间件由一个管道组成,一个中间件的输出传递到下一个中间件输入。
  • 中间件管道是双向的:请求在传入时通过每个中间件,响应在传出时以相反的顺序返回。
  • 中间件可以通过处理请求并返回响应来缩短管道,也可以将请求传递给管道中的下一个中间件。
  • 中间件可以通过向HttpContext对象添加数据或更改HttpContext对象来修改请求。
  • 如果早期的中间件使管道短路,则不是所有中间件都会对所有请求执行。
  • 如果没有处理请求,中间件管道将返回404状态代码。
  • 中间件添加到IApplicationBuilder的顺序定义了中间件在管道中的执行顺序。
  • 只要没有发送响应的头,就可以重新执行中间件管道。
  • 当它被添加到中间件管道中时,StaticFileMiddleware将提供在应用程序的wwwroot文件夹中找到的任何请求文件。
  • DeveloperExceptionPageMiddleware在开发应用程序时提供了许多有关错误的信息,但决不能在生产中使用。
  • ExceptionHandlerMiddleware允许您在管道中发生异常时提供用户友好的自定义错误处理消息。它在生产中使用是安全的,因为它不会暴露应用程序的敏感细节。
  • StatusCodePagesMiddleware允许您在管道返回原始错误响应状态代码时提供用户友好的自定义错误处理消息。
  • Microsoft提供了一些常见的中间件,NuGet和GitHub上有许多第三方选项。

如需导航到本书的其他章节,请点击这里...