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注入进去的,而不是手动传递。微软给我们提供了解决方案来解决这个问题,那就是使用TypeFilterAttributeServiceFilterAttribute,关于这两个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生命周期保持一致每次请求创建一个实例即可。注册成单例的话很多时候会因为使用不当出现一些问题。

通过上面的演示我们大概了解了TypeFilterAttributeServiceFilterAttribute的使用方式和区别。

•使用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请求的时候拿来执行一些类似OnActionExecutingOnActionExecuted的操作的,但是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的时候带来了不便,微软给出了TypeFilterAttributeServiceFilterAttribute解决方案,接下来我们就总结一下它们俩

  • 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接口的都是一样的,即所有的操作都是针对接口的,这也是面向对象编程的本质。如果有更多疑问,或作者描述不正确,欢迎大家评论区讨论。