故事背景
最近在把自己的一個老專案從Framework遷移到.Net Core 3.0,資料訪問這塊選擇的是EFCore+Mysql,使用EF的話不可避免要和DbContext打交道,在Core中的常規用法一般是:創建一個XXXContext類繼承自DbContext,實作一個擁有DbContextOptions引數的構造器,在啟動類StartUp中的ConfigureServices方法里呼叫IServiceCollection的擴展方法AddDbContext,把背景關系注入到DI容器中,然后在使用的地方通過建構式的引數獲取實體,OK,沒任何毛病,官方示例也都是這么來用的,但是,通過建構式這種方式來獲取背景關系實體其實很不方便,比如在Attribute或者靜態類中,又或者是系統啟動時初始化一些資料,更多的是如下一種場景:
public class BaseController : Controller { public BloggingContext _dbContext; public BaseController(BloggingContext dbContext) { _dbContext = dbContext; } public bool BlogExist(int id) { return _dbContext.Blogs.Any(x => x.BlogId == id); } } public class BlogsController : BaseController { public BlogsController(BloggingContext dbContext) : base(dbContext) { } }
從上面的代碼可以看到,任何要繼承BaseController的類都要寫一個“多余”的建構式,如果引數再多幾個,這將是無法忍受的(就算只有一個引數我也忍受不了),那么怎樣才能更優雅的獲取資料庫背景關系實體呢,我想到以下幾種辦法,
DbContext從哪來
1、 直接開溜new
回歸原始,既然要創建實體,沒有比直接new一個更好的辦法了,在Framework中沒有DI的時候也差不多都這么干,但在EFCore中不同的是,DbContext不再提供無參建構式,取而代之的是必須傳入一個DbContextOptions型別的引數,這個引數通常是做一些背景關系選項配置例如使用什么型別資料庫連接字串是多少,
public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { }
默認情況下,我們已經在StartUp中注冊背景關系的時候做了配置,DI容器會自動幫我們把options傳進來,如果要手動new一個背景關系,那豈不是每次都要自己傳?不行,這太痛苦了,那有沒有辦法不傳這個引數?肯定也是有的,我們可以去掉有參建構式,然后重寫DbContext中的OnConfiguring方法,在這個方法中做資料庫配置:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("Filename=./efcoredemo.db"); }
即使是這樣,依然有不夠優雅的地方,那就是連接字串被硬編碼在代碼中,不能做到從組態檔讀取,反正我忍受不了,只能再尋找其他方案,
2、 從DI容器手動獲取
既然前面已經在啟動類中注冊了背景關系,那么從DI容器中獲取實體肯定是沒問題的,于是我寫了這樣一句測驗代碼用來驗證猜想:
var context = app.ApplicationServices.GetService<BloggingContext>();
不過很遺憾拋出了例外:

