jk's notes
  • 使用过滤器 (filter)

使用过滤器 (filter)

过滤器用于添加额外操作.

书中作者首先给出一个 DEMO 作为起点.

  • 初始化为一个 MVC 应用, 视图模型为 string 或 Dictionary<string, sgtring>, 然后输出字符串或依次输出键值对.
  • 作者提供了 MVC 和 RazorPages 的两个演示.
  • 然后启用了 https (通过配置 Properties/launchSettings.json 来实现).
  • 然后清除证书, 重新生成证书 (dotnet dev-certs https --clean 与 dotnet dev-certs https --trust).
  • 删除数据库 (dotnet ef database drop --force), 系统重启后会检查并自动创建数据库.
  • 运行干净的应用.

使用过滤器

设置一个场景, 假定某些资源必须使用 https 来请求. 实现方法是: 在动作方法中检查 Request 的 IsHttps 属性.

image-20240201101004614

常规处理办法:

public IActionResult Index() {
  if (Request.IsHttps) {
    return View(...);
  } else {
    return new StatusCodeResult(StatusCodes.Status403Forbidden);
  }
}

问题: 每次都需要在对应控制器的动作方法中编写这段代码, 维护起来困难. 对于间歇性维护的代码容易遗漏出现安全漏洞等问题.

解决办法: 使用过滤器, 在动作方法上添加 [RequestHttps] 即可解决. 也可以将其放在控制器类上.

说明: [RequestHttps] 是内置过滤器. 该过滤器的作用是仅允许 https 请求通过.

该过滤器的实现与上述案例不同.

而要在 RazorPages 中只需要将 [RequestHttps] 放到视图模型类上即可.

@function {
	[RequestHttps]
	public class XXXX: PageModel {
		...
	}
}

理解过滤器

简单来说过滤器有点像是在 ASP.NET Core 处理管道中不同路段添加的一个 hook. 逻辑上在处理过程经过该 hook 的时候, 如果没定义直接通过, 如果有定义则执行 hook, 进行下一步判断.

内置的过滤器的种类:

名称描述
授权过滤器用于应用程序的授权策略.
资源过滤器用于拦截请求, 通常用于实现类似于缓存等特性.
操作过滤器(MVC)用于在操作方法接收到请求之前, 或在操作方法生成响应后, 对请求与响应进行拦截改写.
页面过滤器(Pages)逻辑与操作过滤器类似, 但作用与 RazorPages 中. 分别在页面接收到请求前, 与页面响应请求后拦截.
结果过滤器在接收到请求前, 或响应后修改结果.
异常过滤器用于操作处理过程中发生的异常.

从描述看, 操作过滤器/页面过滤器 与 结果过滤器有什么区别不明晰.

过滤器有自己的管道, 下图为简化图:

image-20240201103648372

过滤器有一个重要特性 -- 短路过滤器管道. 即在执行过滤器的时候, 可以判断是直接返回 (短路), 还是继续下一个过滤器.

ASP.NET Core 中将过滤器均定义为接口. 目的就是为了实现扩展. 同时也内置了一些特性类 (Attribute).

过滤器类型接口属性类
授权过滤器IAuthorizationFilter, IAsyncAuthorizationFilter无特性类
资源过滤器IResourceFilter, IAsyncResourceFilter无特性类
操作过滤器IActionFilter, IAsyncActionFilterActionFilterAttribute
页面过滤器IPageFilter, IAsyncPageFilter无特性类
结果过滤器IResultFilter, IAsyncResultFilter, IAlwaysRunResultFilter, IAsyncAlwaysRunResultFilterResultFilterAttribute
异常过滤器IExceptionFilter, IAsyncExceptionFilterExceptionFilterAttribute

创建自定义过滤器

所有过滤器接口都实现一个公共接口: IFilterMetadata, 该接口定义在 Microsoft.AspNetCore.Mvc.Filters 命名空间中.

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

由于每一个过滤器实现的任务不统一, 该接口是空的, 接口仅作为统一标识.

image-20240201142740456

每一个具体的过滤器接口都含有对应的 Onxxx 入口方法, 该方法会提供 FilterContext 对象, 来提供上下文数据.

image-20240201142934886

上下文数据中的常用属性有:

