7

ASP.NET Core Filter与IOC的羁绊

 2 years ago
source link: https://www.cnblogs.com/wucy/p/15398927.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

👇欢迎扫码关注我的公众号👇 2042116-20200622133425514-1420050576.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK