前言
看到标題可能大家會有所疑問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年的第一篇文章,新的一年感謝大家的支援。
👇歡迎掃碼關注我的公衆号👇
