ASP.NET Core应用程序13:使用过滤器

  过滤器将额外的逻辑注入请求处理。过滤器类似于应用于单个端点的中间件,可以是操作或页面处理程序方法,它们提供了一种管理特定请求集的优雅方法。
  本章描述 ASP.NET Core 过滤器特性,并解释如何使用它来更改特定端点的请求和结果,描述不同类型的过滤器,演示了如何创建和应用每种过滤器。还展示了如何管理过滤器的生命周期并控制它们的执行顺序。

1 准备工作

  本章使用了上章项目。
  删除控制器、视图和 Razor Pages,留下共享布局、数据模型和配置文件。
  重新添加 Controllers 文件夹,添加 HomeController.cs。

public class HomeController : Controller
{
    private DataContext _context;
    public HomeController(DataContext ctx)
    {
        _context = ctx;
    }
    public async Task<IActionResult> Index()
    {
        return View("Message","这是home控制器的Index方法");
    }
}

  操作方法呈现一个名为 Message 的视图,并传递一个字符串作为视图数据。Views/Shared 添加一个名Message.cshtml 的 Razor 视图。

@{
    Layout = "_SimpleLayout";
}

@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>
}

  在 Pages 文件夹中添加一个名为 Message.cshtml 的 Razor Pages。

@page "/pages/message"
@model MessageModel
@using Microsoft.AspNetCore.Mvc.RazorPages
@using System.Collections.Generic


@if (Model.Message is string)
{
    @Model.Message
}
else if (Model.Message is IDictionary<string, string>)
{
    var dict = Model.Message 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>
}

@functions
{
    public class MessageModel : PageModel
    {
        public object Message { get; set; } = $"This is the Message Razor Page";

    }
}

启用 HTTPS 连接
  本章中的一些示例需要使用 SSL。修改 Properties 文件夹的 launchSettings,json 文件中,来启用 SSL 和 HTTPS,并将端口设置为 44350。

{
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "MyWebApp": {
      "commandName": "Project",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:5000;http://localhost:44350"

    }
  },
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:5000",
      "sslPort": 44350
    }
  }
}

  请求 http://localhost:5000/pages/messagehttps://localhost:44350/pages/message以查看通过 HTTP 和 HTTPS 传递的、来自 Message.cshtml Razor Pages 的响应。

2 使用过滤器

  假设想对一些操作方法强制执行 HTTPS 请求。中间件中通过读取 HttpRequest 对象的 IsHtps 属性来实现这一点if(Request.IsHttps),但更好的方式是在类或方法上使用特性[RequireHttps]过滤器。

[RequireHttps]
public class HomeController : Controller

3 理解过滤器

  ASP.NET Core 支持不同类型的过滤器,每种过滤器用于不同的目的。下表描述了过滤器类别:

名称 描述
授权过滤器 这种类型的过滤器用于应用应用程序的授权策略
资源过滤器 这种类型的过滤器用于拦截请求,通常用于实现诸如缓存的特性
操作过滤器 这种类型的过滤器用于在请求被操作方法接收之前修改请求,或者在生成请求之后修改操作结果。
这种类型的过滤器只能应用于控制器和操作
页面过滤器 这种类型的过滤器用于在 Razor Pages 处理程序方法接收到请求之前修改请求,或者在生成操作
结果之后修改操作结果。这种类型的过滤器只能应用于 Razor Pages
结果过滤器 此类型的过滤器用于在操作执行前更改操作结果,或在执行后修改结果
异常过滤器 这种类型的过滤器用于处理操作方法或页面处理程序执行期间发生的异常

  过滤器有自己的管道,并按照特定的顺序执行。过滤器可以短路过滤器管道,以阻止请求被转发到下一个过滤器。
image.png
  每种类型的过滤器都是使用 ASP.NET Core 定义的接口实现的。它还提供了一些基类,可以方便地将某些类型的过滤器应用为属性。
  过滤器类型、接口和特性基类:

