從EFCore上下文的使用到深入剖析DI的生命周期最后實現(xiàn)自動屬性注入
故事背景
最近在把自己的一個老項目從Framework遷移到.Net Core 3.0,數(shù)據(jù)訪問這塊選擇的是EFCore+Mysql。使用EF的話不可避免要和DbContext打交道,在Core中的常規(guī)用法一般是:創(chuàng)建一個XXXContext類繼承自DbContext,實現(xiàn)一個擁有DbContextOptions參數(shù)的構(gòu)造器,在啟動類StartUp中的ConfigureServices方法里調(diào)用IServiceCollection的擴展方法AddDbContext,把上下文注入到DI容器中,然后在使用的地方通過構(gòu)造函數(shù)的參數(shù)獲取實例。OK,沒任何毛病,官方示例也都是這么來用的。但是,通過構(gòu)造函數(shù)這種方式來獲取上下文實例其實很不方便,比如在Attribute或者靜態(tài)類中,又或者是系統(tǒng)啟動時初始化一些數(shù)據(jù),更多的是如下一種場景:
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òu)造函數(shù),如果參數(shù)再多幾個,這將是無法忍受的(就算只有一個參數(shù)我也忍受不了)。那么怎樣才能更優(yōu)雅的獲取數(shù)據(jù)庫上下文實例呢,我想到以下幾種辦法。
DbContext從哪來
1、 直接開溜new
回歸原始,既然要創(chuàng)建實例,沒有比直接new一個更好的辦法了,在Framework中沒有DI的時候也差不多都這么干。但在EFCore中不同的是,DbContext不再提供無參構(gòu)造函數(shù),取而代之的是必須傳入一個DbContextOptions類型的參數(shù),這個參數(shù)通常是做一些上下文選項配置例如使用什么類型數(shù)據(jù)庫連接字符串是多少。
public BloggingContext(DbContextOptions<BloggingContext> options) : base(options)
{
}
默認(rèn)情況下,我們已經(jīng)在StartUp中注冊上下文的時候做了配置,DI容器會自動幫我們把options傳進來。如果要手動new一個上下文,那豈不是每次都要自己傳?不行,這太痛苦了。那有沒有辦法不傳這個參數(shù)?肯定也是有的。我們可以去掉有參構(gòu)造函數(shù),然后重寫DbContext中的OnConfiguring方法,在這個方法中做數(shù)據(jù)庫配置:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Filename=./efcoredemo.db");
}
即使是這樣,依然有不夠優(yōu)雅的地方,那就是連接字符串被硬編碼在代碼中,不能做到從配置文件讀取。反正我忍受不了,只能再尋找其他方案。
2、 從DI容器手動獲取
既然前面已經(jīng)在啟動類中注冊了上下文,那么從DI容器中獲取實例肯定是沒問題的。于是我寫了這樣一句測試代碼用來驗證猜想:
var context = app.ApplicationServices.GetService<BloggingContext>();
不過很遺憾拋出了異常:

報錯信息說的很明確,不能從root provider中獲取這個服務(wù)。我從G站下載了DI框架的源碼(地址是https://github.com/aspnet/Extensions/tree/master/src/DependencyInjection),拿報錯信息進行反向追溯,發(fā)現(xiàn)異常來自于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方法的實現(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在為空的情況下是不會做驗證的,于是猜想有參數(shù)能對它進行配置。把追溯對象換成_callback繼續(xù)往上翻,在DI框架的核心類ServiceProvider中找到如下方法:
internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)
{
IServiceProviderEngineCallback callback = null;
if (options.ValidateScopes)
{
callback = this;
_callSiteValidator = new CallSiteValidator();
}
//省略....
}
說明我的猜想沒錯,驗證是受ValidateScopes控制的。這樣來看,把ValidateScopes設(shè)置成False就可以解決了,這也是網(wǎng)上普遍的解決方案:
.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = false;
})
但這樣做是極其危險的。
為什么危險?到底什么是root provider?那就要從原生DI的生命周期說起。我們知道,DI容器被封裝成一個IServiceProvider對象,服務(wù)都是從這里來獲取。不過這并不是一個單一對象,它是具有層級結(jié)構(gòu)的,最頂層的即前面提到的root provider,可以理解為僅屬于系統(tǒng)層面的DI控制中心。在Asp.Net Core中,內(nèi)置的DI有3種服務(wù)模式,分別是Singleton、Transient、Scoped,Singleton服務(wù)實例是保存在root provider中的,所以它才能做到全局單例。相對應(yīng)的Scoped,是保存在某一個provider中的,它能保證在這個provider中是單例的,而Transient服務(wù)則是隨時需要隨時創(chuàng)建,用完就丟棄。由此可知,除非是在root provider中獲取一個單例服務(wù),否則必須要指定一個服務(wù)范圍(Scope),這個驗證是通過ServiceProviderOptions的ValidateScopes來控制的。默認(rèn)情況下,Asp.Net Core框架在創(chuàng)建HostBuilder的時候會判定當(dāng)前是否開發(fā)環(huán)境,在開發(fā)環(huán)境下會開啟這個驗證:

所以前面那種關(guān)閉驗證的方式是錯誤的。這是因為,root provider只有一個,如果恰好有某個singleton服務(wù)引用了一個scope服務(wù),這會導(dǎo)致這個scope服務(wù)也變成singleton,仔細(xì)看一下注冊DbContext的擴展方法,它實際上提供的是scope服務(wù):

