Loading

ASP.NET Core 实战-13.MVC 和 Razor Pages 过滤器管道

了解过滤器以及何时使用它们

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

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

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

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

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

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

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

MVC 过滤器管道

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

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

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

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

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

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

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

Razor Pages 过滤器管道

Razor Pages 框架使用与 API 控制器相同的底层架构,因此过滤器管道几乎相同也就不足为奇了。 管道之间的唯一区别是 Razor Pages 不使用操作过滤器。 相反,他们使用页面过滤器,如图 13.3 所示。

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

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

注意 这些过滤器实际上是 Razor 页面和 MVC 框架之间共享的相同类。 例如,如果您创建一个异常过滤器并在全局范围内注册它,该过滤器将同样适用于您的所有 API 控制器和所有 Razor 页面。

Razor Pages 过滤器管道的不同之处在于它使用页面过滤器而不是操作过滤器。 与其他过滤器类型相比,页面过滤器在过滤器管道中运行了 3 次:

  • 页面处理程序选择后——资源过滤器执行后,根据请求的 HTTP 动词和 {handler} 路由值选择一个页面处理程序,如您在第 5 章中学习的那样。选择页面处理程序后,执行页面过滤器方法首次。在这个阶段你不能短路管道,模型绑定和验证还没有执行。
  • 模型绑定之后——在第一个页面过滤器执行之后,请求被模型绑定到 Razor 页面的绑定模型并被验证。此执行非常类似于 API 控制器的操作过滤器执行。此时,您可以通过返回不同的 IActionResult 来操作模型绑定数据或完全短路页面处理程序的执行。
  • 在页面处理程序执行之后——如果你不短路页面处理程序的执行,页面过滤器会在页面处理程序执行后运行第三次也是最后一次。此时你可以自定义页面处理程序返回的 IActionResult 之前结果被执行。

页面过滤器的三次执行使得可视化管道有点困难,但您通常可以将它们视为增强的操作过滤器。 您可以使用操作过滤器完成的所有操作,都可以使用页面过滤器完成。 另外,如果需要,您可以在页面处理程序选择后挂钩。

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

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

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

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

  • 请求在“进入”的过程中通过中间件组件,响应在“退出”的过程中再次通过。 资源、操作和结果过滤器也是双向的,但授权和异常过滤器只对请求运行一次,而页面过滤器运行 3 次。
  • 中间件可以通过返回响应来缩短请求,而不是将其传递给后面的中间件。 过滤器还可以通过返回响应来使过滤器管道短路。
  • 中间件通常用于横切应用程序关注点,例如日志记录、性能分析、

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

  • 中间件可以为所有请求运行; 过滤器只会针对到达 EndpointMiddleware 并执行 API 控制操作或 Razor 页面的请求运行。
  • 过滤器可以访问 MVC 结构,例如 ModelStateIActionResults。中间件通常独立于 MVC 和 Razor Pages,并且工作在“较低级别”,因此它不能使用这些概念。
  • 过滤器可以很容易地应用于请求的子集; 例如,单个控制器或单个 Razor 页面上的所有操作。 中间件没有这个概念作为一流的想法(尽管您可以使用自定义中间件组件实现类似的东西)。

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

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

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

提示 在可能的情况下,考虑将中间件用于横切关注点。当您需要不同操作方法的不同行为时,或者当功能依赖于 MVC 概念(如 ModelState 验证)时,请使用过滤器。

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

过滤器孤立起来可能有点抽象,因此在下一节中,我们将查看一些代码并学习如何在 ASP.NET Core 中编写自定义过滤器。

创建一个简单的过滤器

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

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

Authorization filters—IAuthorizationFilter or IAsyncAuthorizationFilter
Resource filters—IResourceFilter or IAsyncResourceFilter
Action filters—IActionFilter or IAsyncActionFilter
Page filters—IPageFilter or IAsyncPageFilter
Exception filters—IExceptionFilter or IAsyncExceptionFilter
Result filters—IResultFilter or IAsyncResultFilter

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

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

清单 13.1 实现 IResourceFilter 的示例资源过滤器

