第13章 MVC和Razor Pages过滤器管道(ASP.NET Core in Action, 2nd Edition)

2023-03-07,,

本章包括

过滤器管道及其与中间件的区别
创建自定义筛选器以重构复杂的操作方法
使用授权筛选器保护您的操作方法和Razor页面
短路筛选器管道以绕过操作和页面处理程序执行
将依赖项注入筛选器

在第1部分中,我详细介绍了ASPNETCore的MVC和RazorPages框架。您了解了如何使用路由来选择要执行的操作方法或RazorPage。您还看到了模型绑定、验证,以及如何通过从操作和页面处理程序返回IActionResult来生成响应。在本章中,我将深入了解MVC/RazorPages框架,并查看过滤器管道,有时称为操作调用管道。

MVC和RazorPages使用几个内置过滤器来处理横切问题,例如授权(控制哪些用户可以访问应用程序中的哪些操作方法和页面)。任何具有用户概念的应用程序都将至少使用授权过滤器,但过滤器比这个单一用例更强大。

本章主要在API控制器请求的上下文中描述过滤器管道。您将学习如何创建可以在自己的应用程序中使用的自定义筛选器,以及如何使用它们来减少操作方法中的重复代码。您将学习如何为特定操作自定义应用程序的行为,以及如何全局应用筛选器以修改应用程序中的所有操作。

您还将了解过滤器管道如何应用于Razor Pages。RazorPages过滤器管道与MVC/API控制器过滤器管道几乎相同,因此我们将重点关注它的不同之处。您将看到如何在RazorPages中使用页面过滤器,并了解它们与动作过滤器的区别。

将过滤器管道视为在MVC和RazorPages框架内运行的小型中间件管道。与ASPNETCore中的中间件管道一样,过滤器管道由一系列以管道形式连接的组件组成,因此一个过滤器的输出会输入到下一个过滤器。

本章首先讨论过滤器和中间件之间的相似性和差异,以及何时应该选择一个而不是另一个。您将了解所有不同类型的过滤器,以及它们如何结合起来为到达MVC或RazorPages框架的请求创建过滤器管道。

在第13.2节中,我将详细介绍每种过滤器类型,它们如何适合MVC管道,以及它们的用途。对于每一个,我将提供您可能在自己的应用程序中使用的示例实现。

过滤器的一个关键特性是能够通过生成响应并停止通过过滤器管道的进程来缩短请求。这与中间件中的短路工作方式相似,但有细微的差别。最重要的是,每个过滤器的确切行为略有不同,我将在第13.3节中介绍这一点。

通常,通过将过滤器实现为添加到控制器类、操作方法和RazorPages的属性,可以将它们添加到管道中。不幸的是,由于C#的限制,您不能轻松地将DI与属性一起使用。在第13.4节中,我将向您展示如何使用ServiceFilterAttribute和TypeFilterAttribute基类在过滤器中启用依赖注入。

在开始编写代码之前,我们应该掌握过滤器管道的基本知识。本章的第一部分解释了管道是什么,为什么您可能想要使用它,以及它与中间件管道的区别。

13.1 了解过滤器及其使用时间

在本节中,您将了解有关过滤器管道的所有信息。您将看到它在典型请求的生命周期中的位置,MVC和RazorPages之间的区别,以及过滤器与中间件之间的区别。您将了解六种类型的过滤器,如何将它们添加到自己的应用程序中,以及如何控制它们在处理请求时的执行顺序。

过滤器管道是一个相对简单的概念,因为它提供了普通MVC请求的挂钩,如图13.1所示。例如,假设您希望确保用户只有在登录后才能在电子商务应用上创建或编辑产品。该应用会将匿名用户重定向到登录页面,而不是执行该操作。

图13.1 作为MVC请求的正常处理的一部分,过滤器在EndpointMiddleware中的多个点运行。Razor Page请求也有类似的管道。

如果没有筛选器,您需要在每个特定操作方法的开始时包含相同的代码来检查登录用户。使用这种方法,即使用户未登录,MVC框架仍将执行模型绑定和验证。

使用过滤器,您可以使用MVC请求中的钩子在所有请求或请求的子集上运行公共代码。这样你可以做很多事情,比如

确保用户在操作方法、模型绑定或验证运行之前已登录
自定义特定操作方法的输出格式
在调用操作方法之前处理模型验证失败
从操作方法中捕获异常并以特殊方式处理它们

在许多方面,过滤器管道类似于中间件管道,但仅限于MVC和RazorPages请求。与中间件一样,过滤器很好地处理应用程序的横切关注点,在许多情况下是减少代码重复的有用工具。

13.1.1 MVC过滤器管道

如图13.1所示,过滤器在MVC请求中的多个不同点运行。到目前为止,我使用的MVC请求和过滤器管道的线性视图与这些过滤器的执行方式不太匹配。有五种类型的过滤器适用于MVC请求,每种过滤器在MVC框架的不同阶段运行,如图13.2所示。

图13.2 MVC过滤器管道,包括五个不同的过滤器阶段。某些筛选阶段(资源、操作和结果)在管道的其余部分之前和之后运行两次。

每个过滤器阶段都适合于特定的用例,这是因为它在管道中的特定位置,涉及模型绑定、动作执行和结果执行。

授权过滤器——这些过滤器在管道中首先运行,因此它们对保护您的API和操作方法非常有用。如果授权过滤器认为请求未经授权,它将使请求短路,从而阻止过滤器管道(或操作)的其余部分运行。
资源过滤器——授权后,资源过滤器是管道中要运行的下一个过滤器。它们也可以在管道末端执行,其方式与中间件组件处理传入请求和传出响应的方式大致相同。或者,资源过滤器可以完全缩短请求管道并直接返回响应。
由于其在管道中的早期地位,资源过滤器可以有多种用途。您可以向操作方法添加度量,在请求不受支持的内容类型时阻止执行操作方法,或者在模型绑定之前运行度量,控制模型绑定对该请求的工作方式。
动作过滤器——动作过滤器在动作方法执行前后运行。由于模型绑定已经发生,操作过滤器允许您在方法执行之前操纵方法的参数,或者它们可以完全短路操作并返回不同的IActionResult。因为它们也在动作执行之后运行,所以它们可以选择在动作结果执行之前自定义动作返回的IActionResult。
异常过滤器——异常过滤器可以捕捉过滤器管道中发生的异常并对其进行适当处理。您可以使用异常过滤器来编写自定义MVC特定的错误处理代码,这在某些情况下非常有用。例如,您可以捕获API操作中的异常,并将它们与RazorPages中的异常格式化不同。
结果过滤器——在执行操作方法的IActionResult之前和之后运行结果过滤器。您可以使用结果过滤器来控制结果的执行,甚至可以缩短结果的执行。

您选择要实现的过滤器将取决于您尝试引入的功能。希望尽早缩短请求?资源筛选器非常适合。需要访问操作方法参数吗?使用动作筛选器。

将过滤器管道视为一个在MVC框架中独立存在的小型中间件管道。或者,您可以将过滤器视为MVC动作调用过程的钩子,允许您在请求生命周期的某个特定点运行代码。

本节描述了过滤器管道如何为MVC控制器工作,例如您将使用它来创建API;RazorPages使用几乎相同的过滤器管道。

13.1.2 Razor Pages过滤器管道

RazorPages框架使用与API控制器相同的底层架构,因此过滤器管道实际上完全相同可能并不奇怪。管道之间的唯一区别是RazorPages不使用操作过滤器。相反,它们使用页面过滤器,如图13.3所示。

图13.3 Razor Pages过滤管道,包括五个不同的过滤阶段。授权、资源、异常和结果过滤器的执行方式与MVC管道完全相同。页面过滤器特定于RazorPages,在三个位置执行:页面处理程序选择之后、模型绑定和验证之后以及页面处理程序执行之后。

授权、资源、异常和结果过滤器与您在MVC管道中看到的过滤器完全相同。它们以相同的方式执行,服务于相同的目的,并且可以以相同的方法短路。

注意:这些过滤器实际上是RazorPages和MVC框架之间共享的相同类。例如,如果您创建了一个异常筛选器并将其全局注册,则该筛选器将同样应用于所有API控制器和所有Razor Pages。

与RazorPages过滤器管道的不同之处在于,它使用页面过滤器而不是动作过滤器。与其他过滤器类型不同,页面过滤器在过滤器管道中运行三次:

