使用过滤器 (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
属性.
常规处理办法:
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 中. 分别在页面接收到请求前, 与页面响应请求后拦截. |
结果过滤器 | 在接收到请求前, 或响应后修改结果. |
异常过滤器 | 用于操作处理过程中发生的异常. |
从描述看, 操作过滤器/页面过滤器 与 结果过滤器有什么区别不明晰.
过滤器有自己的管道, 下图为简化图:
过滤器有一个重要特性 -- 短路过滤器管道. 即在执行过滤器的时候, 可以判断是直接返回 (短路), 还是继续下一个过滤器.
ASP.NET Core 中将过滤器均定义为接口. 目的就是为了实现扩展. 同时也内置了一些特性类 (Attribute
).
过滤器类型 | 接口 | 属性类 |
---|---|---|
授权过滤器 | IAuthorizationFilter , IAsyncAuthorizationFilter | 无特性类 |
资源过滤器 | IResourceFilter , IAsyncResourceFilter | 无特性类 |
操作过滤器 | IActionFilter , IAsyncActionFilter | ActionFilterAttribute |
页面过滤器 | IPageFilter , IAsyncPageFilter | 无特性类 |
结果过滤器 | IResultFilter , IAsyncResultFilter , IAlwaysRunResultFilter , IAsyncAlwaysRunResultFilter | ResultFilterAttribute |
异常过滤器 | IExceptionFilter , IAsyncExceptionFilter | ExceptionFilterAttribute |
创建自定义过滤器
所有过滤器接口都实现一个公共接口: IFilterMetadata
, 该接口定义在 Microsoft.AspNetCore.Mvc.Filters
命名空间中.
namespace Microsoft.AspNetCore.Mvc.Filters {
public interface IFilterMetadata {}
}
由于每一个过滤器实现的任务不统一, 该接口是空的, 接口仅作为统一标识.
每一个具体的过滤器接口都含有对应的 Onxxx
入口方法, 该方法会提供 FilterContext
对象, 来提供上下文数据.
上下文数据中的常用属性有:
名称 | 描述 |
---|---|
ActionDescriptor | 该属性返回一个 ActionDiscriptor 对象, 用于描述操作方法. |
HttpContext | 该属性返回 HttpContext 对象, 用于描述请求与响应信息. |
ModalState | 该属性返回 ModalStateDictionary 对象, 该对象用于验证客户端发送的数据. |
RouteDaya | 该属性返回 RouteData 对象, 用于描述路由信息. |
Filters | 该属性返回 IList<IFilterMetadata> , 表示已经应用于操作方法的过滤器列表. |
理解授权过滤器
授权过滤器用于实现安全策略. 授权过滤器在其他过滤器和端点处理请求之前执行.
看起来最先执行.
接口定义如下:
public interface IAuthorizationFilter : IFilterMetadata {
void OnAuthorization(AuthorizationFilterContext context);
}
public interface IAsyncAuthorizationFilter : IFilterMetadata {
Task OnAuthorizationAsync(AuthorizationFilterContext context);
}
其中 AuthorizationFilterContext
派生自 FilterContext
, 并增加了 Result
属性. 该属性是 IActionResult
类型. 当请求不符合授权策略时, 授权过滤器就会设置该属性. 一旦设置该属性, 则不再继续下一个流程, 直接返回该结果.
作者给出一个 Demo, 校验请求只允许使用 https
- 创建
Filters
文件夹, 并创建HttpsOnlyAttribute.cs
文件. - 从
Attribute
,IAuthorizationFilter
派生. - 判断
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
在端点处理请求后, 在操作结果执行之前调用.
异步版本接口定义为:
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
项目.
操作过滤器也会执行两次, 与资源过滤器一样. 但是操作过滤器在模型绑定后执行, 而资源过滤器在模型绑定前执行.
也就是说:
- 在资源过滤器中进行拦截, 可以最大限度的减少操作, 从而减少不必要的操作, 进而提升性能.
- 而在模型绑定后进行拦截 (操作过滤器), 则可以对绑定的数据进行重写. 或对数据进行相关的校验.
操作过滤器接口定义为:
namespace Microsoft.AspNetCore.Mvc.Filters;
public interface IActionFilter: IFilterMetadata {
void OnActionExecuting(ActionExecutingContext context);
void OnActionExecuted(ActionExecutedContext context);
}
OnActionExecuting()
方法在模型绑定后, 进入端点处理之前调用.OnActionExecuted()
在端点处理函数处理数据之后调用.
需要操作的数据, 分别通过派生自 FilterContext
的 ActionExecutingContext
和 ActionExecutedContext
来提供.
ActionExecutingContext
常用属性
属性名 | 说明 |
---|---|
Controller | 该属性返回处理当前请求的动作方法所在的控制器. 而操作方法的信息可以从属性 ActionDescriptor 中获得. |
ActionArguments | 该属性存储一个字典, 其中是传递该动作方法的参数. |
Result | 处理结果, IActionResult 类型. 用于短路. |
FilterContext
派生自ActionContext
, 而ActionDescriptor
是ActionContext
的属性.
ActionArguments
测试后, 可以看成反射动作方法时需要传入的参数.
ActionExecutedContext
常用属性
属性名 | 说明 |
---|---|
Controller | 操作当前请求的控制器. |
Canceled | 如果在其他 OnActionExecuteing() 方法中已为 Result 赋值, 那么该属性为 true . 表示已被处理. |
Execption | 包含操作方法抛出的异常. |
ExceptionDispatchInfo | 包含异常的堆栈跟踪信息. |
ExceptionHandled | bool 类型, 表示该异常是否被处理. 处理后的异常不再传播. |
Result | 被处理的结果. |
每一个类型的过滤器存在一个洋葱结构. 例如可以有多个操作过滤器. 当一个操作过滤器在执行阶段 (
OnActionExecuting()
方法中) 被处理后 (为Result
赋值), 也即在此处短路. 就不会进入下一个过滤器 (如果存在的话), 也不会进入控制器的动作方法中. 进而也不会进入当前过滤器的处理完成的方法 (OnActionExecuted()
方法). 而是直接返回, 会进入前一个动作过滤器的完成方法中 (如果有的话).
这里不会执行 "操作过滤器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
派生也可以实现操作过滤器.
使用控制器过滤方法
Controller
类也实现了 IActionFilter
和 IAsyncActionFilter
接口. 也就是说, 可以在控制中直接定义 操作过滤 方法.
这个适用于 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()
方法.
异步接口为:
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
过滤器
结果过滤器是在端点的动作方法执行后, 再经过异常过滤器后才会执行的.
如果存在资源过滤器处的短路, 那么结果过滤器的方法不会执行.
ASP.NET Core 保留了 AlwaysRunResultFilter
过滤器, 无论是否存在短路, 都会触发该过滤器. 其用法与结果过滤器一样, 仅接口名不同.
结果过滤器也有特性基类 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 | 抛出的异常 (任何异常). |
ExceptionHandled | bool 类型, 用于表示该异常是否被处理. |
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 了");
}
然后触发异常会得到:
另一种实现是, 派生自 ExceptionFilterAttribute
特性类, 也是实现 OnException
方法, 为上下文的 Result
属性赋值. 使用时是在控制或方法上使用特性.
管理过滤器声明周期
默认情况下不用管理过滤器的声明周期, 它由 ASP.NET Core 去维护. 但如果想要自行控制, 可以考虑本节内容.
然后作者定义了一个结果过滤器特性 (从 Attribute
, 和 IAsyncAlwaysRunResultFilter
派生的过滤器). 然后在过滤器的定义上使用 AttributeUsage
特性标注: 允许多个, 允许在 方法与类 上使用 ([AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
).
- 结果过滤器类中提供了一个
Count
属性, 用于技术, 但凡调用该过滤器就会加一. - 同时提供一个 guid的属性, 提供唯一标识符.
- 该过滤器会将当前信息输出到视图中以供展示.
使用该过滤器的方式是在对应类上添加特性. 重启后可以看到过滤器被重用了.
代码可以参考书中的示例. 这里说一下结论.
代码中用到几次该过滤器, 就会创建几个过滤器的实例. 但是在该实例的处理上是单例的, 也就是说, 一旦创建了过滤器实例, 后续的请求会复用该实例. 逻辑上, 有点像采用单例模式注入了全局依赖的服务一样.
创建过滤器工厂
过滤器可以实现 IFilterFactory
接口, 该接口名为过滤器工厂, 负责用来创建过滤器. 使用该方式可以控制是否复用过滤器. 其接口成员有:
属性 | 描述 |
---|---|
IsReusable | bool 类型, 表示是否重用过滤器实例. |
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>())
来添加.
理解和改变过滤器的顺序
过滤器的执行按照一定的类型, 这个类型是约定好的:
但是同一个类型的过滤器, 其执行顺序是需要配置的 (全局过滤器会按照其 Add
的顺序执行).
默认情况下不同注册范围的过滤器:
- 首先执行全局过滤器
- 然后是类上作用的过滤器
- 最后是方法上的过滤器 (按照编写代码的先后顺序执行)
改变过滤器的顺序
改变默认的顺序可以实现 IOrderedFilter
接口:
namespace Microsoft.AspNetCore.Mvc.Filters {
public interface IOrderedFilter: IFilterMetadata {
in Order { get; }
}
}
实现该接口后, 过滤器在排序的时候会按照 Order
属性值来排序, 如果需要在所有默认过滤器之前执行该过滤器, 可以将其设置为负值.