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

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

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

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類,直接找到源碼位置[點擊查看源碼👈],我們繼續簡化一下源碼如下所示

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類的源碼[點擊查看源碼👈]代碼並不多,大致如下所示

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年的第一篇文章,新的一年感謝大家的支持。

👇歡迎掃碼關注我的公衆號👇
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章