过滤器类型 接口 特性类
授权过滤器 IAuthorizationFilter、IAsyncAuthorizationFilter
资源过滤器 IResourceFilter、IAsyncResourceFilter
操作过滤器 IActionFilter、 IAsyncActionFilter ActionFilterAttribute
页面过滤器 IPageFilter、IAsyncPageFilter
结果过滤器 IResultFilter、IAsyncResultFilter、
IAlwaysRunResultFilter、IAsyncAlwaysRunResultFilter
ResultFilterAttribute
异常过滤器 lexceptionFilter、 IAsyncExceptionFilter ExceptionFilterAttribute

4 创建自定义过滤器

4.1 授权过滤器

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

namespace Microsoft.AspNetCore.Mvc.Filters
{
    public interface IAuthorizationFilter :IFilterMetadata
    {
        void OnAuthorization (AuthorizationFilterContext context);
    }
}

  调用 OnAuthorization 方法,为过滤器提供授权请求的机会,也有异步版本。

public interface IAsyncAuthorizationFilter : IFilterMetadata
{
    Task OnAuthorizationAsync(AuthorizationFilterContext context);
}

  过滤器会通过 AuthorizationFilterContext 对象接收描述请求的上下文数据,该对象是从 FilterContext 类派生出来的,并添加了一个重要属性 Result,当请求不符合应用程序的授权策略时,授权过滤器会设置这个IActionResult 属性。
创建授权过滤器
  创建 Filters 文件夹,并添加了一个名为 HttpsOnlyAttribute.cs 的文件,并使用它定义过滤器。

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

  如果请求符合授权策略,授权过滤器就不执行任何操作,同时允许 ASP.NET Core 移到下一个过滤器,并最终执行端点。如果存在问题,过滤器将设置传递给 OnAuthorization 方法 AuthorizationFilterContext 对象的 Result 属性。这将防止进一步执行操作,并提供返回给客户端的结果。
  HomeController.cs 文件中应用自定义过滤器。

[HttpsOnly]
public class HomeController : Controller

  请求 http://localhost:5000https://localhost:44350,前者将被拦截。

4.2 资源过滤器

  资源过滤器对每个请求执行两次模型绑定过程,在操作结果处理之前再生成结果。以下是IResourceFilter 接口的定义:

namespace Microsoft.AspNetCore.Mvc.Filters 
{
    public interface IResourcerilter :IFilterMetadata
    {
        void OnResourceExecuting(ResourceExecutingContext context);
        void OnResourceExecuted(ResourceExecutedContext context);
    }
}

  OnResourceExecuting 方法在处理请求时调用,而 OnResourceExecuted 方法在端点处理了请求之后、在操作结果执行之前调用。
  异步版本如下。

public interface IAsyncResourceFilter : IFilterMetadata
{
    Task OnResourceExecutionAsync(ResourceExecutingContext context, 
                                  ResourceExecutionDelegate next);
}

(1)创建资源过滤器
  资源过滤器通常用于可能使管道短路并尽早提供响应的地方,例如在实现数据缓存时。要创建一个简单的缓存过滤器,将一个名为 SimpleCacheAttribute.cs 的类文件添加到 Filters 文件夹。

public class SimpleCacheAttribute : Attribute, IResourceFilter
{
    private Dictionary<PathString, IActionResult> CachedResponses
        = new Dictionary<PathString, IActionResult>();
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        PathString path = context.HttpContext.Request.Path;
        if (CachedResponses.ContainsKey(path))
        {
            context.Result = CachedResponses[path];
            CachedResponses.Remove(path);
        }
    }
    public void OnResourceExecuted(ResourceExecutedContext context)
    {
        CachedResponses.Add(context.HttpContext.Request.Path, context.Result);
    }
}

  OnResourceExecution 方法通过将上下文对象的 Result 属性设置为先前缓存的操作结果,提供了短路管道的机会。如果为 Result 属性分配了一个值,则过滤器管道将短路,并执行操作结果来为客户端生成响应。缓存的操作结果只使用一次,然后从缓存中丢弃。如果没有为 Result 属性分配值,则请求传递到管道中的下一个步骤,该步骤可能是另一个过滤器或端点。
  将自定义资源过滤器应用到 Message.cshtml Razor Pages,并添加一个时间截,时间戳将帮助确定何时缓存操作结果。

