ASP.NETCore的Filter

什么是Filter

过滤器(Filter)是 AOP[^AOP] 思想的一种实现,让我们在执行管道(pipeline)的特定阶段(之前或之后)执行代码,管道在选择了要执行的操作之后运行。

下面图片显示了管道所在位置

aspnetcore-filter-pipeline-1.png

通过使用过滤器可以实现 短路请求、缓存请求结果、日志统一记录、参数合法性验证、异常统一处理、返回值格式化 等,同时使业务代码更加简洁单纯,避免很多重复代码。

5种Filter

简单概括

  • Authorizaion Filter授权过滤器

    Authorization是五种Filter中最先运行的,用于确定是否已针对请求为用户授权。 如果请求未获授权,可以让管道短路。

    作用:主要用于实现复杂的权限角色认证登陆授权等操作。

  • Resource Filter资源过滤器

    OnResourceExecuting:在Authorization之后,Model Binding之前执行,对实现缓存或者对过滤管道进行短路特别有用。

    OnResourceExecuted:在管道的其余阶段完成之后运行代码。

    作用:主要用于进行资源缓存防盗链等操作。

  • Exception Filter异常过滤器

    异常处理的Filter,在向响应正文写入任何内容之前,对未经处理的异常应用全局策略。

    作用:可以进行全局的异常日志收集错误信息友好化处理等操作。

  • Action Filter操作过滤器

    最常使用的Filter,实际上包围了整个Action。

    OnActionExecuting:执行实际Action操作前触发,更改传递到操作中的参数。

    OnActionExecuted:执行实际Action操作后触发,更改从操作返回的结果。

    作用:做法太多了,可以用于执行操作日志参数验证权限控制等一系列操作。

  • Result Filter结果过滤器

    在执行操作结果之前和之后立即运行代码。仅当操作方法成功执行时,它们才会运行。对于必须围绕视图或格式化程序的执行的逻辑,它们很有用。

    OnResultExecuting:在操作结果执行之前调用。

    OnResultExecuted:在操作结果执行之后调用。

    作用:可以对结果进行格式化大小写转换缓存结果等一系列操作。

流程图

下图是我自制的一个比较完整的请求在Filter Pipeline中的流动的流程图。

需要特别注意的是Exception Filter的位置:

  • 它位置之下的流程中抛出的错误才会被其捕捉处理,然后再流经Resource FilterOnResourceExecuted方法后出去。

  • Exception Filter之上的Authorizaion FilterResource Filter抛出的错误实际都不会被Exception Filter捕捉处理。

aspnetcore-filter-pipeline-2.jpg (1003×1344) (raw.githubusercontent.com)

所有Filter均可通过不同的接口定义支持同步和异步的实现。根据需要执行的任务类型,选择同步或异步实现。

通过设置 Context.Result 来截断请求,可以是使Filter管道短路。

注意:下面的例子优先使用同步方式实现。下面的例子是一个WebApi项目。

Authorizaion Filter

同步:继承IResourceFilter接口,实现OnAuthorizationAsync方法

异步:继承IAsyncResourceFilter接口,实现AuthorizationFilterAsync方法

下面是一个实际的鉴权例子,由我以前一个WebApi项目改造而来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Reflection;
using System.Threading.Tasks;

namespace FilterTest.Filters
{
public class AuthorizationFilter : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
Console.WriteLine($"AuthorizationFilter.OnAuthorization, Request Path: {context.HttpContext.Request.Path}");

//判断是否能被匿名调用
var action = context.ActionDescriptor as ControllerActionDescriptor;
var allowAnonymousAttr = action.MethodInfo.GetCustomAttribute<AllowAnonymousAttribute>();
bool isAllowAnonymous = allowAnonymousAttr == null ? false : true;

//能被匿名调用,则继续执行;
//不能被匿名调用,则验证是否授权访问,如果授权未通过,则抛出“未授权访问”信息
try
{
if (isAllowAnonymous == false)
{
//验证是否授权访问,如果授权未通过,则抛出“未授权访问”信息,流程不在往下走
}

}
catch (Exception ex)
{
//对异常进行处理,需要注意抛出的异常不能被Exception Filter所捕获,因此不要想着直接throw出去
context.Result = new ObjectResult(new { rtnCode = 500, msg = "鉴权服务发生错误,请稍后重试或联系管理人员" }); ;
}
}
}
}