页面处理程序选择后——执行资源过滤器后,根据请求的HTTP谓词和{handler}路由值选择页面处理程序,如第5章所述。选择页面处理程序后,第一次执行页面过滤器方法。在这个阶段,您不能缩短管道,模型绑定和验证尚未执行。
在模型绑定之后——在执行第一个页面过滤器之后,请求被模型绑定到RazorPage的绑定模型并被验证。该执行与API控制器的动作过滤器执行非常类似。此时,您可以通过返回不同的IActionResult来操纵模型绑定数据或完全缩短页面处理程序的执行。
页面处理程序执行后——如果不缩短页面处理程序的执行,页面过滤器将在页面处理程序完成后运行第三次,也是最后一次。此时,您可以在执行结果之前自定义页面处理程序返回的IActionResult。

页面过滤器的三重执行使得可视化管道有点困难,但通常可以将其视为增强的动作过滤器。您可以使用动作筛选器执行的所有操作,都可以使用页面筛选器执行。此外,如果需要,您可以在页面处理程序选择后挂接。

提示:过滤器的每次执行都执行相应接口的不同方法,因此很容易知道您在管道中的位置,如果您愿意,只在其可能的位置之一执行过滤器。

当人们了解ASPNETCore中的过滤器时,我听到的一个主要问题是“为什么我们需要它们?”如果过滤器管道就像一个小型中间件管道,为什么不直接使用中间件组件,而不是引入过滤器概念?这是一个很好的观点,我将在下一节讨论。

13.1.3 过滤器或中间件:您应该选择哪个?

过滤器管道在许多方面与中间件管道相似,但在决定使用哪种方法时,您应该考虑几个细微的差异。当考虑到相似之处时,它们有三个主要的相似之处:

请求在“入”的途中通过中间件组件,响应在“出”的途中再次通过。资源、操作和结果过滤器也是双向的,尽管授权和异常过滤器只为请求运行一次,页面过滤器运行三次。
中间件可以通过返回响应来缩短请求,而不是将其传递给稍后的中间件。过滤器还可以通过返回响应来短路过滤器管道。
中间件通常用于跨领域的应用程序关注点,如日志记录、性能分析和异常处理。过滤器也有助于解决交叉问题。

相比之下,中间件和过滤器之间有三个主要区别:

中间件可以为所有请求运行;过滤器将只对到达EndpointMiddleware并执行API控制器操作或Razor Page的请求运行。
过滤器可以访问MVC构造,如ModelState和IActionResults。一般来说,中间件独立于MVC和RazorPages,工作在“较低级别”,因此不能使用这些概念。
过滤器可以很容易地应用于请求的子集;例如,单个控制器或单个Razor Page上的所有操作。中间件没有这个概念作为一流的想法(尽管您可以使用定制中间件组件实现类似的功能)。

这一切都很好,但我们应该如何解释这些差异?我们什么时候应该选择一个而不是另一个?

我喜欢将中间件与过滤器视为一个特殊性问题。中间件是一个更通用的概念,它在较低级别的原语(如HttpContext)上运行,因此它的范围更广。如果您需要的功能没有MVC特定的要求,那么应该使用中间件组件。异常处理就是一个很好的例子;异常可能发生在应用程序的任何地方,您需要处理它们,因此使用异常处理中间件是有意义的。

另一方面,如果您确实需要访问MVC构造,或者您希望对某些MVC操作采取不同的行为,那么您应该考虑使用过滤器。具有讽刺意味的是,这也可以应用于异常处理。当客户端期望JSON时,您不希望WebAPI控制器中的异常自动生成HTML错误页面。相反,您可以在Web API操作上使用异常过滤器将异常呈现为JSON,同时让异常处理中间件从应用程序中的Razor Pages捕获错误。

提示:在可能的情况下,考虑使用中间件解决交叉问题。当您需要为不同的操作方法提供不同的行为时,或者当功能依赖于MVC概念(如ModelState验证)时,请使用过滤器。

中间件与过滤器的争论是微妙的,只要它适合你,你选择哪一个并不重要。您甚至可以使用过滤器管道中的中间件组件作为过滤器,但这超出了本书的范围。

提示:作为过滤器的中间件功能在ASPNETCore 1.1中引入,并且在更高版本中也可用。规范的用例是将请求本地化为多种语言。我在这里有一个关于如何使用该功能的博客系列:http://mng.bz/RXa0.

过滤器在隔离方面可能有点抽象,因此在下一节中,我们将查看一些代码,并学习如何在ASPNETCore中编写自定义过滤器。

13.1.4 创建简单过滤器

在本节中,我将向您展示如何创建第一个过滤器;在第13.1.5节中,您将看到如何将它们应用于MVC控制器和动作。我们将从小处开始,创建仅写入控制台的筛选器,但在第13.2节中,我们将查看一些更实际的示例,并讨论它们的一些细微差别。

通过实现一对接口中的一个接口(一个同步(sync),一个异步(async)),可以为给定阶段实现过滤器:

授权筛选器--IAuthorizationFilter或IAsyncAuthorizationFilter
资源筛选器--IResourceFilter或IAsyncResourceFilter
操作筛选器--IActionFilter或IAsyncActionFilter
页面筛选器--IPageFilter或IAsyncPageFilter
异常筛选器--IExceptionFilter或IAsyncExceptionFilter
结果筛选器--IResultFilter或IAsyncResultFilter

您可以使用任何POCO类来实现过滤器,但通常将它们实现为C#属性,您可以使用这些属性来装饰控制器、操作和Razor Pages,如第13.1.5节所示。您可以使用sync或异步接口获得相同的结果,因此选择哪一个应该取决于您在筛选器中调用的任何服务是否需要异步支持。

注意:您应该实现同步接口或异步接口,而不是两者都实现。如果同时实现这两者,则只使用异步接口。

清单13.1显示了一个资源过滤器,它实现了IResourceFilter,并在执行时写入控制台。当请求首次到达筛选器管道的资源筛选器阶段时,将调用OnResourceExecuting方法。相反,OnResourceExecuted方法是在管道的其余部分执行之后调用的:在模型绑定、操作执行、结果执行和所有中间过滤器运行之后。

清单13.1 实现IResourceFilter的示例资源筛选器

public class LogResourceFilter : Attribute, IResourceFilter
{
//在授权筛选器之后,在管道开始时执行
public void OnResourceExecuting( ResourceExecutingContext context) //上下文包含HttpContext、路由详细信息和有关当前操作的信息。
{
Console.WriteLine("Executing!");
} //在模型绑定、操作执行和结果执行之后执行
public void OnResourceExecuted( ResourceExecutedContext context) //包含其他上下文信息,如操作返回的IActionResult
{
Console.WriteLine("Executed”");
}
}

接口方法很简单,对于过滤器管道中的每个阶段都很相似,将上下文对象作为方法参数传递。两个方法同步筛选器中的每一个都有一个*Executing和一个*Executed方法。每个筛选器的参数类型不同,但它包含筛选器管道的所有详细信息。

例如,传递给资源筛选器的ResourceExecutingContext包含HttpContext对象本身、有关选择此操作的路由的详细信息、有关操作本身的详细信息等。后续筛选器的上下文将包含其他详细信息,如操作筛选器的操作方法参数和ModelState。

ResourceExecutedContext方法的上下文对象类似,但它还包含有关其余管道如何执行的详细信息。您可以检查是否发生了未处理的异常,可以查看来自同一阶段的另一个筛选器是否使管道短路,也可以查看用于生成响应的IActionResult。

这些上下文对象功能强大,是高级过滤器行为(如缩短管道和处理异常)的关键。当我们创建更复杂的过滤器示例时,我们将在第13.2节中使用它们。

异步版本的资源过滤器需要实现一个方法,如清单13.2所示。至于同步版本,您将传递ResourceExecutingContext对象作为参数,并传递代表过滤器管道其余部分的委托。必须(异步)调用此委托以执行管道的其余部分,这将返回ResourceExecutedContext的实例。

清单13.2 实现IAsyncResourceFilter的示例资源筛选器

public class LogAsyncResourceFilter : Attribute, IAsyncResourceFilter
{
//在授权筛选器之后,在管道开始时执行
public async Task OnResourceExecutionAsync( ResourceExecutingContext context,
ResourceExecutionDelegate next) //我们为您提供了一个委托,它封装了过滤器管道的其余部分。
{ //在管道的其余部分执行之前调用
Console.WriteLine("Executing async!");
ResourceExecutedContext executedContext = await next(); //执行管道的其余部分并获取ResourceExecutedContext实例
} Console.WriteLine("Executed async!"); //在管道的其余部分执行后调用
}

同步和异步过滤器实现有细微的差别,但在大多数情况下它们是相同的。我建议在可能的情况下实现同步版本,如果需要,只返回到异步版本。

您现在已经创建了几个过滤器,因此我们应该看看如何在应用程序中使用它们。在下一节中,我们将解决两个具体问题:如何控制哪些请求执行新过滤器,以及如何控制它们的执行顺序。

13.1.5 向您的操作、控制器、Razor Pages和全局添加过滤器