@functions
{
    [SimpleCache]
    public class MessageModel : PageModel
    {
        public object Message { get; set; } = 
            $"{DateTime.Now.ToLongTimeString()}:This is the Message Razor Page";s
    }
}

  请求 https://localhost:44350/pages/message。因为这是对路径的第一个请求,所以不会缓存结果,请求将沿着管道转发。在处理响应时,资源过滤器将缓存操作结果供将来使用。重新加载浏览器以重复请求,会显示相同的时间戳,表明己使用了缓存的操作结果。缓存项在使用时被删除,这意味着重新加载浏览器将生成带有新时间戳的响应。
(2)创建异步资源过滤器
  异步资源过滤器的接口使用单个方法来接收用于沿着过滤器管道转发请求的委托。以下重新实现了前一个示例中的缓存过滤器,以便它实现 IAsyncResourceFilter 接口。

public class SimpleCacheAttribute : Attribute, IAsyncResourceFilter
{
    private Dictionary<PathString, IActionResult> CachedResponses
        = new Dictionary<PathString, IActionResult>();
    public async Task OnResourceExecutionAsync(ResourceExecutingContext context,
            ResourceExecutionDelegate next)
    {
        PathString path = context.HttpContext.Request.Path;
        if (CachedResponses.ContainsKey(path))
        {
            context.Result = CachedResponses[path];
            CachedResponses.Remove(path);
        }
        else
        {
            ResourceExecutedContext execContext = await next();
            CachedResponses.Add(context.HttpContext.Request.Path, execContext.Result);
        }
    }
}

4.3 操作过滤器

  与资源过滤器一样,操作过滤器执行两次。区别在于操作过滤器在模型绑定过程之后执行而资源过滤器在模型绑定之前执行。下面是 IActionFilter 接口:

namespace Microsoft.AspNetCore.Mvc.Filters
{
    public interface IActionrilter :IFilterMetadata
    {
        void OnActionExecuting(ActionExecutingContext context);
        void OnActionExecuted(ActionExecutedContext context);
    }
}

  异步操作过滤器是使用 IAsyncActionFilter 接口实现的。

namespace Microsoft.AspNetCore.Mvc.Filters 
{
    public interface IAsyncActionrilter :IFilterMetadata
    {
        Task OnActionExecutionAsync (ActionExecutingContext context,
                                     ActionExecutionDelegate next);
    }
}

(1)创建操作过滤器
  在 Filters 文件夹中添加一个名为 ChangeArgAttribute.cs 的类文件,并使用它来定义操作过滤器。

public class ChangeArgAttribute : Attribute, IActionActionFiler
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context,
            ActionExecutionDelegate next)
    {
        if (context.ActionArguments.ContainsKey("message1"))
        {
            context.ActionArguments["message1"] = "New message";
        }
        await next();
    }
}

  过滤器査找名为 message1 的操作参数,并更改用于调用操作方法的值。用于操作方法参数的值由模型绑定过程决定。 如下向 Home 控制器添加了一个操作方法,并应用了新的过滤器。

[ChangeArg]
public IActionResult Messages(string message1, string message2 = "Nome")
{
    return View("Message", $"{message1},{message2}");
}

  请求https://localhost:44350/home/messages?message1=hello&message2=world,模型绑定过程从查询字符串中定位操作方法定义的参数值。然后操作过滤器会修改其中一个值。
(2)使用特性基类实现操作过滤器
  操作属性也可以通过派生 ActionFiterAttribute 类来实现,该类扩展了 Atribute 并继承了IActionFilter 和 IAsyncActionFilter 接口,以便实现类只覆盖它们需要的方法。

public class ChangeArgAttribute : ActionFilterAttribute
{
    public override async Task OnActionExecutionAsync(ActionExecutingContext context,
            ActionExecutionDelegate next)
    {
        if (context.ActionArguments.ContainsKey("message1"))
        {
            context.ActionArguments["message1"] = "New message";
        }
        await next();
    }
}

(3)使用控制器过滤方法
  Controller 类是呈现 Razor 视图的控制器的基础,它实现了 IActionFilter 和 IAsyncActionFilter 接口,这意味着可以定义功能,并将其应用到由控制器和任何派生控制器定义的操作中。在 Controlers 文件夹的 Homecontoler.cs 中添加如下方法实现 ChangeArg 过滤器功能。