public class LogResourceFilter : Attribute, IResourceFilter
{
    //在管道开始时执行,在授权过滤器之后
    public void OnResourceExecuting(
        //上下文包含 HttpContext、路由详细信息和有关当前操作的信息。
        ResourceExecutingContext context)
    {
        Console.WriteLine("Executing!");
    }
    //在模型绑定、动作执行、结果执行之后执行
    public void OnResourceExecuted(
        //包含额外的上下文信息,例如操作返回的 IActionResult
        ResourceExecutedContext context)
    {
        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 实例
        ResourceExecutedContext executedContext = await next();
        //在管道的其余部分执行后调用
        Console.WriteLine("Executed async!");
    }
}

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

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

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

在 13.1.2 节中,我讨论了中间件和过滤器之间的异同。 这些差异之一是过滤器可以限定为特定的操作或控制器,以便它们仅针对某些请求运行。 或者,您可以全局应用过滤器,以便它为每个 MVC 操作和 Razor 页面运行。

通过以不同的方式添加过滤器,您可以获得几种不同的结果。 想象一下,您有一个过滤器,它强制您登录以执行操作。 将过滤器添加到应用程序的方式将显着改变应用程序的行为:

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

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

定义 过滤器的范围是指它适用于多少不同的操作。 过滤器的范围可以限定为操作方法、控制器、Razor 页面或全局。

您将从最具体的范围开始——将过滤器应用于单个操作。 下面的清单显示了一个 MVC 控制器的示例,该控制器具有两种操作方法:一种具有 LogResourceFilter,另一种没有。

清单 13.3 将过滤器应用于操作方法

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

或者,如果您想对每个操作方法应用相同的过滤器,您可以在控制器范围内添加属性,如下面的清单所示。 控制器中的每个 action 方法都会使用 LogResourceFilter,而不必专门装饰每个方法。

清单 13.4 对控制器应用过滤器

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

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

清单 13.5 对 Razor 页面应用过滤器

[LogResourceFilter] //LogResourceFilter 被添加到 Razor Pages PageModel。
public class IndexModel : PageModel
{
    //过滤器适用于页面中的每个页面处理程序。
    public void OnGet()
    {
    }
    public void OnPost()
    {
    }
}

当你的应用程序启动时,框架会自动发现你作为属性应用到控制器、操作和 Razor 页面的过滤器。 对于通用属性,您可以更进一步并全局应用过滤器,而无需装饰单个类。

您添加全局过滤器的方式与控制器或操作范围的过滤器不同 - 通过在启动中配置控制器和 Razor 页面时直接将过滤器添加到 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 页面。 如果您在应用程序中使用 Razor Pages,则配置 MvcOptions 不会出现重载。 相反,您需要使用 AddMvcOptions() 扩展方法来配置过滤器,如下面的清单所示。

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

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

由于可能存在三种不同的作用域,您经常会发现应用了多个过滤器的操作方法:一些直接应用于操作方法,而另一些则从控制器或全局继承。 那么问题就变成了,哪个过滤器首先运行?

了解过滤器的执行顺序

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

在本节中,我们正在考虑给定阶段内过滤器的顺序以及范围如何影响它。 我们将从查看默认订单开始,然后转到根据您自己的要求自定义订单的方法。

默认范围执行命令

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

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

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

Razor Pages 的排序稍微简单一些,因为您只有两个范围 - 全局范围筛选器和 Razor Page 范围筛选器。 对于 Razor 页面,全局范围筛选器首先运行 *ExecutingPageHandlerSelected 方法,然后是页面范围筛选器。 对于 *Executed 方法,过滤器以相反的顺序运行。

您有时会发现您需要对此顺序进行更多控制,尤其是当您在同一范围内应用了多个操作过滤器时。 过滤器管道通过 IOrderedFilter 接口满足此要求。

IORDEREDFILTER 覆盖过滤器执行的默认顺序

过滤器非常适合从控制器操作和 Razor 页面中提取横切关注点,但如果您将多个过滤器应用于一个操作,您通常需要控制它们执行的精确顺序。

Scope 可以为您提供一些帮助,但对于其他情况,您可以实现 IOrderedFilter。 该接口由单个属性 Order 组成:

public interface IOrderedFilter
{
    int Order { get; }
}

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

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

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

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

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

为您的应用程序创建自定义过滤器

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

为了给你一些实际的工作,我们将从第 12 章中用于访问配方应用程序的 Web API 控制器开始。这个控制器包含两个动作:一个用于获取 RecipeDetailViewModel,另一个用于使用新值更新配方。 此清单显示了本章的起点,包括两种操作方法。