如果發(fā)生這種情況,數(shù)據(jù)庫連接會一直得不到釋放,至于有什么后果大家應(yīng)該都明白。
所以前面的測試代碼應(yīng)該這樣寫:
using (var serviceScope = app.ApplicationServices.CreateScope())
{
var context = serviceScope.ServiceProvider.GetService<BloggingContext>();
}
與之相關(guān)的還有一個ValidateOnBuild屬性,也就是說在構(gòu)建IServiceProvider的時候就會做驗證,從源碼中也能體現(xiàn)出來:
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());
}
}
正因為如此,Asp.Net Core在設(shè)計的時候為每個請求創(chuàng)建獨立的Scope,這個Scope的provider被封裝在HttpContext.RequestServices中。
[小插曲]
通過代碼提示可以看到,IServiceProvider提供了2種獲取service的方式:

這2個有什么區(qū)別呢?分別查看各自的方法摘要可以看到,通過GetService獲取一個沒有注冊的服務(wù)時會返回null,而GetRequiredService會拋出一個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)在為止,盡管找到了一種看起來合理的方案,但還是不夠優(yōu)雅,使用過其他第三方DI框架的朋友應(yīng)該知道,屬性注入的快感無可比擬。那原生DI有沒有實現(xiàn)這個功能呢,我滿心歡喜上G站搜Issue,看到這樣一個回復(fù)(https://github.com/aspnet/Extensions/issues/2406):

官方明確表示沒有開發(fā)屬性注入的計劃,沒辦法,只能靠自己了。
我的思路大概是:創(chuàng)建一個自定義標(biāo)簽(Attribute),用來給需要注入的屬性打標(biāo)簽,然后寫一個服務(wù)激活類,用來解析給定實例需要注入的屬性并賦值,在某個類型被創(chuàng)建實例的時候也就是構(gòu)造函數(shù)中調(diào)用這個激活方法實現(xiàn)屬性注入。這里有個核心點要注意的是,從DI容器獲取實例的時候一定要保證是和當(dāng)前請求是同一個Scope,也就是說,必須要從當(dāng)前的HttpContext中拿到這個IServiceProvider。
先創(chuàng)建一個自定義標(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容器獲取實例
var innerService = provider.GetService(property.PropertyType);
if (innerService != null)
{
//遞歸解決服務(wù)嵌套問題
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);
}
這樣子下來,雖然功能實現(xiàn)了,但是里面存著幾個問題。第一個是由于控制器的構(gòu)造函數(shù)中不能直接使用ControllerBase的HttpContext屬性,所以必須要通過注入IHttpContextAccessor對象來獲取,貌似問題又回到原點。第二個是每個構(gòu)造函數(shù)中都要寫這么一堆代碼,不能忍。于是想有沒有辦法在控制器被激活的時候做一些操作?沒考慮引入AOP框架,感覺為了這一個功能引入AOP有點重。經(jīng)過網(wǎng)上搜索,發(fā)現(xiàn)Asp.Net Core框架激活控制器是通過IControllerActivator接口實現(xiàn)的,它的默認(rèn)實現(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());
}
這樣一來,我自己實現(xiàn)一個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)
{
//遞歸解決服務(wù)嵌套問題
PropertyActivate(innerService, provider);
//屬性賦值
property.SetValue(service, innerService);
}
}
}
}
}
需要注意的是,DefaultControllerActivator中的控制器實例是從TypeActivatorCache獲取的,而自己的激活器是從DI獲取的,所以必須額外把系統(tǒng)所有控制器注冊到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);
});
}
在ConfigureServices中調(diào)用:
services.AddHosControllers(this);
到此,大功告成!可以愉快的繼續(xù)CRUD了。
結(jié)尾
市面上好用的DI框架一堆一堆的,集成到Core里面也很簡單,為啥還要這么折騰?沒辦法,這不就是造輪子的樂趣嘛。上面這些東西從頭到尾也折騰了不少時間,屬性注入那里也還有優(yōu)化的空間,歡迎探討。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
asp.net得到本機數(shù)據(jù)庫實例的兩種方法代碼
這篇文章介紹了asp.net得到本機數(shù)據(jù)庫實例的兩種方法代碼,有需要的朋友可以參考一下2013-07-07
asp.net導(dǎo)出excel數(shù)據(jù)的常見方法匯總
這篇文章主要介紹了asp.net導(dǎo)出excel數(shù)據(jù)的常見方法,實例匯總了數(shù)據(jù)導(dǎo)出的常見的方法以及相關(guān)的注意事項,是非常實用的技巧,需要的朋友可以參考下2014-10-10
.NET Core3.0創(chuàng)建Worker Services的實現(xiàn)
這篇文章主要介紹了.NET Core3.0創(chuàng)建Worker Services的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
教你30分鐘通過Kong實現(xiàn).NET網(wǎng)關(guān)
Kong是一個Openrestry程序,而Openrestry運行在Nginx上,用Lua擴展了nginx。所以可以認(rèn)為Kong = Openrestry + nginx + lua,這篇文章主要介紹了30分鐘通過Kong實現(xiàn).NET網(wǎng)關(guān),需要的朋友可以參考下2021-11-11
asp.net datalist綁定數(shù)據(jù)后可以上移下移實現(xiàn)示例
這篇文章主要介紹了asp.net datalist綁定數(shù)據(jù)后可以上移下移的示例代碼,需要的朋友可以參考下2014-02-02