public override void OnActionExecuting(ActionExecutingContext context)
{
    if (context.ActionArguments.ContainsKey("message1"))
    {
        context.ActionArguments["message1"] = "New message";
    }
}

4.4 页面过滤器

  页面过滤器是 Razor Pages 等效的操作过滤器。下面是 IPageFilter 接口,它是由同步页面器实现的:

public interface IPageFilter : IFilterMetadata
{
    void OnPageHandlerExecuted(PageHandlerExecutedContext context);
    void OnPageHandlerExecuting(PageHandlerExecutingContext context);
    void OnPageHandlerSelected(PageHandlerSelectedContext context);
}

  在 ASP.NET Core 选择了页面处理程序方法后,但在执行模型绑定之前,调用 OnPageHandlerSelected 方法,这意味着处理程序方法的参数还没有确定。
  OnPageHandlerExecuting 方法在模型绑定过程完成之后,但在调用页面处理程序方法之前调用。
  OnPageHandlerExecuted 方法在调用页面处理程序方法之后,但在处理操作结果以创建响应之前调用。

  异步页面过滤器是通过实现 IAsyncPageFilter 接口创建的,它的定义如下。

public interface IAsyncPageFilter : IFilterMetadata
{
    Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, 
                                     PageHandlerExecutionDelegate next);
    Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context);
}

  OnPageHandlerExecutionAsync 在选中处理程序方法之后调用,它等价于同步的 OnPageHandlerExecuted 方法。为 OnPageHandlerExecutionAsync 提供了一个允许它短路管道的 PageHandlerExecutingContext 对象,以及一个被调用来传递请求的委托。委托产生一个PageHandlerExecutedContext 对象,该对象可用于检査或更改处理程序方法产生的操作结果。
(1)创建页面过滤器
  创建页面过滤器,将一个名为 ChangePageArgs.cs 的类文件添加到 Filters 文件夹。

public class ChangePageArgs : Attribute, IPageFilter
{
    public void OnPageHandlerSelected(PageHandlerSelectedContext context)
    {
        // do nothing
    }
    public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
    {
        if (context.HandlerArguments.ContainsKey("message1"))
        {
            context.HandlerArguments["message1"] = "New message";
        }
    }
    public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
    {
        // do nothing
    }
}

  修改 Message.cshtml Razor Pages 以定义处理程序方法,并应用了页面过滤器。页面过滤器可以应用于单个处理程序方法,也可以应用于页面模型类。

@functions
{
    //[SimpleCache]
    [ChangePageArgs]
    public class MessageModel : PageModel
    {
        public object Message { get; set; } =
            $"{DateTime.Now.ToLongTimeString()}:This is the Message Razor Page";

        public void OnGet(string message1, string message2)
        {
            Message = $"{message1}, {message2}";
        }
    }
}

  请求https://localhost:44350/home/messages?message1=hello&message2=world,页面过滤器替换 OnGet 处理程序方法的 message1 参数值。
(2)使用页面模型过滤方法
  PageModel 类用作页面模型类的基础,它实现了 IPageFilter 和 IAsyncPageFilter 接口,这意味着可以直接向页面模型添加过滤功能。在 Pages 文件夹的 Message.cshtml 文件中,注释掉特性,使用 PageModel 过滤方法。

public override void OnPageHandlerExecuting(PageHandlerExecutingContext context)
{
    if (context.HandlerArguments.ContainsKey("message1"))
    {
        context.HandlerArguments["message1"] = "New message";
    }
}

4.5 结果过滤器

  结果过滤器在操作结果用于生成响应之前和之后执行,允许在端点处理响应之后修改响应。下面是IResultFilter 接口的定义:

public interface IResultFilter : IFilterMetadata
{
    void OnResultExecuted(ResultExecutedContext context);
    void OnResultExecuting(ResultExecutingContext context);
}

  在端点生成操作结果后调用 OnResultExecuting 方法。0nResultExecuted 方法在操作结果执行后调用,为客户端生成响应。
  异步结果过滤器实现 IAsyncResultFilter 接口。

public interface IAsyncResultFilter : IFilterMetadata
{
    Task OnResultExecutionAsync(ResultExecutingContext context,
                                ResultExecutionDelegate next);
}