清单 13.8 重构以使用过滤器之前的配方 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)
    {
        //如果 API 未启用,则阻止进一步执行。
        if (!IsEnabled) { return BadRequest(); }
        try
        {
            //如果请求的配方不存在,则返回 404 响应。
            if (!_service.DoesRecipeExist(id))
            {
                return NotFound();
            }
            //获取RecipeDetail ViewModel。
            var detail = _service.GetRecipeDetail(id);
            Response.GetTypedHeaders().LastModified =
                detail.LastModified;
            return Ok(detail);
        }
        //如果发生异常,捕获它并以预期的格式返回错误,作为 500 错误。
        catch (Exception ex)
        {
            return GetErrorResponse(ex);
        }
    }
    [HttpPost("{id}")]
    public IActionResult Edit(
        int id, [FromBody] UpdateRecipeCommand command)
    {
        if (!IsEnabled) { return BadRequest(); }
        try
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            if (!_service.DoesRecipeExist(id))
            {
                return NotFound();
            }
            _service.UpdateRecipe(command);
            return Ok();
        }
        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 重构为使用过滤器后的配方 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)
    {
        //动作的意图,返回一个Recipe 视图模型,更加清晰。
        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 中的控制器更容易阅读! 在本节中,您将一点一点地重构控制器,删除横切代码以获得更易于管理的东西。 我们将在本节中创建的所有过滤器都将使用同步过滤器接口——作为练习,我将把它留给你来创建它们的异步对应物。我们将从查看授权过滤器以及它们与安全性的关系开始 在 ASP.NET 核心中。

授权过滤器:保护您的 API

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

定义 身份验证与确定谁发出请求有关。授权与允许用户访问什么有关。

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

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

提示 可以通过实现 IAuthorizationFilterIAsyncAuthorizationFilter 来编写自己的授权过滤器,但我强烈建议不要这样做。 ASP.NET Core 授权框架是高度可配置的,应该可以满足您的所有需求。

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

清单 13.10 添加 [Authorize] 到一个动作方法

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

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

管道中的下一个过滤器是资源过滤器。 在下一节中,您将从 RecipeApiController 中提取一些通用代码,并了解创建短路过滤器是多么容易。

资源过滤器:短路您的操作方法

资源过滤器是 MVC 过滤器管道中的第一个通用过滤器。 在第 13.1.4 节中,您看到了同步和异步资源过滤器的最小示例,它们记录到控制台。 在您自己的应用程序中,您可以将资源过滤器用于多种用途,这要归功于它们在过滤器管道中执行得这么早(和这么晚)。

ASP.NET Core 框架包括一些可以在应用程序中使用的资源过滤器的不同实现:

  • ConsumesAttribute——可用于限制操作方法可以接受的允许格式。 如果您的操作使用 [Consumes("application/json")] 进行修饰,但客户端以 XML 格式发送请求,则资源过滤器将使管道短路并返回 415 Unsupported Media Type 响应。
  • DisableFormValueModelBindingAttribute——该过滤器防止模型绑定绑定到请求正文中的表单数据。 如果您知道某个操作方法将处理您需要手动管理的大文件上传,这将很有用。 资源过滤器在模型绑定之前运行,因此您可以通过这种方式禁用单个操作的模型绑定。

当您想要确保过滤器在模型绑定之前在管道中早期运行时,资源过滤器很有用。 它们为您的逻辑提供了一个早期的管道挂钩,因此您可以在需要时快速短路请求。

回顾清单 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) //在模型绑定之前执行,在过滤器管道的早期
    {
        if (!IsEnabled)
        {
            //如果未启用该功能,则通过设置 context.Result 属性使管道短路
            context.Result = new BadRequestResult();
        }
    }
    //必须实现以满足 IResourceFilter,但在这种情况下不需要
    public void OnResourceExecuted(
        ResourceExecutedContext context) { }
}

这个简单的资源过滤器展示了许多重要的概念,它们适用于大多数过滤器类型:

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

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

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

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

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

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

最重要的是,它们还会在操作方法执行后立即运行,并且可以根据需要完全更改或替换操作返回的 IActionResult。 他们甚至可以处理动作中抛出的异常。

注意 Razor 页面不会执行操作过滤器。 同样,页面过滤器不会为操作方法执行。