名称描述
ActionDescriptor该属性返回一个 ActionDiscriptor 对象, 用于描述操作方法.
HttpContext该属性返回 HttpContext 对象, 用于描述请求与响应信息.
ModalState该属性返回 ModalStateDictionary 对象, 该对象用于验证客户端发送的数据.
RouteDaya该属性返回 RouteData 对象, 用于描述路由信息.
Filters该属性返回 IList<IFilterMetadata>, 表示已经应用于操作方法的过滤器列表.

理解授权过滤器

授权过滤器用于实现安全策略. 授权过滤器在其他过滤器和端点处理请求之前执行.

image-20240201144054023

看起来最先执行.

接口定义如下:

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

其中 AuthorizationFilterContext 派生自 FilterContext, 并增加了 Result 属性. 该属性是 IActionResult 类型. 当请求不符合授权策略时, 授权过滤器就会设置该属性. 一旦设置该属性, 则不再继续下一个流程, 直接返回该结果.

作者给出一个 Demo, 校验请求只允许使用 https

  1. 创建 Filters 文件夹, 并创建 HttpsOnlyAttribute.cs 文件.
  2. 从 Attribute, IAuthorizationFilter 派生.
  3. 判断 IsHttps, 如果不是为 Result 赋值, 以结束请求管道.
public class HttpsOnlyAttribute: Attribute, IAuthorizationFilter {
  public void OnAuthoration(AuthorizationFilterContext context) {
    if (!context.HttpContext.Request.IsHttps) {
      context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden);
    }
  }
}

使用的时候只需要在动作方法, 或控制器上使用 [HttpsOnly].

理解资源过滤器

资源过滤器对每个请求执行两次. 接口定义为:

public interface IResourceFilter : IFilterMetadata {
	void OnResourceExecuting(ResourceExecutingContext context);
	void OnResourceExecuted(ResourceExecutedContext context);
}
  • 方法 OnResourceExecuting 在处理请求时调用.

  • 而 OnResourceExecuted 在端点处理请求后, 在操作结果执行之前调用.

image-20240201150909899

异步版本接口定义为:

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

异步接口版本的第二个参数 next 是一个委托类型, 该委托返回 ResourceExecutedContext:

public delegate Task<ResourceExecutedContext> ResourceExecutionDelegate();

ResourceExecutingContext 和 ResourceExecutedContext 均派生自 FilterContext. 同时补充了一些新的属性. 常用的有 IActionResult 类型的 Result. 该属性赋值即中断当前管道 直接返回.

ResourceExecutedContext 还有一个 ValueProviderFactories 属性 (囧. 没找到).

应该是勘误, 这个属性在 ResourceExecutingContext 中.

创建资源过滤器

资源过滤器的一大用处是请求时拦截, 来判断是否短路请求或继续进入过滤器管道.

作者给出一个 DEMO, 模拟简易缓存功能.

  • 在请求进入时先经过资源过滤器, 读取缓存, 命中后直接返回. 缓存仅用一次后失效.
  • 在请求没命中资源过滤器中的缓存时, 会经过完整的过滤器筛选, 最后在管道后期进入过滤器的另一个方法, 可以将数据加入缓存, 待下次命中.

注意, 使用特性语法的过滤器无法在控制器中注入依赖, 除非实现了 IFilterFactory 接口, 并直接创建实例.

同步版本:

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

可以利用记录时间来查看缓存的效果.

异步版本: 会在执行 await next() 阻塞, 并继续执行后续内容, 在后续内容处理完成后回来继续后续代码.

public async Task OnResourceExecutionAsync(
  ResourceExecutingContext context, 
  ResourceExecutionDelegate next) 
{
  var path = context.HttpContext.Request.Path;
  if (CacheResponses.ContainsKey(path)) {
    context.Result = CacheResponses[path];
    CacheResponses.Remove(path);
  } else {
    ResourceExecutedContext ctx = await next();
    CacheResponses.Add(path, ctx.Result);  
  }
}

理解操作过滤器

操作过滤器与页面过滤器属于同一种类型的过滤器. 并且执行时机也是一样. 不同在于: 操作过滤器用于 MVC 项目, 而页面过滤器作用域 RazorPages 项目.

操作过滤器也会执行两次, 与资源过滤器一样. 但是操作过滤器在模型绑定后执行, 而资源过滤器在模型绑定前执行.