在13.1.2节中,我讨论了中间件和过滤器之间的异同。其中一个区别是,过滤器可以被限定为特定的操作或控制器,因此它们只针对特定的请求运行。或者,您可以全局应用过滤器,以便它为每个MVC操作和RazorPage运行。

通过以不同的方式添加过滤器,可以获得多种不同的结果。假设您有一个过滤器,它强制您登录以执行操作。如何将筛选器添加到应用程序将显著改变应用程序的行为:

将筛选器应用于单个操作或Razor Page--匿名用户可以正常浏览应用程序,但如果他们试图访问受保护的操作或Razur Page,他们将被迫登录。
将筛选器应用于控制器--匿名用户可以访问其他控制器的操作,但访问受保护控制器上的任何操作都会迫使他们登录。
全局应用筛选器--用户在未登录的情况下无法使用应用程序。任何访问操作或Razor页面的尝试都会将用户重定向到登录页面。

注意:ASPNETCore附带了这样一个现成的过滤器:AuthorizeFilter。我将在第13.2.1节中讨论这个过滤器,您将在第15章中看到更多内容。

正如我在上一节中所描述的,您通常会将过滤器创建为属性,这是有充分理由的——这使您可以轻松地将它们应用于MVC控制器、动作和RazorPages。在本节中,您将看到如何将清单13.1中的LogResourceFilter应用于操作、控制器、RazorPage和全局。应用筛选器的级别称为其范围。

定义:筛选器的范围是指它应用于多少不同的操作。筛选器的范围可以是操作方法、控制器、Razor Page或全局。

您将从最具体的范围开始--将筛选器应用于单个操作。下面的列表显示了一个MVC控制器的示例,它有两个操作方法:一个使用LogResourceFilter,另一个不使用。

清单13.3 将筛选器应用于操作方法

public class RecipeController : ControllerBase
{
//执行此操作时,LogResourceFilter将作为管道的一部分运行。
[LogResourceFilter]
public IActionResult Index()
{
return Ok();
}
//此操作方法在操作级别没有筛选器。
public IActionResult View()
{
return OK();
}
}

或者,如果要对每个操作方法应用相同的筛选器,可以在控制器范围内添加属性,如下一个列表所示。控制器中的每个操作方法都将使用LogResourceFilter,而不必专门修饰每个方法。

清单13.4 将过滤器应用于控制器

[LogResourceFilter]  //LogResourceFilter添加到控制器上的每个操作。
public class RecipeController : ControllerBase
{
//控制器中的每个操作都用过滤器进行修饰。
public IActionResult Index ()
{
return Ok();
} public IActionResult View()
{
return Ok();
}
}

对于RazorPages,您可以将属性应用于PageModel,如下表所示。过滤器应用于RazorPage中的所有页面处理程序——不可能将过滤器应用于单个页面处理程序;必须在页面级别应用它们。

清单13.5 将过滤器应用于Razor页面

[LogResourceFilter]  //LogResourceFilter被添加到Razor Page的PageModel中。
public class IndexModel : PageModel
{
//LogResourceFilter被添加到Razor Page的PageModel中。
public void OnGet()
{
} public void OnPost()
{
}
}

当应用程序启动时,框架会自动发现作为属性应用于控制器、操作和RazorPages的筛选器。对于公共属性,您可以更进一步,在全局范围内应用过滤器,而不必修饰单个类。

您可以以与controllerror操作范围过滤器不同的方式添加全局过滤器——在Startup中配置控制器和Razor Pages时,直接向MVC服务添加过滤器。此列表显示了添加全局范围筛选器的三种等效方法。

清单13.6 将筛选器全局应用于应用程序

public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options => //使用MvcOptions对象添加过滤器
{
options.Filters.Add(new LogResourceFilter()); //您可以直接传递过滤器的实例。
options.Filters.Add(typeof(LogResourceFilter)); //…或传入筛选器的类型并让框架创建它。
options.Filters.Add<LogResourceFilter>(); //或者,框架可以使用泛型类型参数创建全局过滤器。
});
}
}

您可以使用AddControllers()重载来配置MvcOptions。当您全局配置筛选器时,它们将同时应用于控制器和应用程序中的任何Razor Pages。如果您在应用程序中使用RazorPages,则配置MvcOptions时不会出现过载。相反,您需要使用AddMvcOptions()扩展方法来配置过滤器,如下表所示。

清单13.7 将过滤器全局应用于Razor Pages应用程序

public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages() //此方法不允许您传递lambda来配置MvcOptions。
.AddMvcOptions(options => //必须使用扩展方法将筛选器添加到MvcOptions对象。
{
//您可以按照前面所示的任何方式配置过滤器。
options.Filters.Add(new LogResourceFilter());
options.Filters.Add(typeof(LogResourceFilter)); options.Filters.Add<LogResourceFilter>();
});
}
}

由于可能有三个不同的作用域,您通常会发现应用了多个筛选器的操作方法:一些直接应用于操作方法,另一些从控制器或全局继承。问题是,哪个过滤器首先运行?

13.1.6 了解过滤器执行顺序

您已经看到,过滤器管道包含五个不同的阶段,每种类型的过滤器对应一个阶段。这些阶段始终按照第13.1.1和13.1.2节中所述的固定顺序运行。但是在每个阶段中,您还可以有多个相同类型的过滤器(例如,多个资源过滤器),它们是单个操作方法管道的一部分。这些都可能有多个作用域,这取决于您如何添加它们,正如您在上一节中看到的那样。

在本节中,我们将考虑给定阶段中过滤器的顺序以及范围如何影响这一点。我们将从查看默认订单开始,然后继续讨论如何根据自己的需求定制订单。

默认范围执行顺序

在考虑过滤器排序时,重要的是要记住,资源、操作和结果过滤器实现了两个方法:一个*Executing before方法和一个*Executed after方法。除此之外,页面过滤器实现了三种方法!每个方法的执行顺序取决于过滤器的范围,如图13.4所示的资源过滤器阶段。

图13.4 基于过滤器范围的给定阶段内的默认过滤器排序。对于*Executing方法,首先运行全局范围的筛选器,然后运行controllerscoped,最后运行操作范围的筛选器。对于*Executed方法,过滤器以相反的顺序运行。

默认情况下,在为每个阶段运行*Executing方法时,过滤器从最宽范围(全局)执行到最窄范围(操作)。过滤器的*执行方法以相反的顺序运行,从最窄的范围(操作)到最宽的范围(全局)。

RazorPages的排序有些简单,因为您只有两个作用域——全局作用域筛选器和RazorPage作用域筛选器。对于Razor Pages,全局范围筛选器首先运行*Executing和PageHandlerSelected方法,然后运行页面范围筛选器。对于*Executed方法,过滤器以相反的顺序运行。

有时你会发现你需要对这个顺序进行更多的控制,特别是如果你在同一范围内应用了多个动作过滤器。过滤器管道通过IOrderedFilter接口满足了这一要求。

使用IORDEREDFILTER重写过滤器执行的默认顺序

过滤器非常适合从控制器操作和Razor Page中提取交叉关注点,但如果对一个操作应用了多个过滤器,则通常需要控制它们执行的精确顺序。

作用域可以为您提供一些方法,但对于其他情况,您可以实现IOrderedFilter。此接口由一个属性Order组成:

public interface IOrderedFilter
{
  int Order { get; }
}

您可以在筛选器中实现此属性,以设置它们的执行顺序。过滤器管道首先根据该值对给定阶段中的过滤器进行排序,从最低到最高,并使用默认范围顺序处理关系,如图13.5所示。

图13.5 使用IOrderedFilter接口控制阶段的过滤顺序。过滤器首先按Order属性排序,然后按作用域排序。

Order=-1的筛选器首先执行,因为它们的Order值最低。控制器筛选器首先执行,因为它具有比操作范围筛选器更宽的范围。Order=0的过滤器将按默认范围顺序执行,如图13.5所示。最后,执行Order=1的过滤器。

默认情况下,如果过滤器未实现IOrderedFilter,则假定其Order=0。作为ASPNETCore的一部分提供的所有筛选器都具有Order=0,因此您可以实现与这些筛选器相关的自己的筛选器。

本节介绍了使用过滤器和为自己的应用程序创建自定义实现所需的大部分技术细节。在下一节中,您将看到ASPNETCore提供的一些内置过滤器,以及您可能希望在自己的应用程序中使用的过滤器的一些实用示例。

13.2 为应用程序创建自定义筛选器

ASPNETCore包含许多您可以使用的过滤器,但通常最有用的过滤器是特定于您自己的应用程序的自定义过滤器。在本节中,我们将学习六种类型的过滤器。我将更详细地解释它们的用途以及何时使用它们。我将指出这些过滤器的示例,这些过滤器是ASPNETCore本身的一部分,您将看到如何为示例应用程序创建自定义过滤器。