ASP.NET Core 框架包括几个开箱即用的操作过滤器。 这些常用过滤器之一是 ResponseCacheFilter,它在您的操作方法响应上设置 HTTP 缓存标头。

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

当您通过从您的操作方法中提取公共代码来构建为您自己的应用程序量身定制的过滤器时,操作过滤器的真正威力就出现了。 为了演示,我将为 RecipeApiController 创建两个自定义过滤器:

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

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

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

图 13.6 使用 Postman 将数据发布到 Web API。 数据绑定到操作方法的绑定模型并经过验证。 如果验证失败,通常会返回带有验证错误列表的 400 Bad Request 响应。
image

清单 13.12 用于验证 ModelState 的操作过滤器

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

这个属性是不言自明的,并且遵循与第 13.2.2 节中的资源过滤器类似的模式,但有一些有趣的点:

  • 我派生自抽象的 ActionFilterAttribute。 此类实现 IActionFilterIResultFilter 以及它们的异步对应项,因此您可以根据需要覆盖所需的方法。 这避免了需要添加未使用的 OnActionExecuted() 方法,但使用基类完全是可选的,并且是一个偏好问题。
  • 动作过滤器在模型绑定发生后运行,因此 context.ModelState 包含验证失败时的验证错误。
  • 在上下文中设置 Result 属性会使管道短路。 但是由于action filter阶段的位置,只绕过了action方法执行和后面的action filter; 管道的所有其他阶段就像操作正常执行一样运行。

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

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

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

以下清单显示了 EnsureRecipeExistsAttribute 操作过滤器。 这使用 RecipeService 的实例来检查 Recipe 是否存在,如果不存在则返回 404 Not Found。

清单 13.13 检查配方是否存在的动作过滤器

public class EnsureRecipeExistsAtribute : ActionFilterAttribute
{
    public override void OnActionExecuting(
        ActionExecutingContext context)
    {
        var service = (RecipeService) context.HttpContext
            .RequestServices.GetService(typeof(RecipeService));
        var recipeId = (int) context.ActionArguments["id"];
        //检查具有给定RecipeId 的Recipe 实体是否存在
        if (!service.DoesRecipeExist(recipeId))
        {
            //如果不存在,则返回 404 Not Found 结果并短路管道
            context.Result = new NotFoundResult();
        }
    }
}

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

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

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

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

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

注意 请记住,您可以在一个阶段中运行多个操作过滤器。 通过设置 context.Result 使管道短路将阻止阶段中的后续过滤器运行,并绕过操作方法执行。

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

清单 13.14 直接在 ControllerBase 上覆盖动作过滤器方法

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

如果您在控制器上覆盖这些方法,它们将在过滤器管道的操作过滤器阶段针对控制器上的每个操作运行。 OnActionExecuting ControllerBase 方法在任何其他操作过滤器之前运行,无论顺序或范围如何,而 OnActionExecuted 方法在所有其他操作过滤器之后运行。

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

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

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

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

答案与我在 13.1.3 节中概述的几乎相同:当您需要特定于 MVC 或仅适用于某些路由的行为时,过滤器非常适合横切关注点。

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

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

您还可以使用异常过滤器以不同方式处理来自不同路由的错误。 想象一下,您的应用程序中同时拥有 Razor Pages 和 Web API 控制器,就像我们在食谱应用程序中所做的那样。 当 Razor 页面抛出异常时会发生什么?

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

这对您的 Razor 页面非常有用,但是您的 Web API 控制器中的异常呢?如果您的 API 抛出异常,并因此返回由异常处理程序中间件生成的 HTML,这将破坏调用 API 并期望 JSON 响应的客户端 !

相反,异常过滤器允许您处理过滤器管道中的异常并生成适当的响应正文。 异常处理程序中间件只拦截没有正文的错误,因此它会让修改后的 Web API 响应原封不动地通过。

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

异常过滤器不仅可以从您的操作方法和页面处理程序中捕获异常。 如果在这些时间发生异常,它们将运行:

  • 在模型绑定或验证期间
  • 当动作方法或页面处理程序正在执行时
  • 执行动作过滤器或页面过滤器时

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

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

清单 13.15 HandleExceptionAttribute 异常过滤器

