ASP.NET Core Controller與IOC的羈絆

前言

    看到標題可能你們會有所疑問Controller和IOC能有啥羈絆,可是我仍是拒絕當一個標題黨的。相信有很大一部分人已經知道了這麼一個結論,默認狀況下ASP.NET Core的Controller並不會託管到IOC容器中,注意關鍵字我說的是"默認",首先我們不先說爲何,若是還有不知道這個結論的同窗們能夠本身驗證一下,驗證方式也很簡單,大概能夠經過如下幾種方式。html

驗證Controller不在IOC中

首先,咱們能夠嘗試在ServiceProvider中獲取某個Controller實例,好比git

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    var productController = app.ApplicationServices.GetService<ProductController>();
}

這是最直接的方式,能夠在IOC容器中獲取註冊過的類型實例,很顯然結果會爲null。另外一種方式,也是利用它的另外一個特徵,那就是經過構造注入的方式,以下所示咱們在OrderController中注入ProductController,顯然這種方式是不合理的,可是爲了求證一個結果,咱們這裏僅作演示,強烈不建議實際開發中這麼寫,這是不規範也是不合理的寫法github

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中驗證一下web

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擴展類中[點擊查看源碼👈]的AddMvcCoreServices方法裏緩存

//給IControllerFactory註冊默認的Controller工廠類DefaultControllerFactory
//也是Controller建立的入口
services.TryAddSingleton<IControllerFactory, DefaultControllerFactory>();
//真正建立Controller的工做類DefaultControllerActivator
services.TryAddTransient<IControllerActivator, DefaultControllerActivator>();

由此咱們能夠得出,默認的Controller建立工廠類爲DefaultControllerFactory,那麼咱們直接找到源碼位置[點擊查看源碼👈],
爲了方便閱讀,精簡一下源碼以下所示sass

internal class DefaultControllerFactory : IControllerFactory
{
    //真正建立Controller的工做者
    private readonly IControllerActivator _controllerActivator;
    private readonly IControllerPropertyActivator[] _propertyActivators;

    public DefaultControllerFactory(
        IControllerActivator controllerActivator,
        IEnumerable<IControllerPropertyActivator> propertyActivators)
    {
        _controllerActivator = controllerActivator;
        _propertyActivators = propertyActivators.ToArray();
    }

    /// <summary>
    /// 建立Controller實例的方法
    /// </summary>
    public object CreateController(ControllerContext context)
    {
        //建立Controller實例的具體方法(這是關鍵方法)
        var controller = _controllerActivator.Create(context);
        foreach (var propertyActivator in _propertyActivators)
        {
            propertyActivator.Activate(context, controller);
        }
        return controller;
    }

    /// <summary>
    /// 釋放Controller實例的方法
    /// </summary>
    public void ReleaseController(ControllerContext context, object controller)
    {
        _controllerActivator.Release(context, controller);
    }
}

用過上面的源碼可知,真正建立Controller的地方在_controllerActivator.Create方法中,經過上面的源碼可知爲IControllerActivator默認註冊的是DefaultControllerActivator類,直接找到源碼位置[點擊查看源碼👈],咱們繼續簡化一下源碼以下所示app

internal class DefaultControllerActivator : IControllerActivator
{
    private readonly ITypeActivatorCache _typeActivatorCache;

    public DefaultControllerActivator(ITypeActivatorCache typeActivatorCache)
    {
        _typeActivatorCache = typeActivatorCache;
    }

    /// <summary>
    /// Controller實例的建立方法
    /// </summary>
    public object Create(ControllerContext controllerContext)
    {
        //獲取Controller類型信息
        var controllerTypeInfo = controllerContext.ActionDescriptor.ControllerTypeInfo;
        //獲取ServiceProvider
        var serviceProvider = controllerContext.HttpContext.RequestServices;
        //建立controller實例
        return _typeActivatorCache.CreateInstance<object>(serviceProvider, controllerTypeInfo.AsType());
    }

