ASP.NET Core Controller與IOC的羈絆

yi念之間發表於2021-01-04

前言

    看到標題可能大家會有所疑問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年的第一篇文章,新的一年感謝大家的支援。

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

相關文章