Pro ASP.NET Core MVC2(第7版)翻译

第19章:过滤器

作者:Adam Freeman 翻译:陈广 日期:2018-10-2


过滤器为 MVC 请求处理注入了额外的逻辑。它们提供了一种简单而优雅的方法以实现横切关注点(crosscutting concerns) —— 这个术语指的是在整个应用程序中都使用的某些功能,并且不太适合放在任何一个地方,因为在那里它会打破关注点的分离。横切关注点的典型例子是日志记录、授权和缓存。本章我将向您展示 MVC 支持的不同类别的过滤器,如何创建和使用自定义过滤器,以及如何控制它们的执行。表19-1为过滤器简介。

表 19-1:过滤器简介

问题 回答
它们是什么? 过滤器用于将逻辑应用于 action 方法,而不必向控制器类添加代码。
它们有何用途? 过滤器允许应用不属于 action 的经典 MVC 模式定义的代码。结果是可以在整个应用程序中应用的简单控制器类和可重用功能。
如何使用它们 MVC 以不同的方式使用不同类型的过滤器。创建过滤器的最常见方法是创建一个类,该类需要继承 MVC 为所需过滤器类型提供的特性。
是否有任何缺陷或限制? 不同类型的过滤器提供的功能重叠,很难确定需要哪种类型。
有没有其他选择? 没有,过滤器是 MVC 的核心特性,用于实现需要的常见功能,如授权。

表 19-2:本章摘要

问题 解决方案 清单
向请求处理中注入额外的逻辑 将过滤器应用于控制器或其 action 方法 6-9
限制对 action 的访问 使用授权过滤器 10,11
在请求处理过程中注入通用逻辑 使用 action 过滤器 12-14
检查或改变 action 方法产生的结果 使用结果过滤器 15-19
处理错误 使用异常过滤器 20,21
在过滤器中使用服务 在过滤器构造函数中声明依赖项,在Startup类中注册服务,并使用TypeFilter特性应用过滤器。 22-26
将过滤器置于生命周期管理之下 使用依赖注入生命周期在Startup类中注册过滤器,并使用ServiceFilter特性应用过滤器。 27-29
将过滤器应用于应用程序中的每个 action 方法 使用全局过滤器 30-32
更改执行的过滤器的顺序 使用Order参数 33-36

准备示例项目

对于本章,我采用了与最近几章相同的方法来创建示例应用程序。我使用【ASP.NET Core Web 应用程序(.NET Core)】模板创建了一个名为 Filters 的新的空项目。清单19-1显示了我对Startup类所做的更改,以启用 MVC 框架和开发所需的其他中间件。

清单 19-1:Filters 文件夹下的 Startup.cs 文件的内容

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Filters
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

启用 SSL

本章中的一些示例需要使用 SSL,而 SSL 在默认情况下是禁用的。要启用 SSL,请从 Visual Studio 【项目】菜单中选择【Filter 属性】,并检查调试选项卡中的【启用 SSL】选项,如图19-1所示。记录指定的端口,每个项目都会有不同的端口。

译者注:安装 .NET 2.1 后,Visual Studio 已经默认启用 SSL,所以上面步骤可以不做。

图19-1 启用 SSL

创建控制器和视图

本章中的控制器很简单,因为重点是将逻辑放在应用程序的其他位置。我创建了 Controllers 文件夹,添加了一个名为 HomeController.cs 的类文件,并使用它定义了如清单19-2所示的控制器。

清单 19-2:Controllers 文件夹下的 HomeController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;

namespace Filters.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Message",
            "This is the Index action on the Home controller");
    }
}

action 方法渲染一个名为 Message 的视图,并将一个字符串作为视图数据传递。我创建了 Views/Shared 文件夹,并添加了一个名为 Message.cshtml 的 Razor 视图文件,其标记如清单19-3所示。

清单 19-3:Views/Shared 文件夹下的 Message.cshtml 文件的内容

@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Filters</title>
    <link asp-href-include="lib/twitter-bootstrap/css/*.min.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    @if (Model is string)
    {
        @Model
    }
    else if (Model is IDictionary<string, string>)
    {
        var dict = Model as IDictionary<string, string>;
        <table class="table table-sm table-striped table-bordered">
            <thead><tr><th>Name</th><th>Value</th></tr></thead>
            <tbody>
                @foreach (var kvp in dict)
                {
                    <tr><td>@kvp.Key</td><td>@kvp.Value</td></tr>
                }
            </tbody>
        </table>
    }
</body>
</html>

此视图是弱类型的,将显示一个字符串或通过表格显示一个Dictionary<string, string>

视图依赖于 Bootstrap CSS 包来设置 HTML 元素的样式。为了将 Bootstrap 程序添加到项目中,我在 ControllersAndActions 项目中单击鼠标右键,在弹出菜单中选择【添加】➤【添加客户端库】,并将 twitter-bootstrap 添加至项目中。最终生成的 libman.json 配置文件代码清单18-7所示:

清单 19-4:Filters 文件夹下的 libman.json 文件的内容

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    }
  ]
}

最后的准备工作是在 Views 文件夹中创建 _ViewImports.cshtml 文件,该文件设置内置标签助手,以便在 Razor 视图中使用,如清单19-5所示。

清单 19-5:Views 文件夹下的 _ViewImports.cshtml 文件的内容

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

运行应用程序,您将看到如图 19-2 所示的输出。

图19-2 运行示例应用程序

提示:可能会提示您信任 Visual Studio生成的证书。接受此选项,该选项与本章中依赖 SSL 的示例相关。

使用过滤器

过滤器允许将本应应用于 action 方法的逻辑从控制器中移除并在可重用类中定义。例如,假设我希望确保 action 方法只能使用 HTTPS 访问,而不是使用常规的非加密 HTTP 访问。HttpRequest context 对象提供了我需要的信息,以确定是否使用 HTTPS,如清单19-6所示。

清单 19-6:Controllers 文件夹下的 HomeController.cs 文件,测试

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

namespace Filters.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            if (!Request.IsHttps)
            {
                return new StatusCodeResult(StatusCodes.Status403Forbidden);
            }
            else
            {
                return View("Message",
                    "This is the Index action on the Home controller");
            }
        }
    }
}

这就是在没有过滤器的情况下处理 HTTPS 问题的方法。如果运行应用程序,浏览器将请求该项目的非 non-HTTPs 默认 URL,Index action 方法通过返回StatusCodeResult来处理该项目,它在响应中发送 HTTP 403 状态代码(如第17章所述)。如果请求 HTTPS 默认 URL(对我来说是 https://localhost:44316/),则Index action 方法将通过渲染 Message 视图来响应(在浏览器显示结果之前,您可能需要确认一个安全警告)。图19-3显示了这两种结果.

图19-3 限制对 HTTPS 请求的访问

提示:如果没有从本节中的示例中获得预期的结果,则清除浏览器的历史记录。浏览器通常拒绝向产生 SSL 错误的服务器发送请求,这是一个很好的安全实践,但在开发过程中可能会令人沮丧。

清单19-6中的代码工作正常,但存在问题。第一个问题是,action 方法包含的代码更多的是实现安全策略,而不是处理请求、更新模型和选择响应。一个更严重的问题是,在 action 方法中包含 HTTP 检测代码不能很好地扩展,必须在控制器中的每个 action 方法中重复,如清单19-7所示。

清单 19-7:Controllers 文件夹下的 HomeController.cs 文件,添加一个 Action 方法

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

namespace Filters.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            if (!Request.IsHttps)
            {
                return new StatusCodeResult(StatusCodes.Status403Forbidden);
            }
            else
            {
                return View("Message",
                    "This is the Index action on the Home controller");
            }
        }

        public IActionResult SecondAction()
        {
            if (!Request.IsHttps)
            {
                return new StatusCodeResult(StatusCodes.Status403Forbidden);
            }
            else
            {
                return View("Message",
                "This is the SecondAction action on the Home controller");
            }
        }
    }
}