//ExceptionFilterAttribute 是实现 IExceptionFilter 的抽象基类。
public class HandleExceptionAttribute : ExceptionFilterAttribute
{
    //只有一个方法可以覆盖 IExceptionFilter。
    public override void OnException(ExceptionContext context)
    {
        //构建问题详细信息对象以在响应中返回
        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 页面时,但它们并不总是必需的。 如果您可以使用单个中间件处理应用程序中的所有异常,那么请放弃异常过滤器并改用它。

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

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

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

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

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

  • ProducesAttribute——这会强制将 Web API 结果序列化为特定的输出格式。 例如,使用 [Produces("application/xml")] 装饰您的操作方法会强制格式化程序尝试将响应格式化为 XML,即使客户端没有在其 Accept 标头中列出 XML。
  • FormatFilterAttribute——用这个过滤器装饰一个动作方法告诉格式化程序寻找一个称为格式的路由值或查询字符串参数,并使用它来确定输出格式。 例如,您可以调用 /api/recipe/11?format=json 并且 FormatFilter 会将响应格式化为 JSON,或者调用 api/recipe/11?format=xml 并将响应获取为 XML。

除了控制输出格式化程序外,您还可以使用结果过滤器在执行 IActionResult 并生成响应之前进行任何最后的调整。

作为可用灵活性的一个示例,在下面的清单中,我演示了设置 LastModified 标头,基于从操作返回的对象。这是一个有点做作的示例 - 它对单个操作足够具体,它不保证 被移动到结果过滤器——但希望你明白这一点。

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


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

我在这里使用了另一个帮助器基类 ResultFilterAttribute,因此您只需要重写一个方法即可实现过滤器。 获取当前的 IActionResult,暴露在 context.Result 上,并检查它是否是带有 RecipeDetailViewModel 值的 OkObjectResult 实例。 如果是,则从视图模型中获取 LastModified 字段并将 Last-Modified 标头添加到响应中。

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

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

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

使用 IAlwaysRunResultFilter 短路后运行结果过滤器

结果过滤器旨在“包装”由操作方法或操作过滤器返回的 IActionResult 的执行,以便您可以自定义操作结果的执行方式。 但是,当您通过在授权过滤器、资源过滤器或异常过滤器中设置 context.Result 来短路过滤器管道时,此自定义不适用于 IActionResults 集。

这通常不是问题,因为许多结果过滤器旨在处理“happy path”转换。 但有时您想确保始终将转换应用于 IActionResult,无论它是由操作方法返回还是由短路过滤器返回。

对于这些情况,您可以实现 IAlwaysRunResultFilterIAsyncAlwaysRunResultFilter。 这些接口扩展(并且相同)到标准结果过滤器接口,因此它们就像过滤器管道中的普通结果过滤器一样运行。 但是这些接口将过滤器标记为在授权过滤器、资源过滤器或异常过滤器使管道短路后也运行,而标准结果过滤器将不会运行。

您可以使用 IAlwaysRunResultFilter 来确保始终更新某些操作结果。 例如,文档显示了如何使用 IAlwaysRunResultFilter 将 415 StatusCodeResult 转换为 422 StatusCodeResult,而不管操作结果的来源。 请参阅 Microsoft 的“ASP.NET Core 中的过滤器”文档的“IAlwaysRunResultFilter 和 IAsyncAlwaysRunResultFilter”部分:http://mng.bz/JDo0。

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

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

还有一个过滤器我们还没有研究,因为它只适用于 Razor 页面:页面过滤器。

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

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

与操作过滤器的情况一样,ASP.NET Core 框架包括几个开箱即用的页面过滤器。 其中之一是缓存操作过滤器 ResponseCacheFilter 的 Razor Page 等效项,称为 PageResponseCacheFilter。 这与我在第 13.2.3 节中描述的等效操作过滤器的工作方式相同,在 Razor 页面响应上设置 HTTP 缓存标头。

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

例如,以下清单显示了与 EnsureRecipeExistsAttribute 操作过滤器等效的页面过滤器。

清单 13.17 一个页面过滤器来检查一个 Recipe 是否存在

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

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

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

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

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

正如 ControllerBase 类实现 IActionFilter 一样,PageModel 实现 IPageFilterIAsyncPageFilter。 如果要为单个 Razor 页面创建操作筛选器,则可以省去创建单独页面筛选器的麻烦,并直接在 Razor 页面中覆盖这些方法。

提示 我通常发现除非你有一个非常常见的需求,否则使用页面过滤器是不值得的。 添加的额外级别的间接页面过滤器,再加上单个 Razor 页面的典型定制性质,意味着我通常认为它们不值得使用。 当然,您的里程可能会有所不同,但不要将它们作为第一选择。

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

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

了解管道短路

在这个简短的部分中,您将了解过滤器管道短路的详细信息。您将看到管道短路时某个阶段的其他过滤器会发生什么,以及如何短路每种类型的过滤器 .

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

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

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

了解当您短路一个过滤器时哪些其他过滤器将运行可能有点麻烦,但我在表 13.1 中总结了每个过滤器。 在考虑短路时,您还会发现参考图 13.2 和 13.3 来可视化管道的形状很有用。

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

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

过滤器类型 如何短路? 还运行什么?
Authorization filters 设置 context.Result 仅绕过 IAlwaysRunResultFilters.
Resource filters 设置 context.Result 资源过滤器 *在执行 IActionResult 之前,使用 context.Cancelled = true.Runs IAlwaysRunResultFilters 运行早期过滤器中执行的函数。
Action filters 设置 context.Result 仅绕过操作方法执行。 管道中较早的操作过滤器使用 context.Cancelled = true 运行它们的 *Executed 方法,然后结果过滤器、结果执行和资源过滤器的 *Executed 方法都正常运行。
Page filters OnPageHandlerSelected 设置 context.Result 仅绕过页面处理程序执行。 管道中较早的页面过滤器使用 context.Cancelled = true 运行它们的 *Executed 方法,然后结果过滤器、结果执行和资源过滤器的 *Executed 方法都正常运行。
Exception filters 设置 context.ResultException.Handled =true 所有资源过滤器 * 执行的函数都会运行。 在执行 IActionResult 之前运行 IAlwaysRunResultFilters
Result filters 设置 context.Result 管道中较早的结果过滤器使用 context.Cancelled = true 运行它们的 Executed 函数。 所有资源过滤器执行的功能都正常运行。

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

在本章中我想讨论的最后一件事是如何将 DI 与过滤器一起使用。您在第 10 章中看到 DI 是 ASP.NET Core 不可或缺的,在下一节中您将看到如何设计您的过滤器。 过滤器,以便框架可以为您注入服务依赖项。

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

在本节中,您将学习如何将服务注入您的过滤器,以便您可以在过滤器中利用 DI 的简单性。 您将学习使用两个辅助过滤器来实现此目的,TypeFilterAttributeServiceFilterAttribute,您将了解如何使用它们来简化您在第 13.2.3 节中定义的操作过滤器。

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

在 ASP.NET Core 中,此限制通常仍然存在,因为过滤器通常创建为添加到控制器类、操作方法和 Razor 页面的属性。 如果您需要从单例属性内部访问瞬态或范围服务会发生什么?

清单 13.13 展示了一种方法,使用伪服务定位器模式进入 DI 容器并在运行时提取 RecipeService。 这可行,但通常不赞成作为一种模式,有利于适当的 DI。 如何将 DI 添加到过滤器中?

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

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

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

public class EnsureRecipeExistsFilter : IActionFilter //不是从 Attribute 类派生的
{
    private readonly RecipeService _service;
    //RecipeService 被注入到构造函数中。
    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) { }
}
//派生自 TypeFilter,用于使用 DI 容器填充依赖项
public class EnsureRecipeExistsAttribute : TypeFilterAttribute
{
    //将类型 EnsureRecipeExistsFilter 作为参数传递给基本 TypeFilter 构造函数
    public EnsureRecipeExistsAttribute()
        : base(typeof(EnsureRecipeExistsFilter)) {}
}

EnsureRecipeExistsFilter 是一个有效的过滤器; 您可以通过将其添加为全局过滤器来单独使用它(因为全局过滤器不需要是属性)。 但是你不能直接通过装饰控制器类和动作方法来使用它,因为它不是一个属性。 这就是 EnsureRecipeExistsAttribute 的用武之地。

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

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

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

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

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

