我们在使用ASP.NET Core进行服务端应用开发的时候,或多或少都会涉及到使用Filter的场景。Filter简单来说是Action的拦截器,它可以在Action执行之前或者之后对请求信息进行处理。我们知道.Net Core默认是提供了IOC的功能,而且IOC是.Net Core的核心,.Net Core的底层基本上是基于IOC构建起来的,但是默认情况下自带的IOC不支持属性注入功能,但是我们在定义或使用Filter的时候有时候不得不针对某个Controller或Action,这种情况下我们不得不将Filter作为Attribute标记到Controller或Action上面,但是有时候Filter是需要通过构造函数注入依赖关系的,这个时候就有了一点小小的冲突,就是我们不得不解决在Controller或Action上使用Filter的时候,想办法去构建Filter的实例。本篇文章不是一篇讲解ASP.NET Core如何使用过滤器Filter的文章,而是探究一下Filter与IOC的奇妙关系的。
咱们上面说过了,我们所用的过滤器即Filter,无论如何都是需要去解决与IOC的关系的,特别是在当Filter作用到某些具体的Controller或Action上的时候。因为直接标记的话必须要给构造函数传递初始化参数,但是这些参数是需要通过DI注入进去的,而不是手动传递。微软给我们提供了解决方案来解决这个问题,那就是使用TypeFilterAttribute
或ServiceFilterAttribute
,关于这两个Attribute使用的方式,咱们先通过简单的示例演示一下。首先定义一个Filter,模拟一下需要注入的场景
public class MySampleActionFilter : Attribute, IActionFilter { private readonly IPersonService _personService; private readonly ILogger<MySampleActionFilter> _logger; //模拟需要注入一些依赖关系 public MySampleActionFilter(IPersonService personService, ILogger<MySampleActionFilter> logger) { _personService = personService; _logger = logger; _logger.LogInformation($"MySampleActionFilter.Ctor {DateTime.Now:yyyyMMddHHmmssffff}"); } public void OnActionExecuted(ActionExecutedContext context) { Person personService = _personService.GetPerson(1); _logger.LogInformation($"TraceId=[{context.HttpContext.TraceIdentifier}] MySampleActionFilter.OnActionExecuted "); } public void OnActionExecuting(ActionExecutingContext context) { _logger.LogInformation($"TraceId=[{context.HttpContext.TraceIdentifier}] MySampleActionFilter.OnActionExecuting "); } }
这里的日志功能ILogger在ASP.Net Core底层已经默认注入了,我们还模拟依赖了一些业务的场景,因此我们需要注入一些业务依赖,比如我们这里的PersonService。
public void ConfigureServices(IServiceCollection services) { //模拟注册一下业务依赖 services.AddScoped<IPersonService,PersonService>(); services.AddControllers(); }
这里我们先来演示一下单独在某些Controller或Action上使用Filter的情况,我们先来定义一个Action来模拟一下Filter的使用,由于Filter通过构造函数依赖了一下具体的服务所以我们先选择使用TypeFilterAttribute
来演示,具体使用方式如下
[Route("api/[controller]/[action]")] [ApiController] public class PersonController : ControllerBase { private readonly List<Person> _persons; public PersonController() { //模拟一下数据 _persons = new List<Person> { new Person{ Id=1,Name="张三" }, new Person{ Id=2,Name="李四" }, new Person{ Id=3,Name="王五" } }; } [HttpGet] //这里我们先通过TypeFilter的方式来使用定义的MySampleActionFilter [TypeFilter(typeof(MySampleActionFilter))] public List<Person> GetPersons() { return _persons; } }
然后我们运行起来示例,模拟请求一下GetPersons这个Action看一下效果,因为我们在定义的Filter中记录了日志信息,因此请求完成之后在控制台会打印出如下信息
info: Web5Test.MySampleActionFilter[0] MySampleActionFilter.Ctor 202110121820482450 info: Web5Test.MySampleActionFilter[0] TraceId=[0HMCDD7ARPKDK:00000003] MySampleActionFilter.OnActionExecuting info: Web5Test.MySampleActionFilter[0] TraceId=[0HMCDD7ARPKDK:00000003] MySampleActionFilter.OnActionExecuted
这个时候我们将TypeFilterAttribute
替换为ServiceFilterAttribute
来看一下效果,替换后的Action是这个样子的
[HttpGet] [ServiceFilter(typeof(MySampleActionFilter))] public List<Person> GetPersons() { return _persons; }
然后我们再来请求一下GetPersons这个Action,这个时候我们发现抛出了一个InvalidOperationException的异常,异常信息大致如下
System.InvalidOperationException: No service for type 'Web5Test.MySampleActionFilter' has been registered.
从这个异常信息我们可以看出我们自定义的MySampleActionFilter过滤器需要注册到IOC中去,所以我们需要注册一下
public void ConfigureServices(IServiceCollection services) { //模拟注册一下业务依赖 services.AddScoped<IPersonService,PersonService>(); //注册自定义的MySampleActionFilter services.AddScoped<MySampleActionFilter>(); services.AddControllers(); }
做了如上的修改之后,我们再次启动项目请求一下GetPersons这个Action,这个时候MySampleActionFilter可以正常工作了。
这里简单的说明一下关于需要注册Filter的生命周期时,如果你不知道该注册成哪种生命周期的话那就注册成成
Scope
,这个是一种比较合理的方式,也就是和Controller生命周期保持一致每次请求创建一个实例即可。注册成单例的话很多时候会因为使用不当出现一些问题。
通过上面的演示我们大概了解了TypeFilterAttribute
或ServiceFilterAttribute
的使用方式和区别。
TypeFilterAttribute
的时候我们的Filter过滤器是不需要注册到IOC中去的,因为它使用Microsoft.Extensions.DependencyInjection.ObjectFactory
对Filte过滤器类型进行实例化ServiceFilterAttribute
的时候我们需要提前将我们定义的Filter注册到IOC容器中去,因为它使用容器来创建Filter的实例很多时候呢,我们是针对全局使用Filter对所有的或者绝大多数的Action请求进行处理,这个时候我们会全局注册Filter而不需要在每个Controller或Action上一一注解。这个时候也涉及到关于Filter本身是否需要注册到IOC容器中的情况,这个地方需要注意的是Filter不是必须的需要托管到IOC容器当中去,但是一旦托管到IOC容器当中就需要注意不同注册Filter的方式,首先我们来看一下不将Filter注册到IOC的使用方式,还是那个示例
public void ConfigureServices(IServiceCollection services) { services.AddScoped<IPersonService,PersonService>(); services.AddControllers(options => { options.Filters.Add<MySampleActionFilter>(); }); }
只需要把自定义的MySampleActionFilter依赖的服务提前注册到IOC容器即可不需要多余的操作,这个时候MySampleActionFilter就可以正常的工作。还有一种方式就是你想让IOC容器去托管自定义的Filter,这个时候我们需要将Filter注册到容器中去,当然声明周期我们还是选择Scope
,这个时候我们需要注意一下注册全局Filter的方式了,如下所示
public void ConfigureServices(IServiceCollection services) { services.AddScoped<IPersonService,PersonService>(); services.AddScoped<MySampleActionFilter>(); services.AddControllers(options => { //这里需要注意注册Filter的方法应使用AddService options.Filters.AddService<MySampleActionFilter>(); }); }
如上面代码所示,为了能让Filter的实例来自于IOC容器,在注册全局Filter的时候我们应使用AddService
方法完成注册,否则的话即使使用Add
方法不会报错但是在IOC中你只能注册了个寂寞,总结一下全局注册的时候
Add
方法,这样的话Filter实例则不会通过IOC容器创建AddService
方法,如果使用了AddService
方法,但是你没有在IOC中注册Filter,则会抛出异常上面我们已经演示了将Filter托管到IOC容器和不使用IOC容器的使用方式,这方面微软考虑的也是很周到,不过就是容易让新手犯错。如果能熟练掌握,或者理解其中的工作原理的话,还是可以更好的使用这些,并且微软还为我们提供了一套灵活的扩展方式。想要更好的了解它们的工作方式,我们还得在源码下手。
首先我们来看一下TypeFilterAttribute
的源码,我们知道在某个Action上使用TypeFilterAttribute的时候是不要求将Filter注册到IOC中去的,因为这个时候Filter的实例是通过ObjectFactory
创建出来的。在开始之前我们需要知道一个常识那就是在ASP.NET Core上我们所使用的Filter都必须要实现IFilterMetadata
接口,这是ASP.NET Core底层知道Filter的唯一凭证,比如我们上面自定义的MySampleActionFilter是实现了IActionFilter接口,那么IActionFilter肯定是直接或间接的实现了IFilterMetadata接口,我们可以看一下IActionFilter接口的定义[点击查看源码