我必须记住在每个控制器中的每个 action 方法中实现相同的检查,我希望对每个控制器都需要使用 HTTPS。实现安全策略的代码是简单控制器的一个重要部分,这使得控制器更难理解,而且我忘记将它添加到新的 action 方法中只是时间问题,在我的安全策略中造成了一个漏洞。这是过滤器可以解决的问题,如清单19-8所示。

清单 19-8:Controllers 文件夹下的 HomeController.cs 文件,应用过滤器

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

namespace Filters.Controllers
{
    public class HomeController : Controller
    {
        [RequireHttps]
        public ViewResult Index() => View("Message",
            "This is the Index action on the Home controller");

        [RequireHttps]
        public ViewResult SecondAction() => View("Message",
            "This is the SecondAction action on the Home controller");
    }
}

RequireHttps特性将内置过滤器之一应用于HomeController类。它限制了对 action 方法的访问,因此只支持 HTTPS 请求,并让我可以将每个方法中的安全代码删除,专注于处理成功的请求。

注意RequireHttps过滤器的工作方式与清单19-7中的自定义代码不完全相同。对于 GET 请求,RequireHttps特性将客户端重定向到最初请求的 URL,但是它通过使用https架构来重定向客户机,以便将对 http://localhost/Home/Index 的请求重定向到 https://localhost/Home/Index 。对于大多数已部署的应用程序来说,这是有意义的,但在开发期间则不然,因为 HTTP 和 HTTPS 位于不同的本地端口上。RequireHttpsAttribute类定义了一个名为HandleNonHttpsRequest的受保护方法,您可以重写该方法来更改行为。或者,我在《使用授权过滤器》这一节从头开始重新创建原始功能。

当然,我仍然需要记住对每个 action 方法应用RequireHttps特性,这意味着我可能会忘记。但是过滤器有一个有用的技巧:将属性应用于控制器类与将其应用于每个单独的 action 方法具有相同的效果,如清单19-9所示。

清单 19-9:Controllers 文件夹下的 HomeController.cs 文件,将过滤器应用到所有 Action 方法

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

namespace Filters.Controllers
{
    [RequireHttps]
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Message",
            "This is the Index action on the Home controller");

        public ViewResult SecondAction() => View("Message",
            "This is the SecondAction action on the Home controller");
    }
}

过滤器可以使用不同级别的粒度。如果您希望限制对某些 action 的访问,但不希望限制其他操作,那么您可以只对这些方法应用RequireHttps特性。如果您想保护所有的 action 方法,包括将来添加到控制器中的任何方法,那么RequireHttps特性可以应用到类中。如果要将过滤器应用于应用程序中的每个 action,则可以使用全局过滤器,我将在本章后面对此进行描述。

理解过滤器

现在您已经了解了如何使用过滤器,现在是时候解释幕后发生的事情了。过滤器实现了IFilterMetadata接口,它位于Microsoft.AspNetCore.Mvc.Filters命名空间中。以下是定义:

namespace Microsoft.AspNetCore.Mvc.Filters {
    public interface IFilterMetadata { }
}

接口是空的,不需要过滤器类来实现任何特定的行为。这是因为有几种不同类型的过滤器,每种过滤器都以不同的方式工作,并用于不同的用途。

表19-3列出了每种类型的过滤器、定义它们的接口以及它们所做的事情(MVC 还支持一些其他类型的过滤器,但它们不是直接使用的。相反,它们被集成到我在其他章节中描述的功能中,并通过特定的特性应用,包括我在第20章中描述的ProducesConsumes特性)。

表 19-2:本章摘要

过滤器 接口 描述
Authorization IAuthorizationFilter
IAsyncAuthorizationFilter
此类型的过滤器用于应用程序的安全策略,包括用户授权。
Action IActionFilter
IAsyncActionFilter
此类型的过滤器用于在 action 方法之前或之后立即执行工作。
Result IResultFilter
IAsyncResultFilter
此类型的过滤器用于在处理 action 方法的结果之前或之后立即执行工作。
Exception IExceptionFilter
IAsyncExceptionFilter
此类型的过滤器用于处理异常

表中的描述是模糊的,因为您可以使用过滤器来处理范围广泛的任务,这仅受您的想象力和需要解决的问题的限制。随着我深入讲解过滤器是如何工作的,这一点将变得很清楚,但就目前而言,有两个重要的地方需要理解。

首先,表19-3中每种类型的过滤器都有两个不同的接口.过滤器可以同步或异步地完成它们的工作,例如,一个同步结果过滤器实现IResultFilter接口,而一个异步结果过滤器实现IAsyncResultFilter接口。

第二,过滤器按特定顺序执行。首先执行授权过滤器,然后执行 action 文件,然后执行结果过滤器。只有当抛出异常时才执行异常筛选器,这会破坏正常的序列。

获取 Context 数据

过滤器以FilterContext对象的形式提供 context 数据。FilterContext类派生自ActionContext,它也是我在第17章中描述的ControllerContext类的基类。为了方便起见,表19-4列出了从ActionContext类继承的属性以及FilterContext定义的附加属性。

表 19-4:FilterContext 属性

名称 描述
ActionDescriptor 此属性返回一个描述 action 方法的ActionDescriptor对象
HttpContext 此属性返回一个HttpContext对象,该对象提供将作为返回发送的 HTTP 请求和 HTTP 响应的详细信息。
ModelState 如第27章所述,此属性返回一个用于验证客户端发送的数据的ModelStateDictionary对象。
RouteData 此属性返回一个RouteData对象,该对象描述路由系统处理请求的方式,如第15章所述。
Filters 此属性返回已应用于action方法的过滤器列表,表示为IList<IFilterMetadata>

使用授权过滤器

授权过滤器用于实现应用程序的安全策略。授权过滤器在其他类型的过滤器之前和 action 方法之前执行。以下是IAuthorizationFilter接口的定义:

namespace Microsoft.AspNetCore.Mvc.Filters {

    public interface IAuthorizationFilter : IFilterMetadata {

        void OnAuthorization(AuthorizationFilterContext context);
    }
}

调用OnAuthorization方法为过滤器提供授权请求的机会。对于异步授权过滤器,下面是IAsyncAuthorizationFilter接口的定义:

using System.Threading.Tasks;
    namespace Microsoft.AspNetCore.Mvc.Filters {

        public interface IAsyncAuthorizationFilter : IFilterMetadata {

            Task OnAuthorizationAsync(AuthorizationFilterContext context);
    }
}

调用OnAuthorizationAsync方法,以便过滤器可以授权请求。无论使用哪种接口,过滤器都通过AuthorizationFilterContext对象接收描述请求的 context 数据,该对象来自FilterContext类,并添加了一个重要属性,如表19-5所述。

表 19-5:AuthorizationFilterContext 属性