  • TypeFilterAttribute——从 DI 容器加载过滤器的所有依赖项,并使用它们创建过滤器的新实例。
  • ServiceFilterAttribute——从 DI 容器中加载过滤器本身。 DI 容器负责服务生命周期并构建依赖关系图。不幸的是,您还必须在 StartupConfigureServices 中使用 DI 容器显式注册您的过滤器:
services.AddTransient<EnsureRecipeExistsFilter>();

无论您选择使用 TypeFilterAttribute 还是 ServiceFilterAttribute 都是一个偏好问题,如果需要,您始终可以实现自定义 IFilterFactory。 关键点是您现在可以在过滤器中使用 DI。 如果您不需要将 DI 用于过滤器,那么为简单起见,直接将其实现为属性。

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

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

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

总结

  • 过滤器管道作为 MVC 或 Razor Pages 执行的一部分执行。 它由授权过滤器、资源过滤器、操作过滤器、页面过滤器、异常过滤器和结果过滤器组成。 每个过滤器类型都被分组到一个阶段,并且可以用于实现特定于该阶段的效果。

  • 资源过滤器、操作过滤器和结果过滤器在管道中运行两次:一个Executing 方法在进入的过程中,一个Executing 方法在退出过程中。 页面过滤器运行 3 次:在页面处理程序选择之后,以及在页面处理程序执行之前和之后。

