天天看點

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

👇歡迎掃碼關注我的公衆号👇

ASP.NET Core Controller與IOC的羁絆

繼續閱讀