名称 描述
Result 当请求不符合应用程序的授权策略时,授权过滤器将设置此IActionResult属性。如果设置了该属性,则 MVC 将渲染IActionResult,而不是调用 action 方法。

创建授权过滤器

为了演示授权过滤器是如何工作的,我在示例项目中创建了一个 Infrastructure 文件夹,添加了一个名为 HttpsOnlyAttribute.cs 的类文件,并使用它来定义清单19-10所示的过滤器。

清单 19-10:Infrastructure 文件夹下的 HttpsOnlyAttribute.cs 文件内容

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Filters.Infrastructure
{
    public class HttpsOnlyAttribute : Attribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            if (!context.HttpContext.Request.IsHttps)
            {
                context.Result =
                    new StatusCodeResult(StatusCodes.Status403Forbidden);
            }
        }
    }
}

如果请求符合授权策略,则授权筛选器什么也不做,这种不作为允许 MVC 转移到下一个筛选器,并最终执行 action 方法。

注意:可用于限制对特定用户和组的访问的Authorize特性是作为过滤器实现的,但在 ASP.NET Core MVC 中不再是这种情况。Authorize特性仍在使用,但它以不同的方式工作。在幕后,使用全局过滤器(我在本章后面描述全局过滤器)检测Authorize特性并执行 ASP.NET Core 标识系统定义的策略,但Authorize特性不是过滤器,也不实现IAuthorizationFilter接口。我在第29章中描述了如何使用 ASP.NET Core 标识和Authorize特性。

如果存在问题,则筛选器将设置传递给OnAuthorization方法的AuthorizationFilterContext对象的Result属性。这防止了进一步的执行,并为 MVC 提供了返回客户端的结果。在清单中,我的HttpsOnlyAttribute类检查HttpRequest context 对象的IsHttps属性,并在请求没有使用HTTPS的情况下设置Result属性以中断执行。清单19-11显示了应用于 Home 控制器的新过滤器。

清单 19-11:Controllers 文件夹下的 HomeController.cs 文件,应用自定义过滤器

using Microsoft.AspNetCore.Mvc;
using Filters.Infrastructure;

namespace Filters.Controllers
{
    [HttpsOnly]
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Message",
            "This is the Index action on the Home controller");

        public ViewResult SecondAction() => View("Message",
            "This is the SecondAction action on the Home controller");
    }
}

这个过滤器重新创建了我在清单19-7中的 action 方法中包含的功能。这在实际项目中并不比重定向(比如内置的RequireHttps过滤器)更有用,因为用户不会理解 403 状态码的含义,但它确实提供了授权过滤器如何工作的有用示例。


过滤器的单元测试

过滤器单元测试的大部分工作是设置传递给过滤器的方法的 context 对象。所需的模拟量取决于过滤器使用的 context 信息。作为一个例子,下面是清单19-10中的HttpsOnly过滤器的单元测试:

using System.Linq;
using Filters.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Moq;
using Xunit;

namespace Tests {
    public class FilterTests {
        [Fact]
        public void TestHttpsFilter() {
            // Arrange
            var httpRequest = new Mock<HttpRequest>();
            httpRequest.SetupSequence(m => m.IsHttps).Returns(true)
                .Returns(false);

            var httpContext = new Mock<HttpContext>();
            httpContext.SetupGet(m => m.Request).Returns(httpRequest.Object);

            var actionContext = new ActionContext(httpContext.Object,
                new Microsoft.AspNetCore.Routing.RouteData(),
                new ActionDescriptor());

            var authContext = new AuthorizationFilterContext(actionContext,
                Enumerable.Empty<IFilterMetadata>().ToList());

            HttpsOnlyAttribute filter = new HttpsOnlyAttribute();

            // Act and Assert
            filter.OnAuthorization(authContext);
            Assert.Null(authContext.Result);

            filter.OnAuthorization(authContext);
            Assert.IsType(typeof(StatusCodeResult), authContext.Result);
            Assert.Equal(StatusCodes.Status403Forbidden,
                (authContext.Result as StatusCodeResult).StatusCode);
        }
    }
}

首先,我模拟HttpRequestHttpContext context 对象,它允许我提交一个带有或不带 HTTPS 的请求。我想测试这两种情况,我喜欢这样做:

...
httpRequest.SetupSequence(m => m.IsHttps).Returns(true).Returns(false);
...

该语句设置HttpRequest.IsHttps属性,以便返回一系列值:该属性在第一次读取时返回true,第二次读取时返回false。一旦我有了HttpContext对象,我就可以使用它来创建ActionContext对象,它允许我创建完成单元测试所需的AuthorizationContext对象。通过检查AuthorizationFilterContext对象的Result属性,我测试过滤器如何响应非 HTTPS 请求,然后测试 HTTP 请求发生了什么。设置AuthorizationFilterContext对象需要很多类型,它们依赖于许多 ASP.NET Core 和 MVC 命名空间,但是一旦您拥有了 context 对象,那么编写测试的其余部分就相对简单了。


使用 Action 过滤器

理解 action 过滤器的最好方法是查看定义它们的接口。下面是IActionFilter接口:

namespace Microsoft.AspNetCore.Mvc.Filters {

    public interface IActionFilter : IFilterMetadata {

        void OnActionExecuting(ActionExecutingContext context);
        void OnActionExecuted(ActionExecutedContext context);
    }
}

当一个 action 过滤器被应用到一个 action 方法时,在 action 方法被调用之前调用OnActionExecuting方法,然后调用OnActionExecuted方法。action 过滤器通过两个不同的 context 类提供 context 数据:OnActionExecuting方法的ActionExecutingContext,以及OnActionExecuted方法的的ActionExecutedContext。这两个 context 类都扩展了FilterContext类,我在表19-4中对此进行了描述。

ActionExecutingContext类用于描述即将被调用的 action,它定义了表19-6中描述的附加属性。

表 19-6:ActionExecutingContext 属性

名称 描述
Controller 此属性返回即将调用其 action 方法的控制器。(action 方法的详细信息可以通过从基类继承的ActionDescriptor属性获得)
ActionArguments 此属性返回将传递给 action 方法的参数的字典,并按名称进行索引。过滤器可以插入、删除或更改参数。
Result 如果过滤器为该属性分配了一个IActionResult,那么请求进程就会短路,并且 action result 将用于在不调用 action方 法的情况下生成对客户端的响应。

ActionExecutedContext类用于表示已执行的 action,并定义表19-7中描述的属性。

表 19-7:ActionExecutedContext 属性

名称 描述
Controller 此属性返回将调用其 action 方法的控制器对象
Canceled 如果另一个 action 过滤器通过将 action result 分配给ActionExecutingContext对象的Result属性,从而使请求处理过程短路,则此bool属性设置为true
Exception 此属性包含 action 方法引发的任何异常
ExceptionDispatchInfo 此方法返回一个ExceptionDispatchInfo对象,该对象包含 action 方法引发的任何异常的堆栈跟踪详细信息。
ExceptionHandled 将此属性设置为true表示筛选器已处理异常,该异常将不再传播。
Result 此属性返回由 action 方法返回的IActionResult。如果需要,过滤器可以更改或替换 action result。

创建一个 Action 过滤器

Action 过滤器是一种通用工具,可用于实现应用程序中的任何横切关注点。Action 过滤器可用于在调用 action 之前中断请求进程,并在执行 action 后更改结果。创建 action 过滤器的最简单方法是从 ActionFilterAttribute 类派生一个类,该类实现了 IActionFilter 接口。为了演示,我在 Infrastructure 文件夹中添加了一个名为 ProfileAttribute.cs 的类文件,并使用它来定义清单19-12所示的过滤器。