  • 授权和异常过滤器仅作为管道的一部分运行一次;它们在生成响应后不会运行。

  • 每种类型的过滤器都有同步和异步版本。例如,资源过滤器可以实现 IResourceFilter 接口或 IAsyncResourceFilter 接口。除非您的过滤器需要使用异步方法调用,否则您应该使用同步接口。

  • 您可以在全局、控制器级别、Razor 页面级别或操作级别添加过滤器。这称为过滤器的范围。您应该选择哪个范围取决于您希望应用过滤器的范围。

  • 在给定的阶段内,全局范围的过滤器首先运行,然后是控制器范围的,最后是动作范围的。您还可以通过实现 IOrderedFilter 接口来覆盖默认顺序。过滤器将从最低到最高顺序运行,并使用范围来打破关系。

  • 授权过滤器首先在管道中运行并控制对 API 的访问。ASP.NET Core 包含一个 [Authorization] 属性,您可以将其应用于操作方法,以便只有登录用户才能执行操作。

  • 资源过滤器在授权过滤器之后运行,并在 IActionResult 执行后再次运行。它们可用于使管道短路,从而永远不会执行操作方法。它们还可以用于自定义操作方法的模型绑定过程。

  • 动作过滤器在模型绑定发生之后运行,就在动作方法执行之前。它们也在 action 方法执行后运行。它们可用于从操作方法中提取公共代码以防止重复。它们不会为 Razor 页面执行,仅适用于 MVC 控制器。

  • ControllerBase 基类还实现了 IActionFilterIAsyncActionFilter。它们在动作过滤器管道的开始和结束处运行,与其他动作过滤器的顺序或范围无关。它们可用于创建特定于一个控制器的操作过滤器。

  • 页面过滤器运行 3 次:页面处理程序选择后、模型绑定后和页面处理程序方法执行后。您可以将页面过滤器用于与操作过滤器类似的目的。页面过滤器仅对 Razor 页面执行;它们不适用于 MVC 控制器。

  • Razor Page PageModels 实现了 IPageFilterIAsyncPageFilter,因此它们可用于实现特定于页面的页面过滤器。这些很少使用,因为您通常可以使用简单的私有方法获得类似的结果。

  • 当动作方法或页面处理程序抛出异常时,异常过滤器在动作和页面过滤器之后执行。它们可用于提供特定于所执行操作的自定义错误处理。

  • 通常,您应该在中间件级别处理异常,但您可以使用异常过滤器来自定义如何处理特定操作、控制器或 Razor 页面的异常。

  • 结果过滤器在执行 IActionResult 之前和之后运行。您可以使用它们来控制操作结果的执行方式,或者完全更改将要执行的操作结果。

  • 当您使用授权、资源或异常过滤器使管道短路时,不会执行结果过滤器。您可以通过将结果过滤器实现为 IAlwaysRunResultFilterIAsyncAlwaysRunResultFilter 来确保结果过滤器也针对这些短路情况运行。

  • 您可以使用 ServiceFilterAttributeTypeFilterAttribute 来允许在您的自定义过滤器中注入依赖项。 ServiceFilterAttribute 要求您在 DI 容器中注册您的过滤器及其所有依赖项,而 TypeFilterAttribute 只要求已注册过滤器的依赖项。

posted @ 2023-01-20 10:43  F(x)_King  阅读(304)  评论(0编辑  收藏  举报