//startup.cs文件里注入
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(config =>
{
config.Filters.Add<AuthorizationFilter>();
});
}

需要注意的是如果在Authorizaion Filter中抛出的异常,那 并不会被Exception Filter所捕获

Resource Filter

同步:继承IResourceFilter接口,实现OnResourceExecutingOnResourceExecuted方法

异步:继承IAsyncResourceFilter接口,实现OnResourceExecutionAsync方法,注意没有OnResourceExecutedAsync这个方法

下面是简单的页面缓存例子,在OnResourceExecuted获得缓存,在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using Microsoft.AspNetCore.Mvc.Filters;
using System;

namespace FilterTest.Filters
{
public class ResourceFilter : IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
Console.WriteLine($"ResourceFilter.OnResourceExecuting, Request Path: {context.HttpContext.Request.Path}");

//判断是否由以该Request.Path为key的缓存,有就获得缓存内的value,然后构造response再返回
var cacheKey = context.HttpContext.Request.Path;
}

public void OnResourceExecuted(ResourceExecutedContext context)
{
Console.WriteLine($"ResourceFilter.OnResourceExecuted, Request Path: {context.HttpContext.Request.Path}");

//判断是否由以该Request.Path为key的缓存,没有就保存到缓存,再返回
var cacheKey = context.HttpContext.Request.Path;
}
}
}

//startup.cs文件里注入
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(config =>
{
config.Filters.Add<ResourceFilter>();
});
}

Exception Filter

同步:继承IExceptionFilter接口,实现OnException方法

异步:继承IAsyncExceptionFilter接口,实现OnExceptionAsync方法

下面例子catch了所有错误并进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Configuration;
using System;
namespace FilterTest.Filters
{
public class ExceptionFilter : IExceptionFilter
{
private readonly IConfiguration configuration;

public ExceptionFilter(IConfiguration configuration)
{
this.configuration = configuration;
}

public void OnException(ExceptionContext context)
{
Console.WriteLine($"ExceptionFilter.OnException, Request Path: {context.HttpContext.Request.Path}");

ObjectResult result;
var isShowDevExceptionMsg = "false";
//isShowDevExceptionMsg = configuration.GetSection("IsShowDevExceptionMsg").Value; //由配置得到
if (!string.IsNullOrEmpty(isShowDevExceptionMsg) && isShowDevExceptionMsg.ToLower() == "true")
{
result = new ObjectResult(new { rtnCode = 500, msg = "服务发生错误,请稍后重试或联系管理人员", devMsg = context.Exception.Message });
}
else
{
result = new ObjectResult(new { rtnCode = 500, msg = "服务发生错误,请稍后重试或联系管理人员" });
}
result.StatusCode = 500;
context.Result = result;
context.ExceptionHandled = true;
}
}
}

//startup.cs文件里注入
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(config =>
{
config.Filters.Add<ExceptionFilter>();
});
}

//新建FilterTestController.cs添加测试用Action
[HttpGet("TestException")]
public IActionResult TestException()
{
throw null;
}

需要注意Context.ExceptionHandled = true,设置后 标记异常已被处理,异常不会再抛出,后续的Exception Filter不会再触发

Action Filter

同步:继承IActionFilter接口,实现OnActionExecutingOnActionExecuted方法

异步:继承IAsyncActionFilter接口,实现OnActionExecutionAsync方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using Microsoft.AspNetCore.Mvc.Filters;
using System;

namespace FilterTest.Filters
{
public class ActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
//做权限校验或参数验证等
}

public void OnActionExecuted(ActionExecutedContext context)
{
//
}
}
}