清单 19-12:Infrastructure 文件夹下的 ProfileAttribute.cs 文件的内容

using System.Diagnostics;
using System.Text;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Filters.Infrastructure
{
    public class ProfileAttribute : ActionFilterAttribute
    {
        private Stopwatch timer;

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            timer = Stopwatch.StartNew();
        }
        public override void OnActionExecuted(ActionExecutedContext context)
        {
            timer.Stop();
            string result = "<div>Elapsed time: "
                + $"{timer.Elapsed.TotalMilliseconds} ms</div>";
            byte[] bytes = Encoding.ASCII.GetBytes(result);
            context.HttpContext.Response.Body.Write(bytes, 0, bytes.Length);
        }
    }
}

在清单中,我使用Stopwatch对象来度量 action 方法执行所需的毫秒数,方法是在OnActionExecuting方法中启动一个计时器,然后在OnActionExecuted方法中停止它。为了记录结果,我使用 context 对象获取HttpResponse,并在响应中包含一个简单的 HTML 片段。

清单19-13显示了应用于 Home 控制器的Profile特性(我还删除了以前的过滤器,以便接受标准 HTTP 的请求。)

提示:这是一个怪事,控制器也是 action 过滤器。Controller基类实现了IActionFilterIAsyncActionFilter接口,这意味着您可以覆盖这些接口定义的方法来创建 action 过滤器功能。对于 POCO 控制器,MVC 检查类并检查它们是否实现了 action 过滤器接口中的任何一个,并自动使用它们作为 action 过滤器。

清单 19-13:Controllers 文件夹下的 HomeController.cs 文件,应用过滤器

using Microsoft.AspNetCore.Mvc;
using Filters.Infrastructure;

namespace Filters.Controllers
{
    [Profile]
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Message",
            "This is the Index action on the Home controller");

        public ViewResult SecondAction() => View("Message",
            "This is the SecondAction action on the Home controller");
    }
}

如果运行应用程序,您将看到如图19-4所示的消息。您看到的毫秒数将根据开发机器的速度而有所不同。

图19-4 使用 action 过滤器

注意:将 HTML 片段直接写入响应依赖于浏览器对格式错误的 HTML 文档的容忍程度:我在过滤器中生成的div元素出现在响应 body 的开头,在DOCTYPEhtml元素之前,它们指示由 Razor 视图生成的 HTML 文档的开始部分。这种技术可以工作,对于生成诊断信息也很有用,但是不应使用于生产。

创建异步 action 过滤器

IAsyncActionFilter接口用于定义异步操作的 action 过滤器。下面是接口的定义:

using System.Threading.Tasks;

    namespace Microsoft.AspNetCore.Mvc.Filters {

        public interface IAsyncActionFilter : IFilterMetadata {

            Task OnActionExecutionAsync(ActionExecutingContext context,
                ActionExecutionDelegate next);
    }
}

有一个方法依赖于 task continuation,允许在执行 action 方法之前和之后运行过滤器。清单19-14展示了在Profile过滤器中使用OnActionExecutionAsync方法的情况。

清单 19-14:Infrastructure 文件夹下的 ProfileAttribute.cs 文件,创建异步 Action 过滤器

using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Filters.Infrastructure
{
    public class ProfileAttribute : ActionFilterAttribute
    {
        public override async Task OnActionExecutionAsync(
            ActionExecutingContext context,
            ActionExecutionDelegate next)
        {
            Stopwatch timer = Stopwatch.StartNew();

            await next();

            timer.Stop();
            string result = "<div>Elapsed time: "
                + $"{timer.Elapsed.TotalMilliseconds} ms</div>";
            byte[] bytes = Encoding.ASCII.GetBytes(result);
            await context.HttpContext.Response.Body.WriteAsync(bytes,
                0, bytes.Length);
        }
    }
}

ActionExecutingContext对象向过滤器提供 context 数据,ActionExectionDelegate对象表示要执行的 action 方法(或下一个过滤器)。过滤器在调用委托之前完成其准备工作,然后在委托完成时完成其工作。委托返回一个Task,这就是我在清单中使用await关键字的原因。

使用结果过滤器

在 MVC 处理 action 方法返回的 action result 之前和之后应用结果过滤器。结果筛选器可以更改或替换 action result 或完全取消请求(即使 action 方法已经被调用)。下面是定义结果过滤器的IResultFilter接口:

namespace Microsoft.AspNetCore.Mvc.Filters {

    public interface IResultFilter : IFilterMetadata {

        void OnResultExecuting(ResultExecutingContext context);

        void OnResultExecuted(ResultExecutedContext context);
    }
}

结果过滤器遵循与 action 过滤器相同的模式。在处理 action 方法并生成 action result 之前会调用OnResultExecuting方法,并通过ResultExecutingContext对象向其提供 context 信息。ResultExecutingContext类是从FilterContext派生的,并定义了表19-8中描述的附加属性。

表 19-8:ResultExecutingContext 的属性

名称 描述
Controller 此属性返回执行 action 方法的控制器
Cancel 将此bool属性设置为true将停止处理 action result 以生成响应。
Result 此属性返回由 action 方法返回的IActionResult对象。

在 MVC 处理了 action result 并通过ResultExecutedContext类的实例提供了 context 数据之后,调用OnResultExecuted方法,表19-9显示了该类定义的属性以及从FilterContext继承的属性。

表 19-9:ResultExecutedContext 的属性

名称 描述
Controller 此属性返回执行 action 方法的控制器
Canceled bool属性指示请求是否已取消
Exception 此属性包含 action 方法引发的任何异常
ExceptionDispatchInfo 此方法返回一个ExceptionDispatchInfo对象,该对象包含 action 方法引发的任何异常的堆栈跟踪详细信息。
ExceptionHandled 将此属性设置为true表示过滤器已处理异常,该异常将不再传播
Result 此属性返回用于生成对客户端的响应的IActionResult对象

创建结果过滤器

ResultFilterAttribute类实现了结果过滤器接口,并提供了最简单的方法来创建可以作为特性应用的结果过滤器。为了演示结果过滤器是如何工作的,我向 Infrastructure 文件夹中添加了一个名为 ViewResultDetailsAttribute.cs 的类文件,并使用它来定义如清单19-15所示的过滤器。

清单 19-15:Infrastructure 文件夹下的 ViewResultDetailsAttribute.cs 文件的内容

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;

namespace Filters.Infrastructure
{
    public class ViewResultDetailsAttribute : ResultFilterAttribute
    {
        public override void OnResultExecuting(ResultExecutingContext context)
        {
            Dictionary<string, string> dict = new Dictionary<string, string>
            {
                ["Result Type"] = context.Result.GetType().Name,
            };

            ViewResult vr;
            if ((vr = context.Result as ViewResult) != null)
            {
                dict["View Name"] = vr.ViewName;
                dict["Model Type"] = vr.ViewData.Model.GetType().Name;
                dict["Model Data"] = vr.ViewData.Model.ToString();
            }

            context.Result = new ViewResult
            {
                ViewName = "Message",
                ViewData = new ViewDataDictionary(
                    new EmptyModelMetadataProvider(),
                    new ModelStateDictionary())
                { Model = dict }
            };
        }
    }
}