image-20240204175445766

也就是说:

  • 在资源过滤器中进行拦截, 可以最大限度的减少操作, 从而减少不必要的操作, 进而提升性能.
  • 而在模型绑定后进行拦截 (操作过滤器), 则可以对绑定的数据进行重写. 或对数据进行相关的校验.

操作过滤器接口定义为:

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

image-20240204181026534

  • OnActionExecuting() 方法在模型绑定后, 进入端点处理之前调用.
  • OnActionExecuted() 在端点处理函数处理数据之后调用.

需要操作的数据, 分别通过派生自 FilterContext 的 ActionExecutingContext 和 ActionExecutedContext 来提供.

ActionExecutingContext 常用属性

属性名说明
Controller该属性返回处理当前请求的动作方法所在的控制器. 而操作方法的信息可以从属性 ActionDescriptor 中获得.
ActionArguments该属性存储一个字典, 其中是传递该动作方法的参数.
Result处理结果, IActionResult 类型. 用于短路.

FilterContext 派生自 ActionContext, 而 ActionDescriptor 是 ActionContext 的属性.

ActionArguments 测试后, 可以看成反射动作方法时需要传入的参数.

ActionExecutedContext 常用属性

属性名说明
Controller操作当前请求的控制器.
Canceled如果在其他 OnActionExecuteing() 方法中已为 Result 赋值, 那么该属性为 true. 表示已被处理.
Execption包含操作方法抛出的异常.
ExceptionDispatchInfo包含异常的堆栈跟踪信息.
ExceptionHandledbool 类型, 表示该异常是否被处理. 处理后的异常不再传播.
Result被处理的结果.

每一个类型的过滤器存在一个洋葱结构. 例如可以有多个操作过滤器. 当一个操作过滤器在执行阶段 (OnActionExecuting() 方法中) 被处理后 (为 Result 赋值), 也即在此处短路. 就不会进入下一个过滤器 (如果存在的话), 也不会进入控制器的动作方法中. 进而也不会进入当前过滤器的处理完成的方法 (OnActionExecuted() 方法). 而是直接返回, 会进入前一个动作过滤器的完成方法中 (如果有的话).

image-20240205091631198

这里不会执行 "操作过滤器2" 的 OnActionExecuted() 方法, 而是直接短路, 进入到 "操作过滤器" 的 OnActionExecuted() 方法.

[jk] 满满的 Koa 的感觉. 这个 Result 与 Koa 中在各个中间件传值的对象感觉一样.

异步接口 IAsyncActionFilter 的实现为:

public inerface IAsyncActionFilter: IFilterMetadata: {
  Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next);
}

其执行逻辑与资源过滤器的逻辑一样.

创建操作过滤器

作者给出一个 Demo

  • 从 Attribute 和 IAsyncActionFilter 派生出 ChangeArgAttribute 特性过滤器.
  • 然后在进入方法中判断是否存在参数 message1
  • 存在该参数, 则为其重新赋值 (那么 端点处理函数获得的参数将会被改写)

使用 Attribute 基类实现操作过滤器

部分过滤器有 Attribute 基类, 从 ActionFilterAttribute 派生也可以实现操作过滤器.

image-20240205094305158

使用控制器过滤方法

Controller 类也实现了 IActionFilter 和 IAsyncActionFilter 接口. 也就是说, 可以在控制中直接定义 操作过滤 方法.

image-20240205095551149

这个适用于 MVC 项目, 而 WebAPI 项目的控制器继承自 ControllerBase, 因此无该功能.

[jk] 可见 Controller 和 ControllerBase 的实例化过程还是有很大差异的.

理解页面过滤器

页面过滤器是 RazorPages 中的操作过滤器.

namespace Microsoft.AspNetCore.Mvc.Filters;
public interface IPageFilter : IFilterMetadata {
  void OnPageHandlerExecuted(PageHandlerExecutedContext context);
  void OnPageHandlerExecuting(PageHandlerExecutingContext context);
  void OnPageHandlerSelected(PageHandlerSelectedContext context);
}

在模型绑定之前, 会调用 OnPageHandlerSelected() 方法, 模型绑定之后会执行 OnPageHandlerExecuting() 方法, 然后执行页面处理程序方法, 页面处理程序结束后会调用 OnPageHandlerExecuted() 方法.