//startup.cs文件里注入
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(config =>
{
config.Filters.Add<ActionFilter>();
});
}

需要注意异步的写法,await next();会调用Action或下一个Action Filter

1
2
3
4
5
6
7
8
9
public class ActionFilterAsync : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
//Before Action,相当于同步的OnActionExecuting
await next();
//After Action,相当于同步的OnActionExecuted
}
}

注意,同步于异步是能同时使用的

使用Action Filter的案例:

案例:自动启用事务的ActionFilter

案例:对请求限速的ActionFilter

Result Filter

同步:继承IResultFilter接口,实现OnResultExecutingOnResultExecuted方法

异步:继承IAsyncResultFilter接口,实现OnResultExecutionAsync方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using Microsoft.AspNetCore.Mvc.Filters;
using System;

namespace FilterTest.Filters
{
public class ResultFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
//Response的Header添加信息
context.HttpContext.Response.Headers.Add("MoreInfo", "Just a test");
}

public void OnResultExecuted(ResultExecutedContext context)
{
//在这里就不能再在Header添加信息了
//还可以在这里缓存结果内容
}
}
}

//startup.cs文件里注入
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(config =>
{
config.Filters.Add<ResultFilter>();
});
}

异步,与Action Filter的异步类似

1
2
3
4
5
6
7
8
9
public class ResultFilterAsync : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
//Before Action,相当于同步的OnResultExecuting
await next();
//After Action,相当于同步的OnResultExecuted
}
}

Filter的注册

Filter有三种注册方式ActionController全局 ,其作用域由低到高

Action注册

只针对特定的 Action,影响最小,适合于对特定 Action 进行特殊处理。

使用[TypeFilter(Type)]进行注册,如注册上面例子的 ResourceFilter。

1
2
3
4
5
6
[HttpGet]
[TypeFilter(typeof(ResourceFilter))]
public IActionResult Get()
{
return Ok();
}

Controller注册

影响该 Controller 下的所有 Action。

与上面Action注册的办法一样,通过[TypeFilter(Type)]进行注册。

1
2
3
4
5
6
7
8
9
10
11
[Route("api/[controller]")]
[ApiController]
[TypeFilter(typeof(ResourceFilter))]
public class FilterTestController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok();
}
}

全局注册

前面的例子使用的就是全局注册方式,该方式会影响到所有的进入Filter Pipeline的请求。

该注册方式能够很好的进行一些需要影响全局的处理,如:全局的异常处理、全局的日志记录

1
2
3
4
5
6
7
8
9
10
//startup.cs文件里注册
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(config =>
{
config.Filters.Add<ResourceFilter>();
//下面的也可以,不过不推荐
//config.Filters.Add(new ResourceFilter());
});
}

TypeFilter 和 ServiceFilter 注册方式

前面的Action注册Controller注册使用的都是TypeFilter的方式注册,实际还可以用ServiceFilter来注册,两者的异同为:

  • ServiceFilter和TypeFilter都实现了IFilterFactory,因此都可以使用注册的Filter中构造函数中注入的对象,不需要特殊处理。
  • ServiceFilter需要对自定义的Filter进行注册,TypeFilter不需要。
  • ServiceFilter的Filter生命周期源自于您如何注册,而TypeFilter每次都会创建一个新的实例。

使用ServiceFilter进行Action注册例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[HttpGet("TestException")]
[ServiceFilter(typeof(ExceptionFilter))]
public IActionResult TestException()
{
throw null;
return Ok();
}

//startup.cs文件里注册
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ExceptionFilter>();
}

特性化

编写Filter时多继承Attribute类就能像普通特性一般使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Configuration;
using System;

namespace FilterTest.Attributes
{
public class MyActionFilterAttribute : Attribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
Console.WriteLine($"****MyActionFilterAttribute.OnActionExecuting, Request Path: {context.HttpContext.Request.Path}, Time: {DateTime.Now.ToString("hh:mm:ss ffff")}");
}

