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