(1)理解始终运行的结果过滤器
  实现 IResultFilter 和 IAsyncResultFilter 接口的过滤器仅在请求由端点正常处理时使用。如果另一个过滤器使管道短路或出现异常,则不使用它们。即使在管道短路时,需要检查或更改响应的过滤器,可以实现 IAlwaysRunResultFiter 或 IAsycAlwaysRunResultFilter 接口。这些接口诞生自 IResultFilter 和 IAsyncResultFilter,但没有定义新的特性。相反,ASP.NET Core 检测始终运行的接口,并始终应用该过滤器。
(2)创建结果过滤器
  向 Filters 文件夹添加一个名为 ResultDiagnosticsAttribute.cs 的类文件。

public class ResultDiagnosticsAttribute : Attribute, IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(
        ResultExecutingContext context, ResultExecutionDelegate next)
    {
        if (context.HttpContext.Request.Query.ContainsKey("diag"))
        {
            Dictionary<string, string> diagData =
                new Dictionary<string, string>
                {
                    {"Result type", context.Result.GetType().Name }
                };
            if (context.Result is ViewResult vr)
            {
                diagData["View Name"] = vr.ViewName;
                diagData["Model Type"] = vr.ViewData.Model.GetType().Name;
                diagData["Model Data"] = vr.ViewData.Model.ToString();
            }
            else if (context.Result is PageResult pr)
            {
                diagData["Model Type"] = pr.Model.GetType().Name;
                diagData["Model Data"] = pr.ViewData.Model.ToString();
            }
            context.Result = new ViewResult()
            {
                ViewName = "/Views/Shared/Message.cshtml",
                ViewData = new ViewDataDictionary(
                    new EmptyModelMetadataProvider(),
                    new ModelStateDictionary())
                {
                    Model = diagData
                }
            };
        }
        await next();
    }
}

  此过滤器检查请求是否包含名为 diag 的查询字符串参数,如果包含,则创建一个显示诊断信息的结果,而不是端点生成的输出。
在 Controllers 文件夹的 HomeController.cs 文件中应用结果过滤器。

[ResultDiagnostics]
public class HomeController : Controller

  并请求 https://localhost:44350/?diag,过滤器将检测査询字符串参数。
(3)使用属性基类实现结果过滤器
  ResultFilterAttribute 类派生自 Attribute 并实现 IResultFilter 和IAsyncResultFilter 接口,可以作结果过滤器的基类。

public class ResultDiagnosticsAttribute : ResultFilterAttribute
{
    public override async Task OnResultExecutionAsync(
        ResultExecutingContext context, ResultExecutionDelegate next) 
    {
        ......
    }
}

4.6 异常过滤器

  使用异常过滤器,不必在每个操作方法中编写 ty...catch 块,即可响应异常。异常过滤器可以应用于控制器类、操作方法、页面模型类或处理程序方法。当端点或已应用于端点的操作、页面和结果过滤器不处理异常时,将调用它们。异常过滤器实现 IExceptionFilter 接口,其定义如下:

public interface IExceptionFilter : IFilterMetadata
{
    void OnException(ExceptionContext context);
}

  如果遇到未处理的异常,则调用 OnException方法。IAsyncExceptionrilter 接口可以用来创建异步异常过滤器。下面是异步接口的定义:

public interface IAsyncExceptionFilter : IFilterMetadata
{
    Task OnExceptionAsync(ExceptionContext context);
}

  OnExceptionAsync 方法是 xceptionFilter 接口中的 OnException 方法的异步对等物,当有未处理的异常时调用。
(1)创建异常过滤器
  异常过滤器可以通过实现一个过滤器接口或从 ExceptionFilterAttribute 类派生来创建,该类派生自 Attribute 并实现了 IExceptionFilter 和 IAsyncExceptionFilter。异常过滤器最常见的用途是为特定异常类型显示自定义错误页面,以便为用户提供比标准错误处理功能更有用的信息。
  要创建异常过滤器,将一个名为 RangeExceptionAttribute.cs 的类文件添加到 Filters 文件夹。

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

  此过滤器使用 ExceptionContext 对象来获取未处理异常的类型,如果类型是ArgumentOutOfRangeException,则创建一个操作结果,向用户显示一条消息。在 Controllers 文件夹的 HomeController.cs 文件中应用异常过滤器,将一个操作方法添加到主控制器中,对其应用异常过滤器。

