ASP.NET Core Filter与IOC的羁绊
Posted yi念之间
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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接口的定义[点击查看源码源码解析:ASP.NET Core Controller与IOC的羁绊
前言
看到标题可能大家会有所疑问Controller和IOC能有啥羁绊,但是我还是拒绝当一个标题党的。相信有很大一部分人已经知道了这么一个结论,默认情况下ASP.NET Core的Controller并不会托管到IOC容器中,注意关键字我说的是"默认",首先咱们不先说为什么,如果还有不知道这个结论的同学们可以自己验证一下,验证方式也很简单,大概可以通过以下几种方式。
验证Controller不在IOC中
首先,我们可以尝试在ServiceProvider中获取某个Controller实例,比如
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var productController = app.ApplicationServices.GetService<ProductController>();
}
这是最直接的方式,可以在IOC容器中获取注册过的类型实例,很显然结果会为null。另一种方式,也是利用它的另一个特征,那就是通过构造注入的方式,如下所示我们在OrderController中注入ProductController,显然这种方式是不合理的,但是为了求证一个结果,我们这里仅做演示,强烈不建议实际开发中这么写,这是不规范也是不合理的写法
public class OrderController : Controller
{
private readonly ProductController _productController;
public OrderController(ProductController productController)
{
_productController = productController;
}
public IActionResult Index()
{
return View();
}
}
结果显然是会报一个错InvalidOperationException: Unable to resolve service for type 'ProductController' while attempting to activate 'OrderController'。原因就是因为ProductController并不在IOC容器中,所以通过注入的方式会报错。还有一种方式,可能不太常用,这个是利用注入的一个特征,可能有些同学已经了解过了,那就是通过自带的DI,即使一个类中包含多个构造函数,它也会选择最优的一个,也就是说自带的DI允许类包含多个构造函数。利用这个特征,我们可以在Controller中验证一下
public class OrderController : Controller
{
private readonly IOrderService _orderService;
private readonly IPersonService _personService;
public OrderController(IOrderService orderService)
{
_orderService = orderService;
}
public OrderController(IOrderService orderService, IPersonService personService)
{
_orderService = orderService;
_personService = personService;
}
public IActionResult Index()
{
return View();
}
}
我们在Controller中编写了两个构造函数,理论上来说这是符合DI特征的,运行起来测试一下,依然会报错InvalidOperationException: Multiple constructors accepting all given argument types have been found in type 'OrderController'. There should only be one applicable constructor。以上种种都是为了证实一个结论,默认情况下Controller并不会托管到IOC当中。
DefaultControllerFactory源码探究
上面虽然我们看到了一些现象,能说明Controller默认情况下并不在IOC中托管,但是还没有足够的说服力,接下来我们就来查看源码,这是最有说服力的。我们找到Controller工厂注册的地方,在MvcCoreServiceCollectionExtensions扩展类中[点击查看源码
以上是关于ASP.NET Core Filter与IOC的羁绊的主要内容,如果未能解决你的问题,请参考以下文章
源码解析:ASP.NET Core Controller与IOC的羁绊
ASP.NET Core Web 应用程序系列- 使用ASP.NET Core内置的IoC容器DI进行批量依赖注入(MVC当中应用)
ASP.NET Core Web 应用程序系列- 使用ASP.NET Core内置的IoC容器DI进行批量依赖注入