为了给您提供一些实际的操作方法,我们将从第12章中用于访问配方应用程序的WebAPI控制器开始。此控制器包含两个操作:一个用于获取RecipeDetailViewModel,另一个用于使用新值更新Recipe。此列表显示了本章的起点,包括两种操作方法。

清单13.8 重构以使用过滤器之前的Recipe Web API控制器

[Route("api/recipe")]
public class RecipeApiController : ControllerBase
{
private const bool IsEnabled = true; //此字段将作为配置传入,用于控制对操作的访问。
public RecipeService _service;
public RecipeApiController(RecipeService service)
{
_service = service;
} [HttpGet("{id}")]
public IActionResult Get(int id)
{
if (!IsEnabled) { return BadRequest(); } //如果未启用API,则阻止进一步执行。
try
{
//如果请求的配方不存在,则返回404响应。
if (!_service.DoesRecipeExist(id))
{
return NotFound();
}
var detail = _service.GetRecipeDetail(id); //获取配方详细视图模型。
//将LastModified响应标头设置为模型中的值
Response.GetTypedHeaders().LastModified = detail.LastModified;
return Ok(detail); //返回具有200响应的视图模型
}
//如果发生异常,则捕获它并以预期格式返回错误,即500错误。
catch (Exception ex)
{
return GetErrorResponse(ex);
}
} [HttpPost("{id}")]
public IActionResult Edit( int id, [FromBody] UpdateRecipeCommand command)
{
if (!IsEnabled) { return BadRequest(); } //如果未启用API,则阻止进一步执行。
try
{
//验证绑定模型,如果存在错误,则返回400响应。
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
//如果请求的配方不存在,则返回404响应。
if (!_service.DoesRecipeExist(id))
{
return NotFound();
}
//从命令更新配方并返回200响应。
_service.UpdateRecipe(command);
return Ok();
}
//如果发生异常,则捕获它并以预期格式返回错误,即500错误。
catch (Exception ex)
{
return GetErrorResponse(ex);
}
} private static IActionResult GetErrorResponse(Exception ex)
{
var error = new ProblemDetails
{
Title = "An error occurred",
Detail = context.Exception.Message, Status = 500,
Type = "https://httpstatuses.com/500"
}; return new ObjectResult(error)
{
StatusCode = 500
};
}
}

这些操作方法目前有很多代码,这些代码隐藏了每个操作的意图。方法之间也有很多重复,例如检查Recipe实体是否存在以及格式化异常。

在本节中,您将重构此控制器,以对方法中与每个操作的意图无关的所有代码使用过滤器。在本章结束时,您将得到一个更简单的控制器,它更容易理解,如图所示。

清单13.9 重构后使用过滤器的Recipe Web API控制器

[Route("api/recipe")]
[ValidateModel, HandleException, FeatureEnabled(IsEnabled = true)] //过滤器封装了多个操作方法所共有的大部分逻辑。
public class RecipeApiController : ControllerBase
{
public RecipeService _service;
public RecipeApiController(RecipeService service)
{
_service = service;
} [HttpGet("{id}"), EnsureRecipeExists, AddLastModifiedHeader] //将筛选器放置在操作级别将其限制为单个操作。
public IActionResult Get(int id)
{
//返回配方视图模型这一操作的意图要清晰得多。
var detail = _service.GetRecipeDetail(id); return Ok(detail);
} [HttpPost("{id}"), EnsureRecipeExists] //在操作级别放置筛选器可以控制它们的执行顺序。
public IActionResult Edit(int id, [FromBody] UpdateRecipeCommand command)
{
//更新配方这一行动的意图更加明确。
_service.UpdateRecipe(command);
return Ok();
}
}

我想您必须同意,清单13.9中的控制器更容易阅读!在本节中,您将一点一点地重构控制器,删除交叉代码以获得更易于管理的内容。我们将在本节中创建的所有过滤器都将使用同步过滤器接口——作为练习,我将留给您创建它们的异步过滤器接口。我们将从查看授权筛选器以及它们如何与ASPNETCore中的安全性相关开始。

13.2.1 授权过滤器:保护您的API

身份验证和授权是相关的基本安全概念,我们将在第14章和第15章中详细讨论。

定义:身份验证与确定谁提出了请求有关。授权与用户被允许访问的内容有关。

授权筛选器首先在MVC筛选器管道中运行,然后再运行任何其他筛选器。当请求不满足必要的要求时,它们通过立即短路管道来控制对操作方法的访问。

ASPNETCore有一个内置的授权框架,当您需要保护MVC应用程序或Web API时,应该使用该框架。您可以使用自定义策略来配置此框架,使您可以精细地控制对操作的访问。

提示:可以通过实现IAuthorizationFilter或IAsyncAuthorizationFilter来编写自己的授权筛选器,但我强烈建议不要这样做。ASPNETCore授权框架是高度可配置的,应该满足您的所有需求。

ASPNETCore授权框架的核心是一个授权筛选器AuthorizeFilter,您可以通过使用[Authorize]属性修饰操作或控制器来将其添加到筛选器管道中。在最简单的形式中,将[Authorize]属性添加到操作中,如下面的列表所示,意味着请求必须由经过身份验证的用户发出,才能允许继续。如果您未登录,它将缩短管道,向浏览器返回401未授权响应。

清单13.10 向操作方法添加[Authorize]

public class RecipeApiController : ControllerBase
{
public IActionResult Get(int id) //Get方法没有[Authorize]属性,因此任何人都可以执行它。
{
// 方法主体
}
[Authorize] //使用[Authorize]将AuthorizeFilter添加到筛选器管道
public IActionResult Edit( int id, [FromBody] UpdateRecipeCommand command) //只有登录后才能执行Edit方法。
{
// 方法主体
}
}

与所有筛选器一样,您可以在控制器级别应用[Authorize]属性来保护控制器上的所有操作,也可以应用Razor Page来保护页面中的所有页面处理程序方法,甚至可以应用全局属性来保护应用程序中的每个端点。

注意:我们将在第15章中详细探讨授权,包括如何添加更详细的需求,以便只有特定的用户组才能执行操作。

管道中的下一个过滤器是资源过滤器。在下一节中,您将从RecipeApiController中提取一些常见的代码,并了解创建短路滤波器是多么简单。

13.2.2 资源过滤器:缩短行动方法

资源过滤器是MVC过滤器管道中的第一个通用过滤器。在13.1.4节中,您看到了记录到控制台的同步和异步资源筛选器的最小示例。在您自己的应用程序中,由于资源过滤器在过滤器管道中执行得很早(也很晚),您可以将其用于各种用途。

ASPNETCore框架包括一些不同的资源过滤器实现,您可以在应用程序中使用:

ConsumerAttribute——可用于限制操作方法可以接受的允许格式。如果您的操作用[Consumes(“application/json”)]修饰,但客户端以XML形式发送请求,则资源过滤器将缩短管道并返回415不支持的媒体类型响应。
DisableFormValueModelBindingAttribute——此筛选器阻止模型绑定绑定到请求正文中的表单数据。如果您知道一个操作方法将处理需要手动管理的大型文件上载,那么这将非常有用。资源筛选器在模型绑定之前运行,因此可以通过这种方式禁用单个操作的模型绑定。1

当您希望确保筛选器在模型绑定之前在管道的早期运行时,资源筛选器非常有用。它们为您的逻辑提供了进入管道的早期钩子,因此如果需要,您可以快速缩短请求。

回顾清单13.8,看看是否可以将任何代码重构为资源过滤器。Get和Edit方法的开头都会出现一个候选行:

if (!IsEnabled) { return BadRequest(); }

这行代码是一个特性切换,您可以根据IsEnabled字段禁用整个API的可用性。实际上,您可能会从数据库或配置文件中加载IsEnabled字段,以便在运行时动态控制可用性,但在本例中,我使用的是硬编码值。

这段代码是自包含的交叉逻辑,它与每个操作方法的主要意图有点相切——是过滤器的完美候选。您希望在管道的早期,在任何其他逻辑之前执行特性切换,因此资源过滤器是有意义的。

提示:从技术上讲,您也可以在本示例中使用授权筛选器,但我遵循自己的建议“不要编写自己的授权筛选器!”

下一个列表显示FeatureEnabledAttribute的实现,它从操作方法中提取逻辑并将其移动到过滤器中。我还将IsEnabled字段公开为筛选器上的属性。

清单13.11 FeatureEnabledAttribute资源筛选器

public class FeatureEnabledAttribute : Attribute, IResourceFilter
{
//在模型绑定之前,在过滤器管道的早期执行
public bool IsEnabled { get; set; } //定义是否启用功能
public void OnResourceExecuting( ResourceExecutingContext context)
{
//如果未启用该功能,请通过设置上下文来短路管道。Result属性
if (!IsEnabled)
{
context.Result = new BadRequestResult();
}
}
//必须实现以满足IResourceFilter,但在这种情况下不需要
public void OnResourceExecuted( ResourceExecutedContext context) { }
}