[RangeException]
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 操作方法依赖于默认的路由模式,从请求 URL接收一个可空的 int 值。如果没有匹配的 URL 段,操作方法抛出一个 ArgumentNullException;如果 int 的值大于 10,则抛出一个 ArgumentOutOfRangeException。如果有一个值并且处于范围内,那么操作方法返回 ViewResult。
  并请求https:/localhost:44350/home/generateException/100。最后一个段超过 action 方法所期望的范围,该方法将抛出过滤器所处理的异常类型。如果请求 /home/GenerateException,那么过滤器不会处理操作方法抛出的异常,而使用默认的错误处理。

5 管理过滤器生命周期

  默认情况下,ASP.NET Core 管理它创建的过滤器对象,并在后续请求中重用它们。
  要创建一个显示生命周期的过滤器,将一个名为 GuidResponseAttribute.cs 的类文件添加到 Filters 文件夹中,用它来定义过滤器。

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class,
    AllowMultiple = true)]
public class GuidResponseAttribute : Attribute, IAsyncAlwaysRunResultFilter
{
    private int counter = 0;
    private string guid = Guid.NewGuid().ToString();
    public async Task OnResultExecutionAsync(ResultExecutingContext context,
        ResultExecutionDelegate next)
    {
        Dictionary<string, string> resultData;
        if (context.Result is ViewResult vr
            && vr.ViewData.Model is Dictionary<string, string> data)
        {
            resultData = data;
        }
        else
        {
            resultData = new Dictionary<string, string>();
            context.Result = new ViewResult()
            {
                ViewName = "/Views/Shared/Message.cshtml",
                ViewData = new ViewDataDictionary(
                    new EmptyModelMetadataProvider(),
                    new ModelStateDictionary())
                {
                    Model = resultData
                }
            };
        }
        while (resultData.ContainsKey($"Counter_{counter}"))
        {
            counter++;
        }
        resultData[$"Counter_{counter}"] = guid;
        await next();
    }
}

  此结果过滤器将端点产生的操作结果替换为呈现消息视图并显示唯一 GUID 值的操作结果,对过滤器进行了配置,以便它可以多次应用于相同的目标,并且如果管道中较早的过滤器创建了合适的结果,它将添加新消息。
  如下在 Controllers 文件夹的 HomeControler.cs 文件中,对主控制器应用了两次过滤器(为了简活美郇还删除了所有操作方法,只保留了一个)。

[HttpsOnly]
[ResultDiagnostics]
[GuidResponse]
[GuidResponse]
public class HomeController : Controller
{
    public async Task<IActionResult> Index()
    {
        return View("Message", "这是home控制器的Index方法");
    }
}

  要确认过滤器的重用,请重新启动 ASP.NET Core 并请求 https://localhost:44350/?diag。响应包含来自两个 GuidResponse 过滤器属性的 GUID 值。创建过滤器的两个实例来处理请求。重新加载浏览器,将看到显示的相同 GUID 值,这表明为处理第一个请求而创建的过滤器对象已重用。

5.1 创建过滤器工厂

  过滤器可以实现 IFilterFactory 接口来负责创建过滤器的实例,并指定是否可以重用这些实例。在 Filters 文件夹的 GuidResponseAtribute.cs 文件中实现接口,并为 IsReusable 属性返回 false,这将阻止过滤器的重用。

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class,
    AllowMultiple = true)]
public class GuidResponseAttribute : Attribute, IAsyncAlwaysRunResultFilter,
    IFilterFactory
{
    private int counter = 0;
    private string guid = Guid.NewGuid().ToString();
    public bool IsReusable => false;
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        return ActivatorUtilities
            .GetServiceOrCreateInstance<GuidResponseAttribute>(serviceProvider);
    }
    ......

  使用 GetServiceOrCreateInstance 方法创建新的过滤器对象,该方法由 Microsof.Extensices.DependencyInjecton 名称空间中的 ActivatorUtilities 类定义。尽管可以使用 new关键字创建过滤器,但此方法解析通过过滤器的构造函数声明的、对服务的任何依赖关系。
  要查看实现 IFiterFactory 接口的效果,请重新启动并请求https://localhost:44350/?diag。每次处理请求时,创建新的过滤器,并显示新的 GUID。