public void OnActionExecuted(ActionExecutedContext context)
{
Console.WriteLine($"****MyActionFilterAttribute.OnActionExecuted, Request Path: {context.HttpContext.Request.Path}, Time: {DateTime.Now.ToString("hh:mm:ss ffff")}");
}
}
}

//直接使用
[HttpGet]
[MyActionFilter]
public IActionResult Get()
{
return Ok();
}

但是对于有构造函数的,且其构造函数的参数是由DI容器注入的,则需要在已编写完成的Filter外再加一层,通过继承TypeFilterAttribute来实现,而TypeFilterAttribute实现IFilterFactory接口,IFilterFactory 公开用于创建IFilterMetadata实例的CreateInstance方法,CreateInstance从服务容器 (DI) 中加载指定的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class MyActionFilterWithDIAttribute : TypeFilterAttribute
{
public MyActionFilterWithDIAttribute() : base(typeof(ActionFilterWithDI))
{
}
}

public class ActionFilterWithDI : IActionFilter
{
private readonly IConfiguration configuration;

//这里注入了IConfiguration
public ActionFilterWithDI(IConfiguration configuration)
{
this.configuration = configuration;
}

public void OnActionExecuting(ActionExecutingContext context)
{
Console.WriteLine($"****ActionFilterWithDI.OnActionExecuting, Request Path: {context.HttpContext.Request.Path}, Time: {DateTime.Now.ToString("hh:mm:ss ffff")}");
//throw new Exception("ActionFilter.OnActionExecuting Exception");
}

public void OnActionExecuted(ActionExecutedContext context)
{
Console.WriteLine($"****ActionFilterWithDI.OnActionExecuted, Request Path: {context.HttpContext.Request.Path}, Time: {DateTime.Now.ToString("hh:mm:ss ffff")}");
}
}

//直接使用
[HttpGet]
[MyActionFilterWithDI]
public IActionResult Get()
{
return Ok();
}

Filter的执行顺序

相同类型的Filter不同注册方式的执行顺序

首先对于同类型Filter同样注册方式的Filter,其执行顺序默认时先加的先执行。

而对于同类型Filter不同注册方式(action、Controller、全局)的Filter,以Aciton Filter为例其默认执行顺序如下:

  • OnActionExecuting(全局)

    • OnActionExecuting(Controller)

      • OnActionExecuting(action)

        • Action
      • OnActionExecuted(action)

    • OnActionExecuted(Controller)

  • OnActionExecuted(全局)

可以看到非常符合AOP的风格。

改变执行顺序

虽然默认的执行顺序如上,但是我们是可以更改其执行顺序的,只要我们创建Filter的时候同时实现IOrderedFilter接口,就能在注册时附带上排序值,数值越小优先权越高,没设置的默认值为0,默认值相等的按照上面的规则执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyActionFilterAttribute : Attribute, IActionFilter, IOrderedFilter
{
public int Order { get; set; } //实现的时候IOrderedFilter需要声明一个order属性

public void OnActionExecuting(ActionExecutingContext context)
{
Console.WriteLine($"****MyActionFilterAttribute.OnActionExecuting, Request Path: {context.HttpContext.Request.Path}, Time: {DateTime.Now.ToString("hh:mm:ss ffff")}");
}

public void OnActionExecuted(ActionExecutedContext context)
{
Console.WriteLine($"****MyActionFilterAttribute.OnActionExecuted, Request Path: {context.HttpContext.Request.Path}, Time: {DateTime.Now.ToString("hh:mm:ss ffff")}");
}
}

//使用时设置Order属性,如设置-1提升其优先权
[HttpGet]
[MyActionFilter(Order = -1)]
public IActionResult Get()
{
return Ok();
}

参考

ASP.NET Core Filters

Asp.Net Core Filter 深入浅出的那些事-AOP

ASP.NET Core 2 学习笔记(十四)Filters

[^AOP]:面向切面编程(AOP是Aspect Oriented Program的首字母缩写),在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。