image-20240205103713932

异步接口为:

public interface IAsyncPageFilter: IFilgterMetadata {
  Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context);
  Task OnPageHandlerExecutionAsync(PageHandlerExecutingCOntext context, PageHanderExecutionDelete next);
}

OnPageHandlerSelected 方法

执行该方法时, 模型还没有绑定, 即处理方法的参数还未确定. 该方法不能短路管道, 但是可以修改即将接收请求的处理程序方法. 该方法通过上下文参数 PageHandlerSelectedContext 来获得数据. 常用属性包含:

名称描述
ActionDescriptor该属性返回 RazorPages 的描述.
HandlerMethod此属性返回一个描述所选处理程序方法的 HandlerMethodDescriptor 对象.
HandlerInstance此属性返回处理请求的 RazorPages 的实例.

OnPageHandlerExecuting 方法

该方法使用 PageHandlerExecutingContext 参数来接收数据:

名称描述
HandlerArguments该属性返回一个字典, 表示页面处理程序接收到的参数.
Result结果, 可用于短路管道.

OnPageHandlerExecuted 方法

该方法使用参数 PageHandlerExecutedContext 来接收响应数据:

名称描述
Canceled表示是否被其他过滤器处理.
Exception引用异常.
ExceptionHandled表示是否被处理异常.
Result响应的结果.

创建页面过滤器

作者实现了一个基于特性的页面过滤器 (派生自 IPageFilter 和 Attribute), 来实现操作过滤器中一样的功能.

使用页面模型过滤方法

PageModel 也实现了 IPageFilter 和 IAsyncPageFilter. 与 MVC 项目一样, 可以在模型类中来重写对应的过滤方法.

理解结果过滤器

结果过滤器在操作结果用于生成响应之前和之后执行. 这样就允许即使在端点处返回了操作结果, 也可以在结果过滤器中修改响应内容.

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

OnResultExecuting 方法在操作结果生成后执行. 而 OnResultExecuted 方法是在操作结果执行后调用, 为客户端生成响应. 其分别使用 ResultExecutingContext 和 ResultExecutedContext 来接收参数.

ResultExecutingContext 的常用属性

属性名描述
Result用于设置结果, 短路管道.
ValueProviderFactories返回 IList<IValueProviderFactory>, 可访问模型绑定过程中提供值的对象.

ResultExecutedContext 的常用属性

属性名描述
Canceled描述是否被其他过滤器短路了.
Controller
Exception
ExceptionHandled
Result返回响应操作结果, 该属性是只读的.

异步版本的过滤器为:

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

AlwaysRunResultFilter 过滤器

结果过滤器是在端点的动作方法执行后, 再经过异常过滤器后才会执行的.

image-20240205145144767

如果存在资源过滤器处的短路, 那么结果过滤器的方法不会执行.

ASP.NET Core 保留了 AlwaysRunResultFilter 过滤器, 无论是否存在短路, 都会触发该过滤器. 其用法与结果过滤器一样, 仅接口名不同.

image-20240205150852273

结果过滤器也有特性基类 ResultFilterAttribute, 可以进行派生.

理解异常过滤器

减少页面中 try...catch 块.可以统一捕获异常. 异常过滤器可以作用与 控制器类, 动作方法, 页面模型类, 和处理程序方法上.

那些未处理异常发生时会调用异常过滤器.

注意, 部分其他过滤器中可以设置 上下文的 ExceptionHandled = true 来阻止该过程.

接口定义如下:

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

遇到未处理异常时, 会调用 OnException 或 OnExceptionAsync 方法来处理异常.

上下文 ExceptionContext 常用属性有:

名称描述
Exception抛出的异常 (任何异常).
ExceptionHandledbool 类型, 用于表示该异常是否被处理.
Result设置用于生成响应的 IActionResult.

创建异常过滤器

异常过滤器可以通过派生接口 (IExceptionFilter, IAsyncExceptionFilter), 或特性 (ExceptionAttribute) 的方式来实现.

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

public class MyExceptionFilter : IExceptionFilter {
  readonly ILogger<MyExceptionFilter> _logger;
  public MyExceptionFilter(ILogger<MyExceptionFilter> logger) {
    _logger = logger;
  }