    /// <summary>
    /// 釋放Controller實例
    /// </summary>
    public void Release(ControllerContext context, object controller)
    {
        //若是controller實現了IDisposable接口,那麼Release的時候會自動調用Controller的Dispose方法
        //若是咱們在Controller中存在須要釋放或者關閉的操做,能夠再Controller的Dispose方法中統一釋放
        if (controller is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

經過上面的代碼咱們依然要繼續深刻到ITypeActivatorCache實現中去尋找答案,經過查看MvcCoreServiceCollectionExtensions類的AddMvcCoreServices方法源碼咱們能夠找到以下信息框架

services.TryAddSingleton<ITypeActivatorCache, TypeActivatorCache>();

有了這個信息,咱們能夠直接找到TypeActivatorCache類的源碼[點擊查看源碼👈]代碼並很少,大體以下所示ide

internal class TypeActivatorCache : ITypeActivatorCache
{
    //建立ObjectFactory的委託
    private readonly Func<Type, ObjectFactory> _createFactory =
        (type) => ActivatorUtilities.CreateFactory(type, Type.EmptyTypes);
    //Controller類型和對應建立Controller實例的ObjectFactory實例的緩存
    private readonly ConcurrentDictionary<Type, ObjectFactory> _typeActivatorCache =
           new ConcurrentDictionary<Type, ObjectFactory>();

    /// <summary>
    /// 真正建立實例的地方
    /// </summary>
    public TInstance CreateInstance<TInstance>(
        IServiceProvider serviceProvider,
        Type implementationType)
    {
        //真正建立的操做是createFactory
        //經過Controller類型在ConcurrentDictionary緩存中得到ObjectFactory
        //而ObjectFactory實例由ActivatorUtilities.CreateFactory方法建立的
        var createFactory = _typeActivatorCache.GetOrAdd(implementationType, _createFactory);
        //返回建立實例
        return (TInstance)createFactory(serviceProvider, arguments: null);
    }
}

經過上面類的代碼咱們能夠清晰的得出一個結論,默認狀況下Controller實例是由ObjectFactory建立出來的,而ObjectFactory實例是由ActivatorUtilities的CreateFactory建立出來,因此Controller實例每次都是由ObjectFactory建立而來,並不是註冊到IOC容器中。而且咱們還能夠獲得一個結論ObjectFactory應該是一個委託,咱們找到ObjectFactory定義的地方[點擊查看源碼👈]

delegate object ObjectFactory(IServiceProvider serviceProvider, object[] arguments);

這個確實如咱們猜測的那般,這個委託會經過IServiceProvider實例去構建類型的實例,經過上述源碼相關的描述咱們會產生一個疑問,既然Controller實例並不是由IOC容器託管,它由ObjectFactory建立而來,可是ObjectFactory實例又是由ActivatorUtilities構建的,那麼生產對象的核心也就在ActivatorUtilities類中,接下來咱們就來探究一下ActivatorUtilities的神祕面紗。

ActivatorUtilities類的探究

    書接上面,咱們知道了ActivatorUtilities類是建立Controller實例最底層的地方,那麼ActivatorUtilities到底和容器是啥關係,由於咱們看到了ActivatorUtilities建立實例須要依賴ServiceProvider,一切都要從找到ActivatorUtilities類的源碼開始。咱們最初接觸這個類的地方在於它經過CreateFactory方法建立了ObjectFactory實例,那麼咱們就從這個地方開始,找到源碼位置[點擊查看源碼👈]實現以下

public static ObjectFactory CreateFactory(Type instanceType, Type[] argumentTypes)
{
    //查找instanceType的構造函數
    //找到構造信息ConstructorInfo
    //獲得給定類型與查找類型instanceType構造函數的映射關係
    FindApplicableConstructor(instanceType, argumentTypes, out ConstructorInfo constructor, out int?[] parameterMap);
    //構建IServiceProvider類型參數
    var provider = Expression.Parameter(typeof(IServiceProvider), "provider");
    //構建給定類型參數數組參數
    var argumentArray = Expression.Parameter(typeof(object[]), "argumentArray");
    //經過構造信息、構造參數對應關係、容器和給定類型構建表達式樹Body
    var factoryExpressionBody = BuildFactoryExpression(constructor, parameterMap, provider, argumentArray);
    //構建lambda
    var factoryLamda = Expression.Lambda<Func<IServiceProvider, object[], object>>(
        factoryExpressionBody, provider, argumentArray);
    var result = factoryLamda.Compile();
    //返回執行結果
    return result.Invoke;
}

ActivatorUtilities類的CreateFactory方法代碼雖然比較簡單,可是它涉及到調用了其餘方法,因爲嵌套的比較深代碼比較多,並且不是本文講述的重點,咱們就再也不這裏細說了,咱們能夠大概的描述一下它的工做流程。

  • 首先在給定的類型裏查找到合適的構造函數,這裏咱們能夠理解爲查找Controller的構造函數。
  • 而後獲得構造信息,並獲得構造函數的參數與給定類型參數的對應關係
  • 經過構造信息和構造參數的對應關係,在IServiceProvider獲得對應類型的實例爲構造函數賦值
  • 最後通過上面的操做經過初始化指定的構造函數來建立給定Controller類型的實例
    綜上述的相關步驟,咱們能夠獲得一個結論,Controller實例的初始化是經過遍歷Controller類型構造函數裏的參數,而後根據構造函數每一個參數的類型在IServiceProvider查找已經註冊到容器中相關的類型實例,最終初始化獲得的Controller實例。這就是在IServiceProvider獲得須要的依賴關係,而後建立本身的實例,它內部是使用的表達式樹來完成的這一切,能夠理解爲更高效的反射方式。
    關於ActivatorUtilities類還包含了其餘比較實用的方法,好比CreateInstance方法
public static T CreateInstance<T>(IServiceProvider provider, params object[] parameters)

它能夠經過構造注入的方式建立指定類型T的實例,其中構造函數裏具體的參數實例是經過在IServiceProvider實例裏獲取到的,好比咱們咱們有這麼一個類

public class OrderController 
{
    private readonly IOrderService _orderService;
    private readonly IPersonService _personService;

    public OrderController(IOrderService orderService, IPersonService personService)
    {
        _orderService = orderService;
        _personService = personService;
    }
}

其中它所依賴的IOrderService和IPersonService實例是註冊到IOC容器中的

IServiceCollection services = new ServiceCollection()
 .AddScoped<IPersonService, PersonService>()
 .AddScoped<IOrderService, OrderService>();

而後你想獲取到OrderController的實例,可是它只包含一個有參構造函數,可是構造函數的參數都以註冊到IOC容器中。當存在這種場景你即可以經過如下方式獲得你想要的類型實例,以下所示

IServiceProvider serviceProvider = services.BuildServiceProvider();
OrderController orderController = ActivatorUtilities.CreateInstance<OrderController>(serviceProvider);

即便你的類型OrderController並無註冊到IOC容器中,可是它的依賴都在容器中,你也能夠經過構造注入的方式獲得你想要的實例。總的來講ActivatorUtilities裏的方法仍是比較實用的,有興趣的同窗能夠自行嘗試一下,也能夠經過查看ActivatorUtilities源碼的方式瞭解它的工做原理。

AddControllersAsServices方法

    上面咱們主要是講解了默認狀況下Controller並非託管到IOC容器中的,它只是表現出來的讓你覺得它是在IOC容器中,由於它能夠經過構造函數注入相關實例,這主要是ActivatorUtilities類的功勞。說了這麼多Controller實例到底可不能夠註冊到IOC容器中,讓它成爲真正受到IOC容器的託管者。要解決這個,必需要知足兩點條件

  • 首先,須要將Controller註冊到IOC容器中,可是僅僅這樣還不夠,由於Controller是由ControllerFactory建立而來
  • 其次,咱們要改造ControllerFactory類中建立Controller實例的地方讓它從容器中獲取Controller實例,這樣就解決了全部的問題
    若是咱們本身去實現將Controller託管到IOC容器中,就須要知足以上兩個操做一個是要將Controller放入容器,而後讓建立Controller的地方從IOC容器中直接獲取Controller實例。慶幸的是,微軟幫咱們封裝了一個相關的方法,它能夠幫咱們解決將Controller託管到IOC容器的問題,它的使用方法以下所示
services.AddMvc().AddControllersAsServices();
//或其餘方式,這取決於你構建的Web項目的用途能夠是WebApi、Mvc、RazorPage等
//services.AddMvcCore().AddControllersAsServices();

相信你們都看到了,玄機就在AddControllersAsServices方法中,可是它存在於MvcCoreMvcBuilderExtensions類和MvcCoreMvcCoreBuilderExtensions類中,不過問題不大,由於它們的代碼是徹底同樣的。只是由於你能夠經過多種方式構建Web項目好比AddMvc或者AddMvcCore,廢話很少說直接上代碼[點擊查看源碼👈]

public static IMvcBuilder AddControllersAsServices(this IMvcBuilder builder)
{
    if (builder == null)
    {
        throw new ArgumentNullException(nameof(builder));
    }
    var feature = new ControllerFeature();
    builder.PartManager.PopulateFeature(feature);
    //第一將Controller實例添加到IOC容器中
    foreach (var controller in feature.Controllers.Select(c => c.AsType()))
    {
        //註冊的聲明週期是Transient
        builder.Services.TryAddTransient(controller, controller);
    }
    //第二替換掉本來DefaultControllerActivator的爲ServiceBasedControllerActivator
    builder.Services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());
    return builder;
}

第一點沒問題那就是將Controller實例添加到IOC容器中,第二點它替換掉了DefaultControllerActivator爲爲ServiceBasedControllerActivator。經過上面咱們講述的源碼瞭解到DefaultControllerActivator是默認提供Controller實例的地方是獲取Controller實例的核心所在,那麼咱們看看ServiceBasedControllerActivator與DefaultControllerActivator到底有何不一樣,直接貼出代碼[點擊查看源碼👈]

public class ServiceBasedControllerActivator : IControllerActivator
{
    public object Create(ControllerContext actionContext)
    {
        if (actionContext == null)
        {
            throw new ArgumentNullException(nameof(actionContext));
        }
        //獲取Controller類型
        var controllerType = actionContext.ActionDescriptor.ControllerTypeInfo.AsType();
        //經過Controller類型在容器中獲取實例
        return actionContext.HttpContext.RequestServices.GetRequiredService(controllerType);
    }

    public virtual void Release(ControllerContext context, object controller)
    {
    }
}

    相信你們對上面的代碼一目瞭然了,和咱們上面描述的同樣,將建立Controller實例的地方改造了在容器中獲取的方式。不知道你們有沒有注意到ServiceBasedControllerActivator的Release的方法竟然沒有實現,這並非我沒有粘貼出來,確實是沒有代碼,以前咱們看到的DefaultControllerActivator但是有調用Controller的Disposed的方法,這裏卻啥也沒有。相信聰明的你已經想到了,由於Controller已經託管到了IOC容器中,因此他的生命及其相關釋放都是由IOC容器完成的,因此這裏不須要任何操做。
    咱們上面還看到了註冊Controller實例的時候使用的是TryAddTransient方法,也就是說每次都會建立Controller實例,至於爲何,我想大概是由於每次請求都其實只會須要一個Controller實例,何況EFCore的註冊方式官方建議也是Scope的,而這裏的Scope正是對應的一次Controller請求。在加上自帶的IOC會提高依賴類型的聲明週期,若是將Controller註冊爲單例的話若是使用了EFCore那麼它也會被提高爲單例,這樣會存在很大的問題。也許正是基於這個緣由默認纔將Controller註冊爲Transient類型的,固然這並不表明只能註冊爲Transient類型的,若是你不使用相似EFCore這種須要做用域爲Scope的服務的時候,並且保證使用的主鍵均可以使用單例的話,徹底能夠將Controller註冊爲別的生命週期,固然這種方式我的不是很建議。

Controller結合Autofac

    有時候你們可能會結合Autofac一塊兒使用,Autofac確實是一款很是優秀的IOC框架,它它支持屬性和構造兩種方式注入,關於Autofac託管自帶IOC的原理我們在以前的文章淺談.Net Core DependencyInjection源碼探究中曾詳細的講解過,這裏我們就不過多的描述了,我們今天要說的是Autofac和Controller的結合。若是你想保持和原有的IOC一致的使用習慣,即只使用構造注入的話,你只須要完成兩步便可

  • 首先將默認的IOC容器替換爲Autofac,具體操做也很是簡單,以下所示
public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
              .ConfigureWebHostDefaults(webBuilder =>
              {
                  webBuilder.UseStartup<Startup>();
              })
              //只須要在這裏設置ServiceProviderFactory爲AutofacServiceProviderFactory便可
              .UseServiceProviderFactory(new AutofacServiceProviderFactory());
  • 而後就是我們以前說的,要將Controller放入容器中,而後修改生產Controller實例的ControllerFactory的操做爲在容器中獲取,固然這一步微軟已經爲咱們封裝了便捷的方法
services.AddMvc().AddControllersAsServices();

只須要經過上面簡單得兩步,既能夠將Controller託管到Autofac容器中。可是,咱們說過了Autofac還支持屬性注入,可是默認的方式只支持構造注入的方式,那麼怎麼讓Controller支持屬性注入呢?咱們還得從最根本的出發,那就是解決Controller實例存和取的問題

  • 首先爲了讓Controller託管到Autofac中而且支持屬性注入,那麼就只能使用Autofac的方式去註冊Controller實例,具體操做是在Startup類中添加ConfigureContainer方法,而後註冊Controller並聲明支持屬性注入
public void ConfigureContainer(ContainerBuilder builder)
{
    var controllerBaseType = typeof(ControllerBase);
    //掃描Controller類
    builder.RegisterAssemblyTypes(typeof(Program).Assembly)
    .Where(t => controllerBaseType.IsAssignableFrom(t) && t != controllerBaseType)
    //屬性注入
    .PropertiesAutowired();
}
  • 其次是解決取的問題,這裏咱們就不須要AddControllersAsServices方法了,由於AddControllersAsServices解決了Controller實例在IOC中存和取的問題,可是這裏咱們只須要解決Controller取得問題說只須要使用ServiceBasedControllerActivator便可,具體操做是
services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());

僅須要在默認的狀態下完成這兩步,既能夠解決Controller託管到Autofac中並支持屬性注入的問題,這也是最合理的方式。固然若是你使用AddControllersAsServices但是能夠實現相同的效果了,只不過是不必將容器重複的放入容器中了。

總結

    本文咱們講述了關於ASP.NET Core Controller與IOC結合的問題,我以爲這是有必要讓每一個人都有所瞭解的知識點,由於在平常的Web開發中Controller太經常使用了,知道這個問題可能會讓你們在開發中少走一點彎路,接下來咱們來總結一下本文大體講解的內容

  • 首先說明了一個現象,那就是默認狀況下Controller並不在IOC容器中,咱們也經過幾個示例驗證了一下。
  • 其次講解了默認狀況下創造Controller實例真正的類ActivatorUtilities,並大體講解了ActivatorUtilities的用途。
  • 而後咱們找到了將Controller託管到IOC容器中的辦法AddControllersAsServices,並探究了它的源碼,瞭解了它的工做方式。
  • 最後咱們又演示瞭如何使用最合理的方式將Controller結合Autofac一塊兒使用,而且支持屬性注入。

本次講解到這裏就差很少了,但願原本就知道的同窗們能加深一點了解,不知道的同窗可以給大家提供一點幫助,可以在平常開發中少走一點彎路。新的一年開始了,本篇文章是我2021年的第一篇文章,新的一年感謝你們的支持。

👇歡迎掃碼關注個人公衆號👇
相關文章
相關標籤/搜索