这个简单的资源筛选器演示了许多适用于大多数筛选器类型的重要概念:

过滤器既是一个属性,也是一个过滤器。这允许您使用[FeatureEnabled(IsEnabled=true)]来装饰控制器、操作方法和Razor Pages。
筛选器接口由两个方法组成:*Executing(在模型绑定之前运行)和*Executed(在执行结果之后运行)。您必须同时实现这两个,即使您的用例只需要一个。
过滤器执行方法提供上下文对象。这提供了对请求的HttpContext以及中间件将执行的操作方法的元数据的访问。
若要缩短管道,请将context.Result属性设置为IActionResult实例。框架将执行此结果以生成响应,绕过管道中的任何剩余过滤器,并完全跳过操作方法(或页面处理程序)。在本例中,如果未启用该功能,则通过返回BadRequestResult绕过管道,这将向客户端返回一个400错误。

通过将此逻辑移动到资源过滤器中,您可以将其从操作方法中删除,而用一个简单的属性来装饰整个API控制器:

[Route("api/recipe"), FeatureEnabled(IsEnabled = true)] 
public class RecipeApiController : ControllerBase

到目前为止,您只从操作方法中提取了两行代码,但您走在了正确的轨道上。在下一节中,我们将继续操作过滤器,并从操作方法代码中提取另外两个过滤器。

13.2.3 动作过滤器:自定义模型绑定和动作结果

动作过滤器在模型绑定之后、动作方法执行之前运行。由于这种定位,动作过滤器可以访问将用于执行动作方法的所有参数,这使它们成为从动作中提取公共逻辑的强大方式。

除此之外,它们还可以在操作方法执行后立即运行,如果需要,可以完全更改或替换操作返回的IActionResult。它们甚至可以处理操作中抛出的异常。

注意:动作过滤器不会对Razor Pages执行。类似地,页面过滤器不会对操作方法执行。

ASPNETCore框架包括几个开箱即用的操作筛选器。其中一个常用的过滤器是ResponseCacheFilter,它在动作方法响应上设置HTTP缓存头。

提示:缓存是一个广泛的主题,旨在改善应用程序的性能,而不是简单的方法。但缓存也会使调试问题变得困难,在某些情况下甚至可能是不可取的。因此,我经常将ResponseCacheFilter应用于我的操作方法,以设置禁用缓存的HTTP缓存头!您可以在Microsoft的“ASPNETCore中的响应缓存”文档中阅读此缓存和其他缓存方法,网址为http://mng.bz/2eGd.

当你通过从你的动作方法中提取通用代码来构建适合你自己的应用的过滤器时,动作过滤器的真正威力就来了。为了演示,我将为RecipeApiController创建两个自定义过滤器:

ValidateModelAttribute--如果模型状态指示绑定模型无效并且将缩短操作执行,则返回BadRequestResult。这个属性曾经是我的Web API应用程序的主要部分,但[ApiController]属性现在为您处理这个(以及更多)。尽管如此,我认为了解幕后发生的事情是很有用的。
EnsureRecipeExistsAttribute--这将使用每个操作方法的id参数来验证所请求的Recipe实体在操作方法运行之前是否存在。如果配方不存在,过滤器将返回NotFoundResult,并使管道短路。

正如您在第6章中看到的,MVC框架在执行操作之前会自动验证绑定模型,但这取决于您决定如何处理它。对于Web API控制器,通常返回一个包含错误列表的400BadRequest响应,如图13.6所示。

图13.6 使用Postman将数据发布到Web API。数据被绑定到动作方法的绑定模型并被验证。若验证失败,通常会返回带有验证错误列表的400错误请求响应。

您通常应该在Web API控制器上使用[ApiController]属性,这会自动为您提供此行为。但如果您不能或不想使用该属性,则可以创建自定义操作筛选器。清单13.12显示了与[ApiController]属性的行为类似的基本实现。

清单13.12 用于验证ModelState的操作筛选器

public class ValidateModelAttribute : ActionFilterAttribute  //为了方便起见,可以从ActionFilterAttribute基类派生。
{
//重写Executing方法以在Action执行之前运行筛选器
public override void OnActionExecuting( ActionExecutingContext context)
{
//此时,模型绑定和验证已经运行,因此您可以检查状态。
if (!context.ModelState.IsValid) //如果模型无效,请设置Result属性;这会使动作执行短路。
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}

该属性不言自明,遵循与13.2.2节中的资源过滤器相似的模式,但有几个有趣的点:

我从抽象的ActionFilterAttribute派生。此类实现IActionFilter和IResultFilter以及它们的异步对应项,因此您可以根据需要重写所需的方法。这避免了需要添加未使用的OnActionExecuted()方法,但使用基类是完全可选的,这是一个优先事项。
操作筛选器在模型绑定发生后运行,因此如果验证失败,context.ModelState将包含验证错误。
在上下文上设置Result属性会使管道短路。但由于动作过滤器阶段的位置,只有动作方法执行和稍后的动作过滤器被绕过;管道的所有其他阶段都在运行,就好像该操作已正常执行一样。

如果将此操作筛选器应用于RecipeApiController,则可以从两个操作方法的开头删除此代码,因为它将在筛选器管道中自动运行:

if (!ModelState.IsValid)
{
  return BadRequest(ModelState);
}

您将使用类似的方法删除重复的代码,该代码检查作为操作方法参数提供的id是否与现有Recipe实体对应。

以下列表显示了EnsureRecipeExistsAttribute操作筛选器。这使用RecipeService的实例来检查配方是否存在,如果不存在,则返回404 Not Found。

清单13.13 检查配方是否存在的操作筛选器

public class EnsureRecipeExistsAtribute : ActionFilterAttribute
{
//从DI容器获取RecipeService的实例
public override void OnActionExecuting( ActionExecutingContext context)
{
var service = (RecipeService) context.HttpContext.RequestServices.GetService(typeof(RecipeService));
var recipeId = (int) context.ActionArguments["id"]; //检索执行时将传递给操作方法的id参数
if (!service.DoesRecipeExist(recipeId))
{
context.Result = new NotFoundResult(); //如果不存在,则返回404 Not Found结果并使管道短路
}
}
}

与之前一样,为了简单起见,您从ActionFilterAttribute派生,并重写了OnActionExecuting方法。过滤器的主要功能依赖于RecipeService的DoesRecipeExist()方法,因此第一步是获取RecipeService实例。context参数提供对请求的HttpContext的访问,这反过来允许您访问DI容器并使用RequestServices.GetService()返回RecipeService的实例。

警告:这种获取依赖关系的技术被称为服务位置,通常被认为是一种反模式。在第13.4节中,我将向您展示使用DI容器将依赖项注入过滤器的更好方法。

除了RecipeService,您需要的另一条信息是Get和Edit操作方法的id参数。在操作筛选器中,模型绑定已经发生,因此框架将用于执行操作方法的参数是已知的,并且在context.ActionArguments上公开。

操作参数公开为Dictionary<string,object>,因此可以使用“id”字符串键获取id参数。记住将对象强制转换为正确的类型。

提示:每当我看到像这样的魔术字符串时,我总是尝试用操作符的名称来替换它们。不幸的是,nameof不适用于这样的方法参数,所以在重构代码时要小心。我建议将动作过滤器显式应用于动作方法(而不是全局或控制器),以提醒您有关隐式耦合。

有了RecipeService和id,就可以检查标识符是否对应于现有的Recipe实体,如果不对应,则将context.Result设置为NotFoundResult。这会使管道短路,并完全绕过操作方法。

注意:请记住,您可以在一个阶段中运行多个操作过滤器。通过设置上下文使管道短路。结果将阻止阶段中稍后的筛选器运行,并绕过操作方法执行。

在我们继续之前,值得一提的是动作过滤器的一个特例。ControllerBase基类实现IActionFilter和IAsyncActionFilter本身。如果您发现自己正在为单个控制器创建操作筛选器,并且希望将其应用于该控制器中的每个操作,则可以覆盖控制器上的适当方法。

清单13.14 直接在ControllerBase上重写操作筛选器方法

public class HomeController : ControllerBase  //从ControllerBase类派生
{
//在控制器中每个操作的任何其他操作筛选器之前运行
public override void OnActionExecuting( ActionExecutingContext context) { }
//在控制器中每个操作的所有其他操作筛选器之后运行
public override void OnActionExecuted( ActionExecutedContext context) { }
}

如果在控制器上重写这些方法,它们将在过滤器管道的操作过滤器阶段中为控制器上的每个操作运行。无论顺序或范围如何,OnActionExecuting ControllerBase方法都在任何其他操作筛选器之前运行,OnActionExecuted方法在所有其他操作筛选器之后运行。

提示:控制器实现在某些情况下可能有用,但您无法控制与其他过滤器相关的排序。就我个人而言,我通常倾向于将逻辑分解为显式、声明性的过滤属性,但一如既往,选择权归你。

随着资源和操作筛选器的完成,您的控制器看起来更加整洁,但有一个方面特别值得删除:异常处理。在下一节中,我们将讨论如何为控制器创建自定义异常过滤器,以及为什么您可能希望这样做而不是使用异常处理中间件。

13.2.4 异常过滤器:为您的操作方法定制异常处理

在第3章中,我深入探讨了可以添加到应用程序中的错误处理中间件的类型。这些允许您捕获从任何后续中间件抛出的异常并适当地处理它们。如果您使用的是异常处理中间件,您可能会想知道为什么我们需要异常过滤器。

这个问题的答案与我在13.1.3节中概述的大致相同:当您需要特定于MVC或只适用于特定路由的行为时,过滤器非常适合于交叉关注点。

这两种方法都适用于异常处理。异常过滤器是MVC框架的一部分,因此它们可以访问发生错误的上下文,例如正在执行的操作或Razor Page。这对于在发生错误时记录其他详细信息(例如导致错误的操作参数)非常有用。

警告:如果使用异常筛选器记录操作方法参数,请确保日志中没有存储敏感数据,例如密码或信用卡详细信息。

您还可以使用异常过滤器以不同的方式处理来自不同路由的错误。假设你的应用程序中既有RazorPages,也有WebAPI控制器,就像我们在配方应用程序中所做的那样。当Razor Page抛出异常时会发生什么?

正如您在第3章中看到的,异常沿着中间件管道返回,并被异常处理程序中间件捕获。异常处理程序中间件将重新执行管道并生成HTML错误页。

这对您的Razor Pages来说很好,但Web API控制器中的异常情况呢?如果您的API抛出异常,并因此返回由异常处理程序中间件生成的HTML,这将导致调用API的客户机收到JSON响应!

相反,异常过滤器允许您在过滤器管道中处理异常并生成适当的响应主体。异常处理程序中间件只拦截没有正文的错误,因此它将让修改后的Web API响应不受影响地传递。

注意:[ApiController]属性将错误StatusCodeResults转换为ProblemDetails对象,但它不会捕获异常。

异常过滤器可以捕捉来自不仅仅是操作方法和页面处理程序的异常。如果在以下时间发生异常,它们将运行:

在模型绑定或验证期间
执行操作方法或页面处理程序时
执行操作筛选器或页面筛选器时

您应该注意,异常过滤器不会捕捉在除操作和页面过滤器之外的任何过滤器中抛出的异常,因此资源和结果过滤器不抛出异常非常重要。类似地,它们不会捕捉执行IActionResult时抛出的异常,例如将Razor视图呈现为HTML时。

既然您知道了为什么可能需要一个异常过滤器,那么就继续为RecipeApiController实现一个,如下所示。这使您可以安全地从操作方法中删除try-catch块,因为您知道过滤器将捕获任何错误。

清单13.15 HandleExceptionAttribute异常筛选器

public class HandleExceptionAttribute : ExceptionFilterAttribute  //ExceptionFilterAttribute是实现IExceptionFilter的抽象基类。
{
public override void OnException(ExceptionContext context) //IExceptionFilter只有一个可重写的方法。
{
//构建要在响应中返回的问题详细信息对象
var error = new ProblemDetails
{
Title = "An error occurred",
Detail = context.Exception.Message, Status = 500,
Type = "https://httpstatuses.com/500"
};
//创建ObjectResult以序列化ProblemDetails并设置响应状态代码
context.Result = new ObjectResult(error)
{
StatusCode = 500
};
context.ExceptionHandled = true; //将异常标记为已处理,以防止其传播到中间件管道中
}
}

在应用程序中使用异常过滤器是很常见的,尤其是在应用程序中将API控制器和Razor Pages混合使用时,但它们并不总是必需的。如果您可以用一个中间件处理应用程序中的所有异常,那么就放弃异常过滤器,转而使用它。

您几乎完成了RecipeApiController的重构。您只需要添加一个过滤器类型:结果过滤器。自定义结果过滤器在我编写的应用程序中相对少见,但正如您所见,它们有其用途。

13.2.5 结果过滤器:在执行操作结果之前自定义操作结果

如果管道中的一切都运行成功,并且没有短路,那么在操作过滤器之后,管道的下一个阶段就是结果过滤器。这些在执行操作方法(或操作过滤器)返回的IActionResult之前和之后运行。

警告:如果设置context.Result导致管道短路,则不会运行结果筛选器阶段,但仍将执行IActionResult以生成响应。该规则的例外是动作和页面过滤器——它们只会使动作执行短路,如图13.2和13.3所示,因此结果过滤器正常运行,就像动作或页面处理程序本身生成响应一样。

结果过滤器在操作过滤器之后立即运行,因此它们的许多用例都是相似的,但通常使用结果过滤器来定制IActionResult的执行方式。例如,ASPNETCore在其框架中内置了几个结果过滤器:

ProducesAttribute——这强制将Web API结果序列化为特定的输出格式。例如,用[Products(“application/xml”)]修饰动作方法会迫使格式化程序尝试将响应格式化为xml,即使客户端没有在Accept标头中列出xml。
FormatFilterAttribute——用这个过滤器修饰一个操作方法,告诉格式化程序查找一个名为format的路由值或查询字符串参数,并使用它来确定输出格式。例如,您可以调用/api/cipe/11?format=json和FormatFilter会将响应格式化为json,还是调用api/precipe/11?format=xml,并以xml形式获取响应。

除了控制输出格式器之外,还可以使用结果过滤器在执行IActionResult并生成响应之前进行最后一分钟的调整。作为可用灵活性的一个示例,在下面的列表中,我演示了基于从操作返回的对象设置LastModified标头。

这是一个有点做作的例子——它对一个单独的动作足够具体,不需要移动到结果过滤器——但希望你能理解。

清单13.16 在结果过滤器中设置响应头

public class AddLastModifedHeaderAttribute : ResultFilterAttribute  ////ResultFilterAttribute提供了一个可以重写的有用基类。
{
//您也可以重写Executed方法,但此时已发送响应。
public override void OnResultExecuting( ResultExecutingContext context)
{
//检查操作结果是否返回带有视图模型的200 Ok结果。
if (context.Result is OkObjectResult result
&& result.Value is RecipeDetailViewModel detail) //检查视图模型类型是否为RecipeDetailViewModel。
{
//如果是,则获取LastModified属性并在响应中设置LastModified标头
var viewModelDate = detail.LastModified; context.HttpContext.Response
.GetTypedHeaders().LastModified = viewModelDate;
}
}
}

我在这里使用了另一个助手基类ResultFilterAttribute,因此只需要重写一个方法来实现过滤器。获取context.Result上公开的当前IActionResult,并检查它是否是具有RecipeDetailViewModel值的OkObjectResult实例。如果是,则从视图模型中获取LastModified字段,并向响应中添加LastModified标头。

提示:GetTypedHeaders()是一种扩展方法,它提供对请求和响应头的强类型访问。它负责为您解析和格式化值。您可以在Microsoft.AspNetCore.Http命名空间中找到它。

与资源和操作筛选器一样,结果筛选器可以实现在执行结果后运行的方法:OnResultExecuted。例如,可以使用此方法检查在IActionResult执行期间发生的异常。

警告:通常,您不能修改OnResultExecuted方法中的响应,因为您可能已经开始将响应流式传输到客户端。

使用IAlwaysRunResultFilter在短路后运行结果过滤器
结果过滤器旨在“包装”操作方法或操作过滤器返回的IActionResult的执行,以便您可以自定义操作结果的执行方式。但是,当您通过设置上下文来缩短筛选器管道时,此自定义不适用于IActionResults集。结果是授权筛选器、资源筛选器或异常筛选器。
这通常不是问题,因为许多结果过滤器都是为处理“快乐路径”转换而设计的。但有时您需要确保始终将转换应用于IActionResult,而不管它是由操作方法还是短路过滤器返回的。
对于这些情况,可以实现IAlwaysRunResultFilter或IAsyncAlwaysRunResultFilter。这些接口扩展(并且与标准结果过滤器接口相同),因此它们与过滤器管道中的正常结果过滤器一样运行。但这些接口将筛选器标记为在授权筛选器、资源筛选器或异常筛选器使管道短路后也运行,而标准结果筛选器无法运行。
可以使用IAlwaysRunResultFilter确保始终更新某些操作结果。例如,文档显示了如何使用IAlwaysRunResultFilter将415 StatusCodeResult转换为422 StatusCodeResult,而不考虑操作结果的来源。请参阅Microsoft“ASPNETCore中的筛选器”文档的“IAlwaysRunResultFilter和IAsyncAlwaysRunResultFilter”部分:http://mng.bz/JDo0.

我们现在已经完成了RecipeApiController的简化。通过提取过滤器的各种功能,清单13.8中的原始控制器已简化为清单13.9中的版本。这显然是一个有点极端和做作的演示,我并不主张过滤器应该始终是您的首选。

提示:在大多数情况下,过滤器应该是最后的手段。在可能的情况下,通常最好在控制器中使用简单的私有方法,或者将功能推送到域中,而不是使用过滤器。过滤器通常应用于从控制器中提取重复的、与HTTP相关的或常见的交叉代码。

还有一个过滤器我们还没有看到,因为它只适用于RazorPages:页面过滤器。

13.2.6 页面过滤器:自定义Razor Pages的模型绑定

如前所述,动作过滤器仅适用于控制器和动作;它们对Razor Pages没有影响。类似地,页面过滤器对控制器和操作没有影响。然而,页面过滤器和操作过滤器扮演着类似的角色。

与操作筛选器的情况一样,ASPNETCore框架包括几个现成的页面筛选器。其中之一是Razor Page,相当于缓存操作筛选器ResponseCacheFilter,称为PageResponseCache筛选器。这与我在13.2.3节中描述的动作过滤器等效,在RazorPage响应上设置HTTP缓存头。

页面过滤器有些不寻常,因为它们实现了三种方法,如第13.1.2节所述。在实践中,我很少看到实现这三个功能的页面过滤器。在页面处理程序选择之后和模型验证之前,需要立即运行代码是不寻常的。更常见的是直接执行类似于动作过滤器的角色。

例如,下面的列表显示了与EnsureRecipeExistsAttribute操作筛选器等效的页面筛选器。

清单13.17 检查配方是否存在的页面过滤器

//从DI容器获取RecipeService的实例
public class PageEnsureRecipeExistsAtribute : Attribute, IPageFilter //实现IPageFilter和作为一个属性,以便可以修饰Razor PageModel。
{
//在处理程序选择之后、模型绑定之前执行——本例中未使用
public void OnPageHandlerSelected( PageHandlerSelectedContext context) { } public void OnPageHandlerExecuting( PageHandlerExecutingContext context)
{
//在模型绑定和验证之后、页面处理程序执行之前执行检查是否存在具有给定RecipeId的Recipe实体
var service = (RecipeService) context.HttpContext
.RequestServices.GetService(typeof(RecipeService));
var recipeId = (int) context.HandlerArguments["id"]; //检索执行时将传递给页面处理程序方法的id参数
if (!service.DoesRecipeExist(recipeId))
{
context.Result = new NotFoundResult(); //如果不存在,则返回404 Not Found结果并使管道短路
}
}
//在页处理程序执行(或短路)之后执行--本例中未使用
public void OnPageHandlerExecuted( PageHandlerExecutedContext context) { }
}

页面筛选器与等效的操作筛选器非常相似。最明显的区别是需要实现三种方法来满足IPageFilter接口。您通常希望实现OnPageHandlerExecuting方法,该方法在模型绑定和验证之后、页面处理程序执行之前运行。

动作筛选器代码和页面筛选器代码之间的一个细微差别是,动作筛选器使用context.ActionArguments访问模型绑定的动作参数。在示例中,页面筛选器使用context.HandlerArguments,但也有另一个选项。

请记住,从第6章开始,RazorPages通常使用[BindProperty]属性绑定到PageModel上的公共属性。通过将HandlerInstance属性转换为正确的PageModel类型并直接访问该属性,您可以直接访问这些属性,而不必使用魔术字符串。例如

var recipeId = ((ViewRecipePageModel)context.HandlerInstance).Id

正如ControllerBase类实现IActionFilter一样,PageModel实现IPageFilter和IAsyncPageFilter。如果你想为一个Razor页面创建一个动作过滤器,你可以省去创建一个单独的页面过滤器的麻烦,直接在Razor Page中覆盖这些方法。

提示:我通常认为,除非您有一个非常常见的要求,否则使用页面过滤器不值得麻烦。附加的间接页面过滤器级别,加上个别RazorPages的定制特性,意味着我通常觉得它们不值得使用。当然,你的里程数可能会有所不同,但不要将其作为第一选择。

这让我们结束了对MVC管道中每个过滤器的详细研究。回顾并比较清单13.8和13.9,您可以看到过滤器允许我们重构控制器,并使每个操作方法的意图更加清晰。以这种方式编写代码更容易推理,因为每个过滤器和操作都有一个单独的责任。

在下一节中,我们将稍微绕一绕,了解当你短路滤波器时会发生什么。我已经描述了如何做到这一点,通过在过滤器上设置context.Result属性,但我还没有描述具体发生了什么。例如,如果在短路的阶段有多个滤波器,该怎么办?那些还跑吗?

13.3 了解管道短路

在本小节中,您将了解过滤器管道短路的详细信息。您将看到当管道短路时,其他过滤器在一个阶段发生了什么,以及如何使每种类型的过滤器短路。

一个简短的警告:过滤器短路的主题可能有点混乱。与中间件短路不同,过滤管道更加细微。幸运的是,你不会经常发现自己需要深入研究,但当你这样做时,你会很高兴看到细节。

通过将context.result设置为IActionResult,可以缩短授权、资源、操作、页面和结果筛选器。以这种方式设置操作结果会导致部分或全部剩余管道被绕过。但是,正如图13.2和13.3所示,过滤器管道并不完全是线性的,因此短路并不总是沿着管道朝下。例如,短路动作过滤器仅绕过动作方法执行——结果过滤器和结果执行阶段仍在运行。

另一个困难是如果你有不止一种类型的过滤器会发生什么。假设在一个管道中执行三个资源过滤器。如果第二个过滤器导致短路,会发生什么?所有剩余的过滤器都被绕过,但第一个资源过滤器已经运行了其*Executing命令,如图13.7所示。此较早的筛选器也可以使用context.Canceled=true运行其*Executed命令,表示该阶段(资源筛选器阶段)中的筛选器短路了管道。

图13.7 短路资源过滤器对该阶段其他资源过滤器的影响。阶段中的后期过滤器根本不会运行,但早期过滤器运行其OnResourceExecuted函数。

了解当你短路一个过滤器时,会运行哪些其他过滤器可能有些麻烦,但我在表13.1中总结了每个过滤器。在考虑短路问题时,参考图13.2和13.3,可以直观地看到管道的形状。

表13.1 短路过滤器对过滤器管道执行的影响

过滤器类型

如何短路?

还有什么运行?

Authorization filters

Set context.Result

仅运行IAlwaysRunResultFilters。

Resource filters

Set context.Result

资源筛选器*从早期筛选器执行的函数随上下文运行。Cancelled=true。在执行IActionResult之前运行IAlwaysRunResultFilters。

Action filters

Set context.Result

仅绕过操作方法执行。管道中较早的操作筛选器使用上下文运行其*Executed 方法。Cancelled=true,则结果筛选器、结果执行和资源筛选器的*已执行的方法都正常运行。

Page filters

Set context.Result in OnPageHandlerSelected

仅绕过页面处理程序执行。管道中较早的页面筛选器使用上下文运行其*Executed 的方法。Cancelled=true,则结果筛选器、结果执行和资源筛选器的*Executed 方法都正常运行。

Exception filters

Set context.Result and Exception.Handled = true

所有资源筛选器*Executed 函数都将运行。在执行IActionResult之前运行IAlwaysRunResultFilters。

Result filters

Set context.Cancelled = true

管道中早期的结果筛选器使用上下文运行其*Executed 函数。Cancelled=true。所有资源筛选器*Executed 函数正常运行。

这里最有趣的一点是,短路动作过滤器(或页面过滤器)根本不会短路大部分管道。事实上,它只绕过后面的动作过滤器和动作方法执行本身。通过主要构建动作过滤器,您可以确保其他过滤器(如定义输出格式的结果过滤器)照常运行,即使在动作过滤器短路时也是如此。

在本章中,我最不想谈论的是如何将DI与过滤器一起使用。您在第10章中看到了DI是ASPNETCore不可或缺的一部分,在下一节中,您将看到如何设计过滤器,以便框架可以为您将服务依赖性注入其中。

13.4 使用带有过滤器属性的依赖注入

在本节中,您将学习如何将服务注入到过滤器中,以便在过滤器中利用DI的简单性。您将学习使用两个助手过滤器来实现这一点,即TypeFilterAttribute和ServiceFilterAttribute,您将了解如何使用它们来简化您在13.2.3节中定义的操作过滤器。

ASPNET的前一版本使用了过滤器,但他们遇到了一个特别的问题:很难从中使用服务。这是将它们作为装饰行为的属性来实现的一个基本问题。C#属性不允许您将依赖项传递到它们的构造函数中(常量值除外),并且它们被创建为单实例,因此在应用程序的生命周期中只有一个实例。

在ASPNETCore中,这种限制通常仍然存在,因为过滤器通常创建为添加到控制器类、操作方法和Razor Pages的属性。如果您需要从singleton属性内部访问瞬态或作用域服务,会发生什么?

清单13.13展示了实现这一点的一种方法,即使用伪服务定位器模式进入DI容器并在运行时取出RecipeService。这是可行的,但通常被认为是一种模式,有利于正确的DI。如何将DI添加到过滤器中?

关键是将过滤器一分为二。与其创建既是属性又是筛选器的类,不如创建一个包含功能和属性的筛选器类,该属性告诉框架何时何地使用筛选器。

让我们将其应用于清单13.13中的动作过滤器。之前我从ActionFilterAttribute派生,并从传递给该方法的上下文中获得了RecipeService的实例。在下面的列表中,我展示了两个类:EnsureRecipeExistsFilter和EnsureRecipExistsAttribute。过滤器类负责该功能,并将RecipeService作为构造函数依赖项。

清单13.18 通过不从Attribute派生来在过滤器中使用DI

public class EnsureRecipeExistsFilter : IActionFilter  //不从Attribute类派生
{
//RecipeService被注入构造函数。
private readonly RecipeService _service;
public EnsureRecipeExistsFilter(RecipeService service)
{
_service = service;
}
//方法的其余部分保持不变。
public void OnActionExecuting(ActionExecutingContext context)
{
var recipeId = (int) context.ActionArguments["id"];
if (!_service.DoesRecipeExist(recipeId))
{
context.Result = new NotFoundResult();
}
} public void OnActionExecuted(ActionExecutedContext context) { } //必须实现Executed操作以满足接口要求。
} public class EnsureRecipeExistsAttribute : TypeFilterAttribute //派生自TypeFilter,用于使用DI容器填充依赖项
{
//将类型EnsureRecipeExistsFilt作为参数传递给基TypeFilter构造函数
public EnsureRecipeExistsAttribute() : base(typeof(EnsureRecipeExistsFilter)) {}
}

EnsureRecipeExistsFilter是有效的筛选器;您可以通过将其添加为全局过滤器来单独使用它(因为全局过滤器不需要是属性)。但不能通过修饰控制器类和操作方法直接使用它,因为它不是属性。这就是EnsureRecipeExistsAttribute的来源。

您可以改为使用EnsureRecipeExistsAttribute修饰方法。此属性继承自TypeFilterAttribute,并将要创建的筛选器类型作为参数传递给基构造函数。此属性通过实现IFilterFactory充当EnsureRecipeExistsFilter的工厂。

当ASPNETCore最初加载您的应用程序时,它会扫描您的操作和控制器,查找筛选器和筛选器工厂。它使用这些来为应用程序中的每个操作形成一个过滤管道,如图13.8所示。

图13.8 框架在启动时扫描应用程序,以找到实现IFilterFactory的过滤器和属性。在运行时,框架调用CreateInstance()以获取过滤器的实例。

当调用用EnsureRecipeExistsAttribute修饰的操作时,框架将对该属性调用CreateInstance()。这将创建EnsureRecipeExistsFilter的新实例,并使用DI容器填充其依赖项(RecipeService)。

通过使用这种IFilterFactory方法,您可以两全其美:您可以用属性装饰控制器和操作,也可以在过滤器中使用DI。开箱即用,两个类似的类提供了此功能,它们的行为略有不同:

TypeFilterAttribute——从DI容器加载过滤器的所有依赖项,并使用它们创建过滤器的新实例。
ServiceFilterAttribute——从DI容器加载过滤器本身。DI容器负责服务生命周期和构建依赖关系图。不幸的是,您还必须在“启动”中的ConfigureServices中向DI容器显式注册筛选器:

services.AddTransient<EnsureRecipeExistsFilter>();

您选择使用TypeFilterAttribute还是ServiceFilterAttribute在某种程度上取决于您的偏好,如果需要,您可以始终实现自定义IFilterFactory。关键是您现在可以在过滤器中使用DI。如果不需要将DI用于过滤器,那么为了简单起见,可以直接将其作为属性实现。

提示:使用此模式时,我喜欢将过滤器创建为属性类的嵌套类。这将所有代码都很好地包含在一个文件中,并指示类之间的关系。

这就结束了关于过滤器管道的本章。过滤器是一个有点高级的主题,因为它们对于构建基本应用程序来说并不是绝对必要的,但我发现它们对于确保我的控制器和操作方法简单易懂非常有用。

在下一章中,我们将首先了解如何保护您的应用程序。我们将讨论身份验证和授权之间的区别,ASPNETCore中的身份概念,以及如何使用ASPNETCore身份系统让用户注册和登录应用程序。

总结

过滤器管道作为MVC或RazorPages执行的一部分执行。它由授权筛选器、资源筛选器、操作筛选器、页面筛选器、异常筛选器和结果筛选器组成。每种过滤器类型都分为一个阶段,可用于实现特定于该阶段的效果。
资源、操作和结果筛选器在管道中运行两次:一个*Executingmethod在传入时运行,一个*Executed方法在传出时运行。页面过滤器运行三次:在页面处理程序选择之后,以及页面处理程序执行之前和之后。
授权和异常筛选器仅作为管道的一部分运行一次;它们不会在生成响应后运行。
每种类型的筛选器都有同步和异步版本。例如,资源筛选器可以实现IResourceFilter接口或IAsyncResourceFilter接口。除非过滤器需要使用异步方法调用,否则应使用同步接口。
您可以在控制器级别、Razor Page级别或操作级别全局添加筛选器。这称为过滤器的范围。应该选择哪个范围取决于要应用筛选器的范围。
在给定阶段内,首先运行全局范围的筛选器,然后运行控制器范围的筛选器最后运行操作范围的筛选器。您还可以通过实现IOrderedFilter接口覆盖默认顺序。过滤器将从最低到最高顺序运行,并使用范围来打破联系。
授权过滤器首先在管道中运行,并控制对API的访问。ASPNETCore包含一个[授权]属性,您可以将其应用于操作方法,以便只有登录的用户才能执行操作。
资源筛选器在授权筛选器之后运行,并在执行IActionResult之后再次运行。它们可用于使管道短路,从而永远不会执行动作方法。它们还可以用于自定义操作方法的模型绑定过程。
操作筛选器在模型绑定发生后运行,就在操作方法执行之前。它们也在执行操作方法后运行。它们可用于从操作方法中提取公共代码,以防止重复。它们不为Razor Pages执行,只为MVC控制器执行。
ControllerBase基类还实现IActionFilter和IAsyncActionFilter。无论其他操作筛选器的顺序或范围如何,它们都在操作筛选器管道的开始和结束处运行。它们可用于创建特定于一个控制器的动作筛选器。
页面过滤器运行三次:在页面处理程序选择之后、在模型绑定之后以及在页面处理方法执行之后。您可以将页面筛选器用于与操作筛选器类似的目的。页面过滤器仅对Razor Pages执行;它们不为MVC控制器运行。
RazorPageModels实现IPageFilter和IAsyncPageFilter,因此它们可以用于实现特定于页面的页面过滤器。这些方法很少使用,因为您通常可以使用简单的私有方法获得类似的结果。
当操作方法或页面处理程序引发异常时,异常筛选器在操作和页面筛选器之后执行。它们可用于提供特定于所执行操作的自定义错误处理。
通常,您应该在中间件级别处理异常,但您可以使用异常过滤器自定义如何处理特定操作、控制器或Razor Pages的异常。
结果过滤器在IActionResult执行前后运行。您可以使用它们来控制操作结果的执行方式,或者完全更改将要执行的操作结果。
当您使用授权、资源或异常筛选器缩短管道时,不会执行结果筛选器。通过将结果过滤器实现为IAlwaysRunResultFilter或IAsyncAlwaysRunResultFilter,可以确保结果过滤器也能针对这些短路情况运行。
可以使用ServiceFilterAttribute和TypeFilterAttribute在自定义筛选器中允许依赖注入。ServiceFilterAttribute要求您向DI容器注册筛选器及其所有依赖项,而TypeFilterAttribute只要求已注册筛选器的依赖项。

第13章 MVC和Razor Pages过滤器管道(ASP.NET Core in Action, 2nd Edition)的相关教程结束。

《第13章 MVC和Razor Pages过滤器管道(ASP.NET Core in Action, 2nd Edition).doc》

下载本文的Word格式文档,以方便收藏与打印。