  public void OnException(ExceptionContext context) {
    _logger.LogError(context.Exception, "异常记录");
    
    context.Result = new OkObjectResult(new {
      Success = true,
      Message = context.Exception.Message,
      Status = StatusCodes.Status500InternalServerError
    });
  }
}

将其定义为全局过滤器

builder.Services.Configure<MvcOptions>(opts => opts.Filters.Add<MyExceptionFilter>());

将动作方法定义为:

[HttpGet]
public IActionResult Get(bool trigger = false) {
  if (trigger) {
    throw new Exception("一段抛出的异常");
  }
  return Ok("Ok 了");
}

然后触发异常会得到:

image-20240201163643016

image-20240201163805293

另一种实现是, 派生自 ExceptionFilterAttribute 特性类, 也是实现 OnException 方法, 为上下文的 Result 属性赋值. 使用时是在控制或方法上使用特性.

管理过滤器声明周期

默认情况下不用管理过滤器的声明周期, 它由 ASP.NET Core 去维护. 但如果想要自行控制, 可以考虑本节内容.

然后作者定义了一个结果过滤器特性 (从 Attribute, 和 IAsyncAlwaysRunResultFilter 派生的过滤器). 然后在过滤器的定义上使用 AttributeUsage 特性标注: 允许多个, 允许在 方法与类 上使用 ([AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]).

  • 结果过滤器类中提供了一个 Count 属性, 用于技术, 但凡调用该过滤器就会加一.
  • 同时提供一个 guid的属性, 提供唯一标识符.
  • 该过滤器会将当前信息输出到视图中以供展示.

使用该过滤器的方式是在对应类上添加特性. 重启后可以看到过滤器被重用了.

代码可以参考书中的示例. 这里说一下结论.

代码中用到几次该过滤器, 就会创建几个过滤器的实例. 但是在该实例的处理上是单例的, 也就是说, 一旦创建了过滤器实例, 后续的请求会复用该实例. 逻辑上, 有点像采用单例模式注入了全局依赖的服务一样.

创建过滤器工厂

过滤器可以实现 IFilterFactory 接口, 该接口名为过滤器工厂, 负责用来创建过滤器. 使用该方式可以控制是否复用过滤器. 其接口成员有:

属性描述
IsReusablebool 类型, 表示是否重用过滤器实例.
CreateInstance(serviceProvider)调用此方法来创建过滤器实例.

似乎只需要添加该接口, 实现这两个成员即可. 系统的调用似乎是自动的.

可以在执行过滤器的方法中打印出 Guid 的值, 可以发现每次会不一样, 即表示不再是复用实例.

使用依赖注入来管理过滤器生命周期

过滤器可以注册为服务. ch14 使用依赖注入一章中有介绍. 例如:

services.AddScoped<MyFilter>();

在不使用 IFilterFactory 和 Attribute 时, 可以使用 [ServiceFilter(typeof(MyFilter))].

创建全局过滤器

全局过滤器不需要使用, 会默认作用于所有的请求.

代码实现上有两组:

  • 官方文档的实现, 是在 AddController() 方法中添加, 使用 opts.Filters.Add<T>() 来添加.
  • 书中作者是利用 services.Configure<MvcOptions>(opts => opts.Filters.Add<XXX>()) 来添加.

理解和改变过滤器的顺序

过滤器的执行按照一定的类型, 这个类型是约定好的:

image-20240201103648372

但是同一个类型的过滤器, 其执行顺序是需要配置的 (全局过滤器会按照其 Add 的顺序执行).

默认情况下不同注册范围的过滤器:

  1. 首先执行全局过滤器
  2. 然后是类上作用的过滤器
  3. 最后是方法上的过滤器 (按照编写代码的先后顺序执行)

改变过滤器的顺序

改变默认的顺序可以实现 IOrderedFilter 接口:

namespace Microsoft.AspNetCore.Mvc.Filters {
  public interface IOrderedFilter: IFilterMetadata {
    in Order { get; }
  }
}

实现该接口后, 过滤器在排序的时候会按照 Order 属性值来排序, 如果需要在所有默认过滤器之前执行该过滤器, 可以将其设置为负值.

小结 (略)

Last Updated:
Contributors: jk