ASP.NET Core Filter与IOC的羁绊
前言
我们在使用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();
}
单独使用Filter
这里我们先来演示一下单独在某些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中你只能注册了个寂寞,总结一下全局注册的时候
- 如果你不想将全局注册的Filter托管到IOC容器中,那么需要使用
Add
方法,这样的话Filter实例则不会通过IOC容器创建 - 如果你想控制Filter实例的生命周期,则需要将Filter提前注册到IOC容器中去,这个时候注册全局Filter的时候就需要使用
AddService
方法,如果使用了AddService
方法,但是你没有在IOC中注册Filter,则会抛出异常
源码探究
上面我们已经演示了将Filter托管到IOC容器和不使用IOC容器的使用方式,这方面微软考虑的也是很周到,不过就是容易让新手犯错。如果能熟练掌握,或者理解其中的工作原理的话,还是可以更好的使用这些,并且微软还为我们提供了一套灵活的扩展方式。想要更好的了解它们的工作方式,我们还得在源码下手。
TypeFilterAttribute
首先我们来看一下TypeFilterAttribute
的源码,我们知道在某个Action上使用TypeFilterAttribute的时候是不要求将Filter注册到IOC中去的,因为这个时候Filter的实例是通过ObjectFactory
创建出来的。在开始之前我们需要知道一个常识那就是在ASP.NET Core上我们所使用的Filter都必须要实现IFilterMetadata
接口,这是ASP.NET Core底层知道Filter的唯一凭证,比如我们上面自定义的MySampleActionFilter是实现了IActionFilter接口,那么IActionFilter肯定是直接或间接的实现了IFilterMetadata接口,我们可以看一下IActionFilter接口的定义[点击查看源码👈]
public interface IActionFilter : IFilterMetadata
{
void OnActionExecuting(ActionExecutingContext context);
void OnActionExecuted(ActionExecutedContext context);
}
通过上面的代码我们可以看到Filter本身肯定是要实现自IFilterMetadata接口的,这个是Filter的身份标识。接下来我们就来看一下TypeFilterAttribute源码的定义[点击查看源码👈]
public class TypeFilterAttribute : Attribute, IFilterFactory, IOrderedFilter
{
//创建Filter实例的工厂
private ObjectFactory? _factory;
public TypeFilterAttribute(Type type)
{
ImplementationType = type ?? throw new ArgumentNullException(nameof(type));
}
/// <summary>
/// 创建Filter时需要的构造参数
/// </summary>
public object[]? Arguments { get; set; }
/// <summary>
/// Filter实例的类型
/// </summary>
public Type ImplementationType { get; }
/// <summary>
/// Filter的优先级顺序
/// </summary>
public int Order { get; set; }
/// <summary>
/// 是否跨请求使用
/// </summary>
public bool IsReusable { get; set; }
/// <summary>
/// 创建Filter实例的实现方法
/// </summary>
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new ArgumentNullException(nameof(serviceProvider));
}
if (_factory == null)
{
//获取自定义传递的初始化Filter实例的参数类型以创建ObjectFactory
var argumentTypes = Arguments?.Select(a => a.GetType())?.ToArray();
//通过ActivatorUtilities创建ObjectFactory
_factory = ActivatorUtilities.CreateFactory(ImplementationType, argumentTypes ?? Type.EmptyTypes);
}
//通过IServiceProvider实例和传递的初始换参数得到IFilterMetadata实例即Filter实例
var filter = (IFilterMetadata)_factory(serviceProvider, Arguments);
//可以是嵌套的IFilterFactory实例
if (filter is IFilterFactory filterFactory)
{
filter = filterFactory.CreateInstance(serviceProvider);
}
//返回创建的IFilterMetadata实例
return filter;
}
}
通过上面的代码我们可以得知TypeFilterAttribute
中包含一个CreateInstance
方法,而这个方法正是创建返回了一个IFilterMetadata
实例即Filter实例,而创建IFilterMetadata实例则是通过ActivatorUtilities
这个类创建的。在之前的文章中我们曾大致提到过这个类,ActivatorUtilities类可以借助IServiceProvider来创建一个具体的对象实例,所以当你不想使用DI的方式获取一个类的实例,但是这个类的依赖需要通过IOC容器去获得,那么可以借助ActivatorUtilities类来实现。需要注意的是虽然Filter实例是通过ActivatorUtilities创建出来的,而且它的依赖项来自IOC容器,但是FIlter实例本身并不受IOC容器托管
。所以我们在使用的时候并没有将Filter注册到IOC容器中去。
ServiceFilterAttribute
上面我们看到了TypeFilterAttribute的实现方式,接下来我们来看一下和它类似的ServiceFilterAttribute
的实现。我们知道ServiceFilterAttribute创建Filter实例必须要依赖IOC容器,即我们需要自行将Filter提前注册到IOC容器中去,这样才能通过ServiceFilterAttribute来正确的获取到Filter的实例,接下来我们就来通过源码来一探究竟[点击查看源码👈]
public class ServiceFilterAttribute : Attribute, IFilterFactory, IOrderedFilter
{
/// <summary>
/// 要实例化Filter的类型
/// </summary>
public ServiceFilterAttribute(Type type)
{
ServiceType = type ?? throw new ArgumentNullException(nameof(type));
}
/// <summary>
/// Filter执行的优先级顺序
/// </summary>
public int Order { get; set; }
/// <summary>
/// 要实例化Filter的类型
/// </summary>
public Type ServiceType { get; }
/// <summary>
/// 是否跨请求使用
/// </summary>
public bool IsReusable { get; set; }
/// <summary>
/// 创建Filter实例的实现方法
/// </summary>
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new ArgumentNullException(nameof(serviceProvider));
}
//直接在IServiceProvider实例中获取IFilterMetadata实例
var filter = (IFilterMetadata)serviceProvider.GetRequiredService(ServiceType);
//支持IFilterFactory自身的嵌套执行
if (filter is IFilterFactory filterFactory)
{
filter = filterFactory.CreateInstance(serviceProvider);
}
return filter;
}
}
通过上面的代码我们可以看到ServiceFilterAttribute与TypeFilterAttribute的不同之处。首先ServiceFilterAttribute不支持手动传递初始化参数,因为它初始化的依赖全部来自于IOC容器。其次IFilterMetadata实例本身也是直接在IOC容器中获取的,而并不是仅仅只是依赖关系使用IOC容器。这也就是为何我们在使用ServiceFilterAttribute的时候需要自行先将Filter注册到IOC容器中去。
IFilterFactory
我们上面看到了无论是ServiceFilterAttribute还是TypeFilterAttribute,它们都是实现了IFilterFactory
接口,它们之所以可以定义创建Filter实例的实现方法也完全是实现了CreateInstance
方法,所以本质都是IFilterFactory。通过这个名字我们可以看出它是创建Filter的工厂,ServiceFilterAttribute和TypeFilterAttribute只是通过这个接口实现了自己创建IFilterFactory的逻辑。这是微软给我们提供的一个灵活之处,通过它我们可以在请求管道的任意位置创建Filter实例。接下来我们就来看一下IFilterFactory的定义[点击查看源码👈]
public interface IFilterFactory : IFilterMetadata
{
/// <summary>
/// 是否跨请求使用
/// </summary>
bool IsReusable { get; }
/// <summary>
/// 创建Filter实例
/// </summary>
/// <param name="serviceProvider">IServiceProvider实例</param>
/// <returns>返回Filter实例</returns>
IFilterMetadata CreateInstance(IServiceProvider serviceProvider);
}
通过代码可知IFilterFactory也是实现了IFilterMetadata接口,所以它本身也是一个Filter,只是它比较特殊一些。既然它是一个Filter,但是它也很特殊,那么ASP.NET Core在使用的时候是如何区分是一个Filter实例,还是一个IFilterFactory实例呢?这两者存在一个本质的区别,Filter实例是可以直接在Action请求的时候拿来执行一些类似OnActionExecuting
或OnActionExecuted
的操作的,但是IFilterFactory实例需要先调用CreateInstance方法得到一个真正可以执行的Filter实例的。
这个我们可以在FilterProvider
中得到答案。IFilterProvider
是用来定义提供Filter实现的操作,通过它我们可以得到可执行的Filter实例,在它的默认实现DefaultFilterProvider
类中的OnProvidersExecuting
方法里调用了它自身的ProvideFilter
方法,看到方法的名字我们可以知道这是提供Filter实例之前的操作,在这里我们可以准备好Filter实例,我们来看一下OnProvidersExecuting方法的实现[点击查看源码👈]
public void OnProvidersExecuting(FilterProviderContext context)
{
//如果Action描述里的Filter描述存在,即存在Filter定义
if (context.ActionContext.ActionDescriptor.FilterDescriptors != null)
{
var results = context.Results;
var resultsCount = results.Count;
for (var i = 0; i < resultsCount; i++)
{
//循环调用了ProvideFilter方法
ProvideFilter(context, results[i]);
}
}
}
这个方法通过判断执行的Action是否存在需要执行的Filter,如果存在则获取可执行的Filter实例,因为每个Action上可能存在许多个可执行的Filter,所以这里采用了循环操作,那么核心就在ProvideFilter方法[点击查看源码👈]
public void ProvideFilter(FilterProviderContext context, FilterItem filterItem)
{
if (filterItem.Filter != null)
{
return;
}
var filter = filterItem.Descriptor.Filter;
//如果Filter不是IFilterFactory实例则是可以直接使用的Filter
if (filter is not IFilterFactory filterFactory)
{
//直接赋值Filter
filterItem.Filter = filter;
filterItem.IsReusable = true;
}
else
{
//如果是IFilterFactory实例
//获取IOC容器实例即IServiceProvider实例
var services = context.ActionContext.HttpContext.RequestServices;
//调用IFilterFactory的CreateInstance得到Filter实例
filterItem.Filter = filterFactory.CreateInstance(services);
filterItem.IsReusable = filterFactory.IsReusable;
if (filterItem.Filter == null)
{
throw new InvalidOperationException();
}
ApplyFilterToContainer(filterItem.Filter, filterFactory);
}
}
通过这个代码我们就可以看出,这里会判断Filter是常规的IFilterMetadata实例还是IFilterFactory实例,如果是IFilterFactory则需要调用它的CreateInstance方法得到一个可以直接使用的Filter实例,否则就可以直接使用这个Filter了。所以我们注册Filter的时候可以是任何IFilterMetadata实例,但是真正执行的时候需要转换成统一的可直接执行的类似ActionFilter的实例。
既然ServiceFilterAttribute和TypeFilterAttribute可以实现自IFilterFactory接口,那么我们完全可以自己通过IFilterFactory接口来实现一个Filter创建的工厂,这样的话为我们创建Filter提供了另一种思路,我们以我们上面自定义的MySampleActionFilter为例,为它创建一个MySampleActionFilterFactory工厂,实现代码如下
public class MySampleActionFilterFactory : Attribute, IFilterFactory
{
public bool IsReusable => false;
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
//我们这里模拟通过IServiceProvider获取依赖的实例
IPersonService personService = serviceProvider.GetService<IPersonService>();
ILogger<MySampleActionFilter> logger = serviceProvider.GetService<ILogger<MySampleActionFilter>>();
//通过依赖构造MySampleActionFilter实例并返回
return new MySampleActionFilter(personService,logger);
}
}
这样的话我们可以把MySampleActionFilterFactory同样作用于上面的示例代码中去,如下所示,执行效果是一样的
[HttpGet]
//[ServiceFilter(typeof(MySampleActionFilter))]
[MySampleActionFilterFactory]
public List<Person> GetPersons()
{
return _persons;
}
全局注册
之前我们通过示例看到,全局注册Filter的时候也存在是否将Filter注册到IOC容器的这种情况。既可以注册到IOC容器,也可以不注册到IOC容器,只不过添加过滤器的方法不一样,看着也挺神奇的,但是一旦用错IOC就容易注册了个寂寞。我们知道全局注册Filter的时候承载Filter的本质是一个集合,这个集合的名字叫FilterCollection
,这里我们只关注它的Add方法和AddService方法即可。FilterCollection继承自Collection<IFilterMetadata>
。在.Net Core中微软的代码风格是用特定的类继承自已有的泛型操作,这样的话可以让开发者更关注类功能的本身,而且还可以防止书写泛型出错,是个不错的思路。Add存在好几个重载方法但是本质都是调用最全的哪一个方法,接下来我们就来先看一下最本质的Add方法[点击查看源码👈]
public IFilterMetadata Add(Type filterType, int order)
{
if (filterType == null)
{
throw new ArgumentNullException(nameof(filterType));
}
//不是IFilterMetadata类型添加会报错
if (!typeof(IFilterMetadata).IsAssignableFrom(filterType))
{
throw new ArgumentException();
}
//最终还是将注册的Filter类型包装成TypeFilterAttribute
var filter = new TypeFilterAttribute(filterType) { Order = order };
Add(filter);
return filter;
}
有点意思,豁然开朗了,通过Add方法全局添加的Filter本质还是包装成了TypeFilterAttribute,这也就解释了为啥我们可以不用再IOC容器中注册Filter而之前使用Filter了原因就是TypeFilterAttribute帮我们创建了。那接下来我们再来看看AddService方法的实现[点击查看源码👈]
public IFilterMetadata AddService(Type filterType, int order)
{
if (filterType == null)
{
throw new ArgumentNullException(nameof(filterType));
}
//不是IFilterMetadata类型添加会报错
if (!typeof(IFilterMetadata).IsAssignableFrom(filterType))
{
throw new ArgumentException();
}
//最终还是将注册的Filter类型包装成ServiceFilterAttribute
var filter = new ServiceFilterAttribute(filterType) { Order = order };
Add(filter);
return filter;
}
同理AddService本质是将注册的Filter类型包装成了ServiceFilterAttribute,所以我们如果已经提前在IOC中注册了Filter,那么我们只需要直接使用AddService注册Filter即可。当然如果你不知道这个方法而是使用了Add方法也不会报错,只是IOC容器可能有点寂寞。不过微软的这思路确实值得我们学习,这种情况下处理逻辑是统一的,最终都是来自IFilterFactory
这个接口。
总结
通过本篇文章我们了解了在ASP.NET Core使用Filter的时候,Filter有构建实例的方式,即可以将Filter注册到IOC容器中去,也可以不用注册。区别就是你是否可以自行控制Filter实例的生命周期,整体来说微软的设计思路还是非常合理的,有助于我们统一处理Filter实例的生成。我们都知道自带的IOC只支持构造注入这样的话就给特定的Action构建Filter的时候带来了不便,微软给出了TypeFilterAttribute
和ServiceFilterAttribute
解决方案,接下来我们就总结一下它们俩
- TypeFilterAttribute和ServiceFilterAttribute都实现了
IFilterFactory
接口,只是创建Filter实例的方式不同。 - TypeFilterAttribute通过
ActivatorUtilities
创建Filter实例,虽然它的依赖模块来自IOC容器,但是Filter实例本身并不受IOC容器管理。 - ServiceFilterAttribute则是通过
IServiceProvider
获取了Filter实例,这样整个Filter是受到IOC容器管理的,注入当然是基础操作了。 - 全局注册Filter的时候如果没有将Filter注册到IOC容器中,则使用
Add方法
添加过滤器,Add方法的本质是将注册的Filter包装成TypeFilterAttribute - 如果全局注册Filter的时候Filter已经提前注册到IOC容器中,则使用
AddService方法
添加过滤器,AddService方法的本质是将注册的Filter包装成ServiceFilterAttribute
通过上面的描述相信大家能更好的理解Filter本身与IOC容器的关系,这样的话也能帮助大家在具体使用的时候知道如何去用,如何更合理的使用。这里我们是用的IActionFilter作为示例,不过没有没关系,只要是实现了IFilterMetadata接口的都是一样的,即所有的操作都是针对接口的,这也是面向对象编程的本质。如果有更多疑问,或作者描述不正确,欢迎大家评论区讨论。