5.2 依赖注入过滤器的生命周期

  过滤器可以注册为服务,这允许通过依赖注入来控制它们的生命周期。 在 Startup.cs 文件中创建过滤器服务。

services.AddScoped<GuidResponseAttribute>();

6 创建全局过滤器

  全局过滤器应用于 ASPNET Core 处理的每一个请求,这意味着它们不必应用于单个控制器或Razor Pages。任何过滤器都可以用作全局过滤器;然而,操作过滤器将仅应用于端点是操作方法的请求,而页面过滤器将仅应用于端点是 Razor Pages 的请求。
  使用 Startup 类中的 options 模式设置全局过滤器,如下所示。

services.Configure<MvcOptions>(opts => opts.Filters.Add<HttpsOnlyAttribute>());

  MvcOptions.Filters 属性返回一个集合,其中添加了过滤器以全局应用它们,对于同样是服务的过滤器,可以使用 Add<T>方法或使用 AddService<T>方法。还有一个没有泛型类型参数的 Add 方法,可用于将特定对象注册为全局过滤器。

7 理解和改变过滤器的顺序

  过滤器以特定顺序运行:授权、资源、操作(或页面)和结果。但是,如果有给定类型的多个过滤器,则应用它们的顺序由应用过滤器的范围驱动。
为了演示它是如何工作的,在 Filters 文件夹中添加一个名为 MessageAttibute.cs 的类文件。

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class,
    AllowMultiple = true)]
public class MessageAttribute : Attribute, IAsyncAlwaysRunResultFilter
{
    private int counter = 0;
    private string msg;
    public MessageAttribute(string message) => msg = message;
    public async Task OnResultExecutionAsync(ResultExecutingContext context,
           ResultExecutionDelegate next)
    {
        Dictionary<string, string> resultData;
        if (context.Result is ViewResult vr
            && vr.ViewData.Model is Dictionary<string, string> data)
        {
            resultData = data;
        }
        else
        {
            resultData = new Dictionary<string, string>();
            context.Result = new ViewResult()
            {
                ViewName = "/Views/Shared/Message.cshtml",
                ViewData = new ViewDataDictionary(
                    new EmptyModelMetadataProvider(),
                    new ModelStateDictionary())
                {
                    Model = resultData
                }
            };
        }
        while (resultData.ContainsKey($"Message_{counter}"))
        {
            counter++;
        }
        resultData[$"Message_{counter}"] = msg;
        await next();
    }
}

  此结果过滤器使用前面示例中展示的技术来替换来自端点的结果,并允许多个过滤器构建一系列显示给用户的消息。在 Controllers 文件夹的 HomeController.cs 文件中将消息过滤器的几个实例应用于主控制器。

[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 async Task<IActionResult> Index()
    {
        return View("Message", "这是home控制器的Index方法");
    }
}

  全局注册了消息过滤器。

services.Configure<MvcOptions>(opts =>
{
    opts.Filters.Add<HttpsOnlyAttribute>();
    opts.Filters.Add(new MessageAttribute("This is the globally-scoped filter"));
});

  同一个过滤器有四个实例。要查看应用它们的顺序,请求https://localhost:44350。默认情况下,ASP.NET Core 运行全局过滤器,然后过滤器应用于控制器或页面模型类,最后过滤器应用于操作或处理程序方法。
改变过滤器顺序
  默认的顺序可以通过实现 IOrderedFilter 接口来改变。Order 属性返回一个 int 值,低值的过滤器会在高 Order 值的过滤器之前应用。

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class,
    AllowMultiple = true)]
public class MessageAttribute : Attribute, IAsyncAlwaysRunResultFilter,
        IOrderedFilter
{
    private int counter = 0;
    private string msg;
    public MessageAttribute(string message) => msg = message;
    public int Order { get; set; }
    ......
}
[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 async Task<IActionResult> Index()
    {
        return View("Message", "这是home控制器的Index方法");
    }
}
posted @ 2024-06-19 17:19  一纸年华  阅读(33)  评论(0编辑  收藏  举报