该类仅重写OnResultExecuting方法,并使用 context 对象更改用于生成对客户端的响应的 action result。过滤器使用包含简单诊断信息的字典作为视图模型,创建渲染 Message 视图的ViewResult对象。

在 action 方法生成 action result 之后,但在处理它以生成结果之前调用OnResultExecuting方法,并且更改 context 对象的Result对象的值可以使我提供与应用过滤器的 action 方法不同的结果类型。清单19-16显示了应用于 Home 控制器的结果过滤器。

清单 19-16:Controllers 文件夹下的 HomeController.cs 文件,应用结果过滤器

using Microsoft.AspNetCore.Mvc;
using Filters.Infrastructure;

namespace Filters.Controllers
{
    [ViewResultDetails]
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Message",
            "This is the Index action on the Home controller");

        public ViewResult SecondAction() => View("Message",
            "This is the SecondAction action on the Home controller");
    }
}

如果运行应用程序,您将看到结果过滤器的效果,如图19-5所示。

图19-5 结果过滤器的效果

创建一个异步结果过滤器

IAsyncResultFilter接口可用于创建异步结果过滤器。下面是接口的定义:

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Mvc.Filters {

    public interface IAsyncResultFilter : IFilterMetadata {

        Task OnResultExecutionAsync(ResultExecutingContext context,
            ResultExecutionDelegate next);
    }
}

此接口类似于异步 action 过滤器的接口。在清单19-17中,我重写了ViewResultDetailsAttribute类,以实现IAsyncResultFilter接口。

清单 19-15:Infrastructure 文件夹下的 ViewResultDetailsAttribute.cs 文件的内容

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;

namespace Filters.Infrastructure
{
    public class ViewResultDetailsAttribute : ResultFilterAttribute
    {
        public override async Task OnResultExecutionAsync(
            ResultExecutingContext context,
            ResultExecutionDelegate next)
        {
            Dictionary<string, string> dict = new Dictionary<string, string>
            {
                ["Result Type"] = context.Result.GetType().Name,
            };

            ViewResult vr;
            if ((vr = context.Result as ViewResult) != null)
            {
                dict["View Name"] = vr.ViewName;
                dict["Model Type"] = vr.ViewData.Model.GetType().Name;
                dict["Model Data"] = vr.ViewData.Model.ToString();
            }

            context.Result = new ViewResult
            {
                ViewName = "Message",
                ViewData = new ViewDataDictionary(
                    new EmptyModelMetadataProvider(),
                    new ModelStateDictionary())
                    {
                        Model = dict
                    }
            };

            await next();
        }
    }
}

请注意,我负责调用接收到的委托,将其作为OnResultExecutionAsync方法的参数。如果不调用委托,请求处理管道将不完成,action result 也不会被渲染。

创建混合 Action/结果 过滤器

区分请求处理的 action 和结果阶段并不总是有帮助的。这可能是因为您希望将这两个阶段作为一个步骤来处理,或者因为您的过滤器响应于执行 action 的方式,但这是通过干扰结果来做到的。能够创建一个过滤器,它既是一个 action 过滤器,也是一个结果过滤器,并且能够在每个阶段执行工作,这是非常有用的。

这是一个非常常见的要求,ActionFilterAttribute类实现了两种过滤器接口,这意味着您可以在单个特性中混合和匹配过滤器类型。为了演示它是如何工作的,我在清单19-18中修改了ProfileAttribute类,以便将 action 过滤器与结果过滤器结合起来。

清单 19-18:Infrastructure 文件夹下的 ProfileAttribute.cs 文件,创建混合过滤器

using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Filters.Infrastructure
{
    public class ProfileAttribute : ActionFilterAttribute
    {
        private Stopwatch timer;
        private double actionTime;

        public override async Task OnActionExecutionAsync(
            ActionExecutingContext context,
            ActionExecutionDelegate next)
        {
            timer = Stopwatch.StartNew();
            await next();
            actionTime = timer.Elapsed.TotalMilliseconds;
        }
        public override async Task OnResultExecutionAsync(
            ResultExecutingContext context,
            ResultExecutionDelegate next)
        {
            await next();

            timer.Stop();
            string result = "<div>Action time: "
                + $"{actionTime} ms</div><div>Total time: "
                + $"{timer.Elapsed.TotalMilliseconds} ms</div>";
            byte[] bytes = Encoding.ASCII.GetBytes(result);

            await context.HttpContext.Response.Body.WriteAsync(bytes,
                0, bytes.Length);
        }
    }
}

我已经为这两种类型的过滤器使用了异步方法,但是您可以混合和匹配以获得所需的功能,因为这些方法的默认实现会调用它们的同步对应项。在过滤器中,我使用Stopwatch来测量处理 action 需要多长时间,以及所用的总时间是多少,并将结果写入响应。在清单19-19中,我将组合过滤器应用于 Home 控制器。

清单 19-19:Controllers 文件夹下的 HomeController.cs 文件,应用混合过滤器

using Microsoft.AspNetCore.Mvc;
using Filters.Infrastructure;

namespace Filters.Controllers
{
    [Profile]
    [ViewResultDetails]
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Message",
            "This is the Index action on the Home controller");

        public ViewResult SecondAction() => View("Message",
            "This is the SecondAction action on the Home controller");
    }
}

如果运行应用程序,您将看到类似于图19-6所示的输出。由于输出是在结果过滤器的后处理阶段编写的,而不是从上一个版本中使用的 action 过滤器方法编写的,所以输出出现在ViewResultDetails提供的内容之后。

图19-6 混合 action/结果 过滤器的输出

使用异常过滤器

异常过滤器允许您响应异常,而不必在每个操作方法中编写try...catch块。异常过滤器可以应用于控制器类或 action 方法。当 action 方法或已应用于 action 方法的 action 或结果过滤器未处理异常时,将调用它们(action 和结果过滤器可以通过将 context 对象的ExceptionHandled属性设置为true来处理未处理的异常)。异常过滤器实现IExceptionFilter接口,定义如下:

namespace Microsoft.AspNetCore.Mvc.Filters {

    public interface IExceptionFilter : IFilterMetadata {

        void OnException(ExceptionContext context);
    }
}

如果遇到未处理的异常,则调用OnException方法。IAsyncExceptionFilter接口可用于创建异步异常过滤器,如果需要使用异步 API 响应异常,则此操作非常有用。下面是异步接口的定义:

using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.Filters {

    public interface IAsyncExceptionFilter : IFilterMetadata {

        Task OnExceptionAsync(ExceptionContext context);
    }
}

OnExceptionAsync方法是IExceptionFilter接口中OnException方法的异步对应,当出现未处理的异常时调用该方法。

对于两个接口来说,都是通过ExceptionContext类来提供 context 数据的,此类派生自FilterContext,并定义了如表19-10所示的附加属性。

表 19-10:ExceptionContext 属性

名称 描述
Exception 此属性包含引发的任何异常
ExceptionDispatchInfo 此方法返回一个ExceptionDispatchInfo对象,该对象包含异常的堆栈跟踪详细信息
ExceptionHandled bool属性用于指示是否已处理异常
Result 此属性设置将用于生成响应的IActionResult

创建异常过滤器

ExceptionFilterAttribute类实现了两个异常过滤器接口,并且是创建过滤器以便将其作为特性应用的最简单方法。异常过滤器最常见的用途是显示特定异常类型的自定义错误页,以便向用户提供比标准错误处理功能所能提供的更多有用信息。作为演示,我向 Infrastructure 文件夹中添加了一个名为 RangeExceptionAttribute.cs 的类文件,并使用它定义了清单19-20所示的过滤器。

