您好,登錄后才能下訂單哦!
這篇文章主要介紹了從EFCore上下文的使用到深入剖析DI的生命周期最后實(shí)現(xiàn)自動(dòng)屬性注入的示例分析,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
故事背景
最近在把自己的一個(gè)老項(xiàng)目從Framework遷移到.Net Core 3.0,數(shù)據(jù)訪問(wèn)這塊選擇的是EFCore+Mysql。使用EF的話不可避免要和DbContext打交道,在Core中的常規(guī)用法一般是:創(chuàng)建一個(gè)XXXContext類繼承自DbContext,實(shí)現(xiàn)一個(gè)擁有DbContextOptions參數(shù)的構(gòu)造器,在啟動(dòng)類StartUp中的ConfigureServices方法里調(diào)用IServiceCollection的擴(kuò)展方法AddDbContext,把上下文注入到DI容器中,然后在使用的地方通過(guò)構(gòu)造函數(shù)的參數(shù)獲取實(shí)例。OK,沒(méi)任何毛病,官方示例也都是這么來(lái)用的。但是,通過(guò)構(gòu)造函數(shù)這種方式來(lái)獲取上下文實(shí)例其實(shí)很不方便,比如在Attribute或者靜態(tài)類中,又或者是系統(tǒng)啟動(dòng)時(shí)初始化一些數(shù)據(jù),更多的是如下一種場(chǎng)景:
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的類都要寫一個(gè)“多余”的構(gòu)造函數(shù),如果參數(shù)再多幾個(gè),這將是無(wú)法忍受的(就算只有一個(gè)參數(shù)我也忍受不了)。那么怎樣才能更優(yōu)雅的獲取數(shù)據(jù)庫(kù)上下文實(shí)例呢,我想到以下幾種辦法。
DbContext從哪來(lái)
1、 直接開溜new
回歸原始,既然要?jiǎng)?chuàng)建實(shí)例,沒(méi)有比直接new一個(gè)更好的辦法了,在Framework中沒(méi)有DI的時(shí)候也差不多都這么干。但在EFCore中不同的是,DbContext不再提供無(wú)參構(gòu)造函數(shù),取而代之的是必須傳入一個(gè)DbContextOptions類型的參數(shù),這個(gè)參數(shù)通常是做一些上下文選項(xiàng)配置例如使用什么類型數(shù)據(jù)庫(kù)連接字符串是多少。
public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { }
默認(rèn)情況下,我們已經(jīng)在StartUp中注冊(cè)上下文的時(shí)候做了配置,DI容器會(huì)自動(dòng)幫我們把options傳進(jìn)來(lái)。如果要手動(dòng)new一個(gè)上下文,那豈不是每次都要自己傳?不行,這太痛苦了。那有沒(méi)有辦法不傳這個(gè)參數(shù)?肯定也是有的。我們可以去掉有參構(gòu)造函數(shù),然后重寫DbContext中的OnConfiguring方法,在這個(gè)方法中做數(shù)據(jù)庫(kù)配置:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("Filename=./efcoredemo.db"); }
即使是這樣,依然有不夠優(yōu)雅的地方,那就是連接字符串被硬編碼在代碼中,不能做到從配置文件讀取。反正我忍受不了,只能再尋找其他方案。
2、 從DI容器手動(dòng)獲取
既然前面已經(jīng)在啟動(dòng)類中注冊(cè)了上下文,那么從DI容器中獲取實(shí)例肯定是沒(méi)問(wèn)題的。于是我寫了這樣一句測(cè)試代碼用來(lái)驗(yàn)證猜想:
var context = app.ApplicationServices.GetService<BloggingContext>();
不過(guò)很遺憾拋出了異常:
報(bào)錯(cuò)信息說(shuō)的很明確,不能從root provider中獲取這個(gè)服務(wù)。我從G站下載了DI框架的源碼(地址是https://github.com/aspnet/Extensions/tree/master/src/DependencyInjection),拿報(bào)錯(cuò)信息進(jìn)行反向追溯,發(fā)現(xiàn)異常來(lái)自于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())); } }
繼續(xù)往上,看到了GetService方法的實(shí)現(xiàn):
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); }
可以看到,_callback在為空的情況下是不會(huì)做驗(yàn)證的,于是猜想有參數(shù)能對(duì)它進(jìn)行配置。把追溯對(duì)象換成_callback繼續(xù)往上翻,在DI框架的核心類ServiceProvider中找到如下方法:
internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options) { IServiceProviderEngineCallback callback = null; if (options.ValidateScopes) { callback = this; _callSiteValidator = new CallSiteValidator(); } //省略.... }
說(shuō)明我的猜想沒(méi)錯(cuò),驗(yàn)證是受ValidateScopes控制的。這樣來(lái)看,把ValidateScopes設(shè)置成False就可以解決了,這也是網(wǎng)上普遍的解決方案:
.UseDefaultServiceProvider(options => { options.ValidateScopes = false; })
但這樣做是極其危險(xiǎn)的。
為什么危險(xiǎn)?到底什么是root provider?那就要從原生DI的生命周期說(shuō)起。我們知道,DI容器被封裝成一個(gè)IServiceProvider對(duì)象,服務(wù)都是從這里來(lái)獲取。不過(guò)這并不是一個(gè)單一對(duì)象,它是具有層級(jí)結(jié)構(gòu)的,最頂層的即前面提到的root provider,可以理解為僅屬于系統(tǒng)層面的DI控制中心。在Asp.Net Core中,內(nèi)置的DI有3種服務(wù)模式,分別是Singleton、Transient、Scoped,Singleton服務(wù)實(shí)例是保存在root provider中的,所以它才能做到全局單例。相對(duì)應(yīng)的Scoped,是保存在某一個(gè)provider中的,它能保證在這個(gè)provider中是單例的,而Transient服務(wù)則是隨時(shí)需要隨時(shí)創(chuàng)建,用完就丟棄。由此可知,除非是在root provider中獲取一個(gè)單例服務(wù),否則必須要指定一個(gè)服務(wù)范圍(Scope),這個(gè)驗(yàn)證是通過(guò)ServiceProviderOptions的ValidateScopes來(lái)控制的。默認(rèn)情況下,Asp.Net Core框架在創(chuàng)建HostBuilder的時(shí)候會(huì)判定當(dāng)前是否開發(fā)環(huán)境,在開發(fā)環(huán)境下會(huì)開啟這個(gè)驗(yàn)證:
所以前面那種關(guān)閉驗(yàn)證的方式是錯(cuò)誤的。這是因?yàn)椋瑀oot provider只有一個(gè),如果恰好有某個(gè)singleton服務(wù)引用了一個(gè)scope服務(wù),這會(huì)導(dǎo)致這個(gè)scope服務(wù)也變成singleton,仔細(xì)看一下注冊(cè)DbContext的擴(kuò)展方法,它實(shí)際上提供的是scope服務(wù):
如果發(fā)生這種情況,數(shù)據(jù)庫(kù)連接會(huì)一直得不到釋放,至于有什么后果大家應(yīng)該都明白。
所以前面的測(cè)試代碼應(yīng)該這樣寫:
using (var serviceScope = app.ApplicationServices.CreateScope()) { var context = serviceScope.ServiceProvider.GetService<BloggingContext>(); }
與之相關(guān)的還有一個(gè)ValidateOnBuild屬性,也就是說(shuō)在構(gòu)建IServiceProvider的時(shí)候就會(huì)做驗(yàn)證,從源碼中也能體現(xiàn)出來(lái):
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()); } }
正因?yàn)槿绱?,Asp.Net Core在設(shè)計(jì)的時(shí)候?yàn)槊總€(gè)請(qǐng)求創(chuàng)建獨(dú)立的Scope,這個(gè)Scope的provider被封裝在HttpContext.RequestServices中。
[小插曲]
通過(guò)代碼提示可以看到,IServiceProvider提供了2種獲取service的方式:
這2個(gè)有什么區(qū)別呢?分別查看各自的方法摘要可以看到,通過(guò)GetService獲取一個(gè)沒(méi)有注冊(cè)的服務(wù)時(shí)會(huì)返回null,而GetRequiredService會(huì)拋出一個(gè)InvalidOperationException,僅此而已。
// 返回結(jié)果: // A service object of type T or null if there is no such service. public static T GetService<T>(this IServiceProvider provider); // 返回結(jié)果: // A service object of type T. // // 異常: // T:System.InvalidOperationException: // There is no service of type T. public static T GetRequiredService<T>(this IServiceProvider provider);
終極大招
到現(xiàn)在為止,盡管找到了一種看起來(lái)合理的方案,但還是不夠優(yōu)雅,使用過(guò)其他第三方DI框架的朋友應(yīng)該知道,屬性注入的快感無(wú)可比擬。那原生DI有沒(méi)有實(shí)現(xiàn)這個(gè)功能呢,我滿心歡喜上G站搜Issue,看到這樣一個(gè)回復(fù)(https://github.com/aspnet/Extensions/issues/2406):
官方明確表示沒(méi)有開發(fā)屬性注入的計(jì)劃,沒(méi)辦法,只能靠自己了。
我的思路大概是:創(chuàng)建一個(gè)自定義標(biāo)簽(Attribute),用來(lái)給需要注入的屬性打標(biāo)簽,然后寫一個(gè)服務(wù)激活類,用來(lái)解析給定實(shí)例需要注入的屬性并賦值,在某個(gè)類型被創(chuàng)建實(shí)例的時(shí)候也就是構(gòu)造函數(shù)中調(diào)用這個(gè)激活方法實(shí)現(xiàn)屬性注入。這里有個(gè)核心點(diǎn)要注意的是,從DI容器獲取實(shí)例的時(shí)候一定要保證是和當(dāng)前請(qǐng)求是同一個(gè)Scope,也就是說(shuō),必須要從當(dāng)前的HttpContext中拿到這個(gè)IServiceProvider。
先創(chuàng)建一個(gè)自定義標(biāo)簽:
[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容器獲取實(shí)例 var innerService = provider.GetService(property.PropertyType); if (innerService != null) { //遞歸解決服務(wù)嵌套問(wèn)題 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); }
這樣子下來(lái),雖然功能實(shí)現(xiàn)了,但是里面存著幾個(gè)問(wèn)題。第一個(gè)是由于控制器的構(gòu)造函數(shù)中不能直接使用ControllerBase的HttpContext屬性,所以必須要通過(guò)注入IHttpContextAccessor對(duì)象來(lái)獲取,貌似問(wèn)題又回到原點(diǎn)。第二個(gè)是每個(gè)構(gòu)造函數(shù)中都要寫這么一堆代碼,不能忍。于是想有沒(méi)有辦法在控制器被激活的時(shí)候做一些操作?沒(méi)考慮引入AOP框架,感覺為了這一個(gè)功能引入AOP有點(diǎn)重。經(jīng)過(guò)網(wǎng)上搜索,發(fā)現(xiàn)Asp.Net Core框架激活控制器是通過(guò)IControllerActivator接口實(shí)現(xiàn)的,它的默認(rèn)實(shí)現(xiàn)是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()); }
這樣一來(lái),我自己實(shí)現(xiàn)一個(gè)Controller激活器不就可以接管控制器激活了,于是有如下這個(gè)類:
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容器獲取實(shí)例 var innerService = provider.GetService(property.PropertyType); if (innerService != null) { //遞歸解決服務(wù)嵌套問(wèn)題 PropertyActivate(innerService, provider); //屬性賦值 property.SetValue(service, innerService); } } } } }
需要注意的是,DefaultControllerActivator中的控制器實(shí)例是從TypeActivatorCache獲取的,而自己的激活器是從DI獲取的,所以必須額外把系統(tǒng)所有控制器注冊(cè)到DI中,封裝成如下的擴(kuò)展方法:
/// <summary> /// 自定義控制器激活,并手動(dòng)注冊(cè)所有控制器 /// </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); }); }
在ConfigureServices中調(diào)用:
services.AddHosControllers(this);
到此,大功告成!可以愉快的繼續(xù)CRUD了。
感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“從EFCore上下文的使用到深入剖析DI的生命周期最后實(shí)現(xiàn)自動(dòng)屬性注入的示例分析”這篇文章對(duì)大家有幫助,同時(shí)也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識(shí)等著你來(lái)學(xué)習(xí)!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。