報錯資訊說的很明確,不能從root provider中獲取這個服務,我從G站下載了DI框架的原始碼(地址是https://github.com/aspnet/Extensions/tree/master/src/DependencyInjection),拿報錯資訊進行反向追溯,發現例外來自于CallSiteValidator類的ValidateResolution方法:
public void ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope) { if (ReferenceEquals(scope, rootScope) && _scopedServices.TryGetValue(serviceType, out var scopedService)) { if (serviceType == scopedService) { throw new InvalidOperationException( Resources.FormatDirectScopedResolvedFromRootException(serviceType, nameof(ServiceLifetime.Scoped).ToLowerInvariant())); } throw new InvalidOperationException( Resources.FormatScopedResolvedFromRootException( serviceType, scopedService, nameof(ServiceLifetime.Scoped).ToLowerInvariant())); } }View Code
繼續往上,看到了GetService方法的實作:
internal object GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) { if (_disposed) { ThrowHelper.ThrowObjectDisposedException(); } var realizedService = RealizedServices.GetOrAdd(serviceType, _createServiceAccessor); _callback?.OnResolve(serviceType, serviceProviderEngineScope); DependencyInjectionEventSource.Log.ServiceResolved(serviceType); return realizedService.Invoke(serviceProviderEngineScope); }View Code
可以看到,_callback在為空的情況下是不會做驗證的,于是猜想有引數能對它進行配置,把追溯物件換成_callback繼續往上翻,在DI框架的核心類ServiceProvider中找到如下方法:
internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options) { IServiceProviderEngineCallback callback = null; if (options.ValidateScopes) { callback = this; _callSiteValidator = new CallSiteValidator(); } //省略.... }
說明我的猜想沒錯,驗證是受ValidateScopes控制的,這樣來看,把ValidateScopes設定成False就可以解決了,這也是網上普遍的解決方案:
.UseDefaultServiceProvider(options => { options.ValidateScopes = false; })
但這樣做是極其危險的,
為什么危險?到底什么是root provider?那就要從原生DI的生命周期說起,我們知道,DI容器被封裝成一個IServiceProvider物件,服務都是從這里來獲取,不過這并不是一個單一物件,它是具有層級結構的,最頂層的即前面提到的root provider,可以理解為僅屬于系統層面的DI控制中心,在Asp.Net Core中,內置的DI有3種服務模式,分別是Singleton、Transient、Scoped,Singleton服務實體是保存在root provider中的,所以它才能做到全域單例,相對應的Scoped,是保存在某一個provider中的,它能保證在這個provider中是單例的,而Transient服務則是隨時需要隨時創建,用完就丟棄,由此可知,除非是在root provider中獲取一個單例服務,否則必須要指定一個服務范圍(Scope),這個驗證是通過ServiceProviderOptions的ValidateScopes來控制的,默認情況下,Asp.Net Core框架在創建HostBuilder的時候會判定當前是否開發環境,在開發環境下會開啟這個驗證:

所以前面那種關閉驗證的方式是錯誤的,這是因為,root provider只有一個,如果恰好有某個singleton服務參考了一個scope服務,這會導致這個scope服務也變成singleton,仔細看一下注冊DbContext的擴展方法,它實際上提供的是scope服務:

如果發生這種情況,資料庫連接會一直得不到釋放,至于有什么后果大家應該都明白,
所以前面的測驗代碼應該這樣寫:
using (var serviceScope = app.ApplicationServices.CreateScope()) { var context = serviceScope.ServiceProvider.GetService<BloggingContext>(); }
與之相關的還有一個ValidateOnBuild屬性,也就是說在構建IServiceProvider的時候就會做驗證,從原始碼中也能體現出來:
if (options.ValidateOnBuild) { List<Exception> exceptions = null; foreach (var serviceDescriptor in serviceDescriptors) { try { _engine.ValidateService(serviceDescriptor); } catch (Exception e) { exceptions = exceptions ?? new List<Exception>(); exceptions.Add(e); } } if (exceptions != null) { throw new AggregateException("Some services are not able to be constructed", exceptions.ToArray()); } }View Code
正因為如此,Asp.Net Core在設計的時候為每個請求創建獨立的Scope,這個Scope的provider被封裝在HttpContext.RequestServices中,
[小插曲]
通過代碼提示可以看到,IServiceProvider提供了2種獲取service的方式:

這2個有什么區別呢?分別查看各自的方法摘要可以看到,通過GetService獲取一個沒有注冊的服務時會回傳null,而GetRequiredService會拋出一個InvalidOperationException,僅此而已,
// 回傳結果: // A service object of type T or null if there is no such service. public static T GetService<T>(this IServiceProvider provider); // 回傳結果: // A service object of type T. // // 例外: // T:System.InvalidOperationException: // There is no service of type T. public static T GetRequiredService<T>(this IServiceProvider provider);
終極大招
到現在為止,盡管找到了一種看起來合理的方案,但還是不夠優雅,使用過其他第三方DI框架的朋友應該知道,屬性注入的快感無可比擬,那原生DI有沒有實作這個功能呢,我滿心歡喜上G站搜Issue,看到這樣一個回復(https://github.com/aspnet/Extensions/issues/2406):

官方明確表示沒有開發屬性注入的計劃,沒辦法,只能靠自己了,
我的思路大概是:創建一個自定義標簽(Attribute),用來給需要注入的屬性打標簽,然后寫一個服務激活類,用來決議給定實體需要注入的屬性并賦值,在某個型別被創建實體的時候也就是建構式中呼叫這個激活方法實作屬性注入,這里有個核心點要注意的是,從DI容器獲取實體的時候一定要保證是和當前請求是同一個Scope,也就是說,必須要從當前的HttpContext中拿到這個IServiceProvider,
先創建一個自定義標簽:
[AttributeUsage(AttributeTargets.Property)] public class AutowiredAttribute : Attribute { }
決議屬性的方法:
public void PropertyActivate(object service, IServiceProvider provider) { var serviceType = service.GetType(); var properties = serviceType.GetProperties().AsEnumerable().Where(x => x.Name.StartsWith("_")); foreach (PropertyInfo property in properties) { var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>(); if (autowiredAttr != null) { //從DI容器獲取實體 var innerService = provider.GetService(property.PropertyType); if (innerService != null) { //遞回解決服務嵌套問題 PropertyActivate(innerService, provider); //屬性賦值 property.SetValue(service, innerService); } } } }
然后在控制器中激活屬性:
[Autowired] public IAccountService _accountService { get; set; } public LoginController(IHttpContextAccessor httpContextAccessor) { var pro = new AutowiredServiceProvider(); pro.PropertyActivate(this, httpContextAccessor.HttpContext.RequestServices); }
這樣子下來,雖然功能實作了,但是里面存著幾個問題,第一個是由于控制器的建構式中不能直接使用ControllerBase的HttpContext屬性,所以必須要通過注入IHttpContextAccessor物件來獲取,貌似問題又回到原點,第二個是每個建構式中都要寫這么一堆代碼,不能忍,于是想有沒有辦法在控制器被激活的時候做一些操作?沒考慮引入AOP框架,感覺為了這一個功能引入AOP有點重,經過網上搜索,發現Asp.Net Core框架激活控制器是通過IControllerActivator介面實作的,它的默認實作是DefaultControllerActivator(https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/Mvc.Core/src/Controllers/DefaultControllerActivator.cs):
/// <inheritdoc /> public object Create(ControllerContext controllerContext) { if (controllerContext == null) { throw new ArgumentNullException(nameof(controllerContext)); } if (controllerContext.ActionDescriptor == null) { throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull( nameof(ControllerContext.ActionDescriptor), nameof(ControllerContext))); } var controllerTypeInfo = controllerContext.ActionDescriptor.ControllerTypeInfo; if (controllerTypeInfo == null) { throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull( nameof(controllerContext.ActionDescriptor.ControllerTypeInfo), nameof(ControllerContext.ActionDescriptor))); } var serviceProvider = controllerContext.HttpContext.RequestServices; return _typeActivatorCache.CreateInstance<object>(serviceProvider, controllerTypeInfo.AsType()); }View Code
這樣一來,我自己實作一個Controller激活器不就可以接管控制器激活了,于是有如下這個類:
public class HosControllerActivator : IControllerActivator { public object Create(ControllerContext actionContext) { var controllerType = actionContext.ActionDescriptor.ControllerTypeInfo.AsType(); var instance = actionContext.HttpContext.RequestServices.GetRequiredService(controllerType); PropertyActivate(instance, actionContext.HttpContext.RequestServices); return instance; } public virtual void Release(ControllerContext context, object controller) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (controller == null) { throw new ArgumentNullException(nameof(controller)); } if (controller is IDisposable disposable) { disposable.Dispose(); } } private void PropertyActivate(object service, IServiceProvider provider) { var serviceType = service.GetType(); var properties = serviceType.GetProperties().AsEnumerable().Where(x => x.Name.StartsWith("_")); foreach (PropertyInfo property in properties) { var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>(); if (autowiredAttr != null) { //從DI容器獲取實體 var innerService = provider.GetService(property.PropertyType); if (innerService != null) { //遞回解決服務嵌套問題 PropertyActivate(innerService, provider); //屬性賦值 property.SetValue(service, innerService); } } } } }View Code
需要注意的是,DefaultControllerActivator中的控制器實體是從TypeActivatorCache獲取的,而自己的激活器是從DI獲取的,所以必須額外把系統所有控制器注冊到DI中,封裝成如下的擴展方法:
/// <summary> /// 自定義控制器激活,并手動注冊所有控制器 /// </summary> /// <param name="services"></param> /// <param name="obj"></param> public static void AddHosControllers(this IServiceCollection services, object obj) { services.Replace(ServiceDescriptor.Transient<IControllerActivator, HosControllerActivator>()); var assembly = obj.GetType().GetTypeInfo().Assembly; var manager = new ApplicationPartManager(); manager.ApplicationParts.Add(new AssemblyPart(assembly)); manager.FeatureProviders.Add(new ControllerFeatureProvider()); var feature = new ControllerFeature(); manager.PopulateFeature(feature); feature.Controllers.Select(ti => ti.AsType()).ToList().ForEach(t => { services.AddTransient(t); }); }View Code
在ConfigureServices中呼叫:
services.AddHosControllers(this);
到此,大功告成!可以愉快的繼續CRUD了,
結尾
市面上好用的DI框架一堆一堆的,集成到Core里面也很簡單,為啥還要這么折騰?沒辦法,這不就是造輪子的樂趣嘛,上面這些東西從頭到尾也折騰了不少時間,屬性注入那里也還有優化的空間,歡迎探討,
推薦閱讀:
https://www.cnblogs.com/artech/p/inside-asp-net-core-03-05.html
https://www.cnblogs.com/tdfblog/p/controller-activation-and-dependency-injection-in-asp-net-core-mvc.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/93314.html
標籤:.NET Core
上一篇:最近的專案之開篇