清单 19-20:Infrastructure 文件夹下的 RangeExceptionAttribute.cs 文件的内容

using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;

namespace Filters.Infrastructure
{
    public class RangeExceptionAttribute : ExceptionFilterAttribute
    {
        public override void OnException(ExceptionContext context)
        {
            if (context.Exception is ArgumentOutOfRangeException)
            {
                context.Result = new ViewResult()
                {
                    ViewName = "Message",
                    ViewData = new ViewDataDictionary(
                        new EmptyModelMetadataProvider(),
                        new ModelStateDictionary())
                    {
                        Model = @"The data received by the application cannot be processed"
                    }
                };
            }
        }
    }
}

此过滤器使用ExceptionContext对象获取未处理异常的类型,如果该类型为ArgumentOutOfRangeException,则创建一个向用户显示消息的 action result。在清单19-21中,我向 Home 控制器添加了一个 action 方法,并对其应用了异常筛选器。

清单 19-21:Controllers 文件夹下的 HomeController.cs 文件,应用异常过滤器

using Filters.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using System;

namespace Filters.Controllers
{
    [Profile]
    [ViewResultDetails]
    [RangeException]
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Message",
            "This is the Index action on the Home controller");

        public ViewResult SecondAction() => View("Message",
            "This is the SecondAction action on the Home controller");

        public ViewResult GenerateException(int? id)
        {
            if (id == null)
            {
                throw new ArgumentNullException(nameof(id));
            }
            else if (id > 10)
            {
                throw new ArgumentOutOfRangeException(nameof(id));
            }
            else
            {
                return View("Message", $"The value is {id}");
            }
        }
    }
}

GenerateException action 方法依赖于默认的路由模式从请求 URL 接收可空的int值。如果没有匹配的 URL 段,action 方法将抛出ArgumentNullException,如果它的值大于50,则抛出ArgumentOutOfRangeException。如果有一个值并且它在范围内,那么 action 方法将返回一个ViewResult

您可以通过运行应用程序并请求 /Home/GenerateException/100 URL 来测试异常过滤器。最后一个段将超出 action 方法所期望的范围,后者将抛出过滤器处理的异常类型,产生如图19-7所示的结果。如果您请求 /Home/GenerateException,则由 action 方法引发的异常将不会由过滤器处理,并且将使用默认的错误处理。

图19-7 使用异常过滤器

为过滤器使用依赖注入

当您从一个便捷的特性类(如ExceptionFilterAttribute)派生过滤器时,MVC 将创建一个过滤器类的新实例来处理每个请求。这是一种合理的方法,因为它避免了任何可能的重用或并发问题,并且适合开发人员所需的大多数过滤器类的需要。

另一种方法是使用依赖注入系统为过滤器选择不同的生命周期。在过滤器中使用依赖注入有两种不同的方法,我将在下面的部分中对此进行描述。

解析过滤依赖关系

第一种方法是使用依赖注入来管理过滤器的 context 数据,这允许不同类型的过滤器共享数据,或者让单个过滤器与用于处理其他请求的实例共享数据。为了演示这是如何工作的,我向 Infrastructure 文件夹中添加了一个名为 FilterDiagnostics.cs 的类文件,并使用它定义了如清单19-22所示的接口和实现类。

清单 19-20:Infrastructure 文件夹下的 FilterDiagnostics.cs 文件的内容

using System.Collections.Generic;

namespace Filters.Infrastructure
{
    public interface IFilterDiagnostics
    {
        IEnumerable<string> Messages { get; }
        void AddMessage(string message);
    }

    public class DefaultFilterDiagnostics : IFilterDiagnostics
    {
        private List<string> messages = new List<string>();

        public IEnumerable<string> Messages => messages;

        public void AddMessage(string message) =>
            messages.Add(message);
    }
}

IFilterDiagnostics接口定义了一个用于在过滤器执行期间收集诊断消息的简单模型。DefaultFilterDiagnostics类是我将要使用的实现。在清单19-23中,我更新了Startup类,以便使用新接口及其实现配置服务提供者。

清单 19-23:Filters 文件夹下的 Startup.cs 文件,配置服务提供者

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Filters.Infrastructure;

namespace Filters
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IFilterDiagnostics, DefaultFilterDiagnostics>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

我使用AddScoped扩展方法来配置服务提供者,这意味着为处理单个请求实例化的所有过滤器都将接收相同的DefaultFilterDiagnostics对象。这是过滤器之间共享自定义 context 数据的基础。

创建带依赖项的过滤器

下一步是创建过滤器,声明IFilterDiagnostics接口上的依赖项。我在 Infrastructure 文件夹中创建了一个名为 TimeFilter.cs 的类文件,并使用它来定义清单19-24所示的类。

清单 19-20:Infrastructure 文件夹下的 TimeFilter.cs 文件的内容

using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Filters.Infrastructure
{
    public class TimeFilter : IAsyncActionFilter, IAsyncResultFilter
    {
        private Stopwatch timer;
        private IFilterDiagnostics diagnostics;

        public TimeFilter(IFilterDiagnostics diags)
        {
            diagnostics = diags;
        }

        public async Task OnActionExecutionAsync(
            ActionExecutingContext context,
            ActionExecutionDelegate next)
        {
            timer = Stopwatch.StartNew();
            await next();
            diagnostics.AddMessage($@"Action time:
                {timer.Elapsed.TotalMilliseconds}");
        }

        public async Task OnResultExecutionAsync(
            ResultExecutingContext context,
            ResultExecutionDelegate next)
        {
            await next();
            timer.Stop();
            diagnostics.AddMessage($@"Result time:
                {timer.Elapsed.TotalMilliseconds}");
        }
    }
}

TimeFilter是一个混合的 action/结果 过滤器,它从先前的示例中重新创建计时器功能,但是使用IFilterDiagnostics接口的实现来存储它的定时信息,该实现被声明为构造函数参数,并且在创建过滤器时由依赖注入系统提供。

注意,TimeFilter类直接实现过滤器接口,而不是从便捷特性类派生。正如您将看到的,依赖于依赖注入的过滤器通过不同的特性应用,而不是直接用于修饰控制器或 action。

为了演示过滤器如何使用依赖项注入来共享 context 数据的,我向 Infrastructure 文件夹中添加了一个名为 DiagnosticsFilter.cs 的类文件,并使用它创建了清单19-25所示的过滤器。

清单 19-25:Infrastructure 文件夹下的 DiagnosticsFilter.cs 文件的内容

using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Filters.Infrastructure
{
    public class DiagnosticsFilter : IAsyncResultFilter
    {
        private IFilterDiagnostics diagnostics;

        public DiagnosticsFilter(IFilterDiagnostics diags)
        {
            diagnostics = diags;
        }

        public async Task OnResultExecutionAsync(
            ResultExecutingContext context,
            ResultExecutionDelegate next)
        {
            await next();

            foreach (string message in diagnostics?.Messages)
            {
                byte[] bytes = Encoding.ASCII
                    .GetBytes($"<div>{message}</div>");
                await context.HttpContext.Response.Body
                    .WriteAsync(bytes, 0, bytes.Length);
            }
        }
    }
}

DiagnosticsFilter类是一个结果过滤器,它接收IFilterDiagnostics接口的实现作为构造函数参数,并将它包含的消息写入响应。

应用过滤器

最后一步是将过滤器应用于控制器类。标准 C# 特性还未完全支持解析构造函数依赖关系,这就是前几节中的过滤器不是特性的原因。相反,可使用TypeFilter特性,并将其配置为所需的过滤器类型,如清单19-26所示。

提示:我在清单19-26中应用过滤器的顺序很重要,我在本章后面的《理解和更改过滤器顺序》一节中解释了这一点。

清单 19-26:Controllers 文件夹下的 HomeController.cs 文件,应用带依赖项的过滤器

using Filters.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using System;

namespace Filters.Controllers
{
    [TypeFilter(typeof(DiagnosticsFilter))]
    [TypeFilter(typeof(TimeFilter))]
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Message",
            "This is the Index action on the Home controller");

        public ViewResult SecondAction() => View("Message",
            "This is the SecondAction action on the Home controller");

        public ViewResult GenerateException(int? id)
        {
            if (id == null)
            {
                throw new ArgumentNullException(nameof(id));
            }
            else if (id > 10)
            {
                throw new ArgumentOutOfRangeException(nameof(id));
            }
            else
            {
                return View("Message", $"The value is {id}");
            }
        }
    }
}

TypeFilter特性为每个请求创建一个过滤器类的新实例,但是使用依赖注入特性来创建过滤器类的新实例,该特性允许创建松散耦合的组件,并将用于解决依赖关系的对象置于生命周期管理之下。

在此示例中,这意味着清单19-26中应用的两个过滤器都将接收相同的IFilterDiagnostics实现对象,因此由TimeFilter类编写的消息将被DiagnosticsFilter类写入响应。图19-8显示了效果,您可以通过启动应用程序并请求应用程序的默认 URL 来看到这一点。

图19-8 使用带依赖项的过滤器

管理过滤器生命周期

当使用TypeFilter特性时,将为每个请求创建过滤器类的新实例。这与直接将过滤器作为特性应用相同,但TypeFilter特性允许过滤器类声明通过服务提供者解析的依赖项。

ServiceFilter特性更进一步,并使用服务提供者创建过滤器对象。这也允许将过滤器对象置于生命周期管理之下。作为一个演示,在清单19-27中,我修改了TimeFilter类,以便它保持记录时间的简单平均值。

清单 19-27:Infrastructure 文件夹下的 TimeFilter.cs 文件,保持平均数

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Filters.Infrastructure
{
    public class TimeFilter : IAsyncActionFilter, IAsyncResultFilter
    {
        private ConcurrentQueue<double> actionTimes = new ConcurrentQueue<double>();
        private ConcurrentQueue<double> resultTimes = new ConcurrentQueue<double>();
        private IFilterDiagnostics diagnostics;

        public TimeFilter(IFilterDiagnostics diags)
        {
            diagnostics = diags;
        }

        public async Task OnActionExecutionAsync(
            ActionExecutingContext context, ActionExecutionDelegate next)
        {
            Stopwatch timer = Stopwatch.StartNew();
            await next();
            timer.Stop();
            actionTimes.Enqueue(timer.Elapsed.TotalMilliseconds);
            diagnostics.AddMessage($@"Action time:
                {timer.Elapsed.TotalMilliseconds}
                Average: {actionTimes.Average():F2}");
        }

        public async Task OnResultExecutionAsync(
            ResultExecutingContext context, ResultExecutionDelegate next)
        {
            Stopwatch timer = Stopwatch.StartNew();
            await next();
            timer.Stop();
            resultTimes.Enqueue(timer.Elapsed.TotalMilliseconds);
            diagnostics.AddMessage($@"Result time:
                {timer.Elapsed.TotalMilliseconds}
                Average: {resultTimes.Average():F2}");
        }
    }
}

过滤器现在使用线程安全集合来存储它为请求处理的 action 和结果阶段记录的时间,并在每次请求处理请求时使用一个单独的Stopwatch。在清单19-28中,我已经将TimeFilter类注册为Startup类中的服务提供者的单例。

清单 19-28:Filters 文件夹下的 Startup.cs 文件,配置服务提供者

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Filters.Infrastructure;

namespace Filters
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IFilterDiagnostics, DefaultFilterDiagnostics>();
            services.AddSingleton<TimeFilter>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

注意,我还更改了IFilterDiagnostics的生命周期,因此这是一个单例。如果我继续为每个请求创建一个新实例,那么单例TimeFilter将从DiagnosticsFilter接收一个不同的IFilterDiagnostics对象,该对象将继续通过TypeFilter特性实例化,并将为每个请求创建该对象。

应用过滤器

最后一步是使用ServiceType特性对控制器应用过滤器,如清单19-29所示。

清单 19-26:Controllers 文件夹下的 HomeController.cs 文件,应用过滤器

using Filters.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using System;

namespace Filters.Controllers
{
    [TypeFilter(typeof(DiagnosticsFilter))]
    [ServiceFilter(typeof(TimeFilter))]
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Message",
            "This is the Index action on the Home controller");

        public ViewResult SecondAction() => View("Message",
            "This is the SecondAction action on the Home controller");

        public ViewResult GenerateException(int? id)
        {
            if (id == null)
            {
                throw new ArgumentNullException(nameof(id));
            }
            else if (id > 10)
            {
                throw new ArgumentOutOfRangeException(nameof(id));
            }
            else
            {
                return View("Message", $"The value is {id}");
            }
        }
    }
}

通过运行应用程序并请求默认的 URL,您可以看到效果。由于IFilterDiagnostics接口的单个实现对象用于解析所有依赖项,所以显示的消息集与每个请求一起生成,如图19-9所示。

图19-9 使用服务提供者管理过滤器生命周期

创建全局过滤器

在本章的开头,我解释了可以将过滤器应用到控制器类,这样就不必将它们应用于单个 action 方法。全局过滤器更进一步,在Startup类中应用一次,如其名称所示,自动应用于应用程序中每个控制器中的每个 action 方法。任何过滤器都可以用作全局过滤器;为了演示,我在 Infrastructure 文件夹中创建了一个名为 ViewResultDiagnostics.cs 的类文件,并使用它定义了清单19-30中所示的过滤器。

