ASP.NET Core Filter與IOC的羈絆

yi念之間發表於2021-10-14

前言

    我們在使用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介面的都是一樣的,即所有的操作都是針對介面的,這也是物件導向程式設計的本質。如果有更多疑問,或作者描述不正確,歡迎大家評論區討論。

?歡迎掃碼關注我的公眾號? ASP.NET Core Filter與IOC的羈絆

相關文章