清单 19-30:Infrastructure 文件夹下的 ViewResultDiagnostics.cs 文件的内容

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Filters.Infrastructure
{
    public class ViewResultDiagnostics : IActionFilter
    {
        private IFilterDiagnostics diagnostics;

        public ViewResultDiagnostics(IFilterDiagnostics diags)
        {
            diagnostics = diags;
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            // do nothing - not used in this filter
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            ViewResult vr;
            if ((vr = context.Result as ViewResult) != null)
            {
                diagnostics.AddMessage($"View name: {vr.ViewName}");
                diagnostics.AddMessage($@"Model type:
                    {vr.ViewData.Model.GetType().Name}");
            }
        }
    }
}

过滤器使用IFilterDiagnostics对象存储有关视图名称和ViewResult action results 的模型类型的消息。在清单19-31中,我在全局范围内应用了这个过滤器,以及它所依赖的用于写出诊断消息的DiagnosticsFilter类。

清单 19-31:Filters 文件夹下的 Startup.cs 文件,注册全局过滤器

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Filters.Infrastructure;

namespace Filters
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IFilterDiagnostics, DefaultFilterDiagnostics>();
            services.AddScoped<TimeFilter>();
            services.AddScoped<ViewResultDiagnostics>();
            services.AddScoped<DiagnosticsFilter>();
            services.AddMvc().AddMvcOptions(options => {
                options.Filters.AddService(typeof(ViewResultDiagnostics));
                options.Filters.AddService(typeof(DiagnosticsFilter));
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

全局过滤器是通过配置 MVC 服务包来设置的。在本例中,我使用MvcOptions.Filters.AddService方法全局注册过滤器。AddService方法接受一个 .NET 类型,该类型将使用ConfigureServices方法中其他地方指定的生命周期规则进行实例化。我将其他过滤器类型的生命周期更改为作用域,以便为每个请求创建新实例。其结果是,将为每个控制器的每个请求创建并应用ViewResultDiagnosticsDiagnosticsFilter的新实例。

提示:还可以使用Add方法而不是AddService方法添加全局过滤器,该方法允许将过滤器对象注册为全局过滤器,而不依赖项注入和服务提供者。我将在下一节中使用Add方法。

我在 Controllers 文件夹中添加了一个名为 GlobalController.cs 的类文件,并使用它来定义如清单19-32所示的控制器。

清单 19-32:Controllers 文件夹下的 GlobalController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;

namespace Filters.Controllers
{
    public class GlobalController : Controller
    {
        public ViewResult Index() => View("Message",
            "This is the global controller");
    }
}

没有对 Global 控制器应用过滤器,但是如果启动应用程序并请求 /global URL,您将看到两个全局过滤器的输出,如图19-10所示。

图19-10 使用全局过滤器

理解和改变过滤顺序

过滤器以指定顺序运行:授权、action 然后是结果。但如果给定类型存在多个过滤器,那么应用它们的顺序由应用过滤器的作用域驱动。为了演示这是如何工作的,我在 Infrastructure 文件夹中添加了一个名为 MessageAttribute.cs 的类文件,并使用它来定义清单19-33所示的过滤器。

清单 19-33:Infrastructure 文件夹下的 MessageAttribute.cs 文件的内容

using System.Text;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Filters.Infrastructure
{
    public class MessageAttribute : ResultFilterAttribute
    {
        private string message;

        public MessageAttribute(string msg)
        {
            message = msg;
        }

        public override void OnResultExecuting(ResultExecutingContext context)
        {
            WriteMessage(context, $"<div>Before Result:{message}</div>");
        }

        public override void OnResultExecuted(ResultExecutedContext context)
        {
            WriteMessage(context, $"<div>After Result:{message}</div>");
        }

        private void WriteMessage(FilterContext context, string msg)
        {
            byte[] bytes = Encoding.ASCII
                .GetBytes($"<div>{msg}</div>");
            context.HttpContext.Response
                .Body.Write(bytes, 0, bytes.Length);
        }
    }
}

这是一个结果过滤器,它在处理 action result 之前和之后将 HTML 片段写入响应。过滤器编写的消息是通过构造函数参数配置的,该参数在应用为特性时可以使用。在清单19-34中,我简化了 Home 控制器,并用多个 Message 过滤器实例替换了前面示例中的过滤器。

清单 19-34:Controllers 文件夹下的 HomeController.cs 文件,应用过滤器

using Microsoft.AspNetCore.Mvc;
using Filters.Infrastructure;

namespace Filters.Controllers
{
    [Message("This is the Controller-Scoped Filter")]
    public class HomeController : Controller
    {
        [Message("This is the First Action-Scoped Filter")]
        [Message("This is the Second Action-Scoped Filter")]
        public ViewResult Index() => View("Message",
            "This is the Index action on the Home controller");
    }
}

我已经更改了全局过滤器的集合,以便在那里也使用 Message 过滤器,如清单19-35所示。

清单 19-35:Filters 文件夹下的 Startup.cs 文件,创建一个全局过滤器

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Filters.Infrastructure;

namespace Filters
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IFilterDiagnostics, DefaultFilterDiagnostics>();
            services.AddScoped<TimeFilter>();
            services.AddScoped<ViewResultDiagnostics>();
            services.AddScoped<DiagnosticsFilter>();
            services.AddMvc().AddMvcOptions(options => {
                options.Filters.Add(new
                    MessageAttribute("This is the Globally-Scoped Filter"));
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

Index方法响应请求时,将使用筛选器的四个实例。如果运行应用程序并请求默认的 URL,将在浏览器中看到以下输出:

Before Result:This is the Globally-Scoped Filter
Before Result:This is the Controller-Scoped Filter
Before Result:This is the First Action-Scoped Filter
Before Result:This is the Second Action-Scoped Filter
After Result:This is the Second Action-Scoped Filter
After Result:This is the First Action-Scoped Filter
After Result:This is the Controller-Scoped Filter
After Result:This is the Globally-Scoped Filter

默认情况下,MVC 运行全局过滤器,然后运行应用于控制器过滤器的过滤器,最后运行应用于 action 方法的过滤器。一旦调用了 action 方法或处理了 action result,就会解除过滤器的堆栈,这就是为什么输出中的After Result消息以相反的顺序显示的原因。

改变过滤器顺序

可以通过实现IOrderedFilter接口来更改默认顺序,MVC 在研究如何按顺序堆栈过滤器时会查找该接口。下面是接口的定义:

namespace Microsoft.AspNetCore.Mvc.Filters {

    public interface IOrderedFilter : IFilterMetadata {
        int Order { get; }
    }
}

Order属性返回int值;低值告诉 MVC 在具有较高Order值的过滤器之前应用过滤器。便捷特性已经实现了IOrder值,在清单19-36中,我已经为应用于 Home 控制器的过滤器设置了Order属性。

提示TypeFilterServiceFilter特性还实现了IOrderedFilter接口,这意味着您也可以在使用依赖注入时更改过滤顺序。

清单 19-36:Controllers 文件夹下的 HomeController.cs 文件,设置过滤器顺序

using Filters.Infrastructure;
using Microsoft.AspNetCore.Mvc;

namespace Filters.Controllers
{
    [Message("This is the Controller-Scoped Filter", Order = 10)]
    public class HomeController : Controller
    {
        [Message("This is the First Action-Scoped Filter", Order = 1)]
        [Message("This is the Second Action-Scoped Filter", Order = -1)]
        public ViewResult Index() => View("Message",
            "This is the Index action on the Home controller");
    }
}

Order值也可以是负值,这是确保在任何具有默认顺序的全局过滤器之前应用过滤器的有用方法(尽管在创建全局过滤器时也可以设置顺序)。如果运行该示例,您将看到输出消息的顺序已经更改,以反映新的优先级。

Before Result:This is the Second Action-Scoped Filter
Before Result:This is the Globally-Scoped Filter
Before Result:This is the First Action-Scoped Filter
Before Result:This is the Controller-Scoped Filter
After Result:This is the Controller-Scoped Filter
After Result:This is the First Action-Scoped Filter
After Result:This is the Globally-Scoped Filter
After Result:This is the Second Action-Scoped Filter

总结

在本章中,您了解了如何将解决横切关注点的逻辑封装为过滤器。我向您展示了可用的不同类型的过滤器以及如何实现它们。您了解了如何将过滤器作为控制器和 action 方法的特性应用,以及如何将它们作为全局筛选器应用。在下一章中,我将向您展示如何使用控制器来创建 Web 服务。

;

© 2018 - IOT小分队文章发布系统 v0.3