EntityFramework Core 3.x上下文構造函數可以注入實例呢?
- 2020 年 4 月 14 日
- 筆記
- EntityFramework Core 3.x
前言
今天討論的話題來自一位微信好友遇到問題後請求我的幫助,當然他的意圖並不是本文標題,只是我將其根本原因進行了一個概括,接下來我們一起來探索標題的問號最終的答案是怎樣的呢?
上下文構造函數是否可以注入實例?
老規矩,首先我們定義如下上下文
public class EFCoreDbContext : DbContext { public EFCoreDbContext(DbContextOptions<EFCoreDbContext> options) : base(options) { } }
接下來在Web應用程式中如下注入該上下文實例,然後我們就可以開心的玩耍了
services.AddDbContext<EFCoreDbContext>(options => { options.UseSqlServer(@"Server=.;Database=EFCoreTest;Trusted_Connection=True;"); });
問題來了,這位童鞋說,我想要在上述上下文中注入一個實例,當時聽到這種情況還比較驚訝,什麼情況下才會在上下文構造函數中注入實例呢?我們先不關心這個問題,那還不好說,和正常在ASP.NET Core中使用不就完事了么,實踐是檢驗真理的唯一標準,我們來試試,定義如下介面:
public interface IHello { string Say(); } public class Hello : IHello { public string Say() { return "Hello World"; } }
接下來則是注入該介面,如下:
services.AddScoped<IHello, Hello>();
然後就來到上下文構造函數中使用該介面,我們搞個方法來測試下看看,如下:
public class EFCoreDbContext : DbContext { private readonly IHello _hello; public EFCoreDbContext(DbContextOptions<EFCoreDbContext> options, IHello hello) : base(options) { _hello = hello; } public string Print() { return _hello.Say(); } }
最後我們在控制器中使用上下文並調用上述方法,看看是否可行
[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private readonly EFCoreDbContext _context; public WeatherForecastController(EFCoreDbContext context) { _context = context; } [HttpGet] public string Get() { return _context.Print(); } }
呀,沒毛病啊,自我感覺甚是良好,莫慌,這位童鞋說這樣操作沒問題啊,但是我想將上下文注入為實例池的方式,結果卻不行,會拋出異常,到底啥異常啊,如下我們修改成實例池的方式瞧瞧:
services.AddDbContextPool<EFCoreDbContext>(options => { options.UseSqlServer(@"Server=.;Database=EFCoreTest;Trusted_Connection=True;"); });
大意為因為該上下文沒有隻有單個參數是DbContextOptions的構造函數,所以該上下文不能被池化,說明構造函數只能有一個包含DbContextOptions的參數,否則報錯,我們還是看看源碼中到底是如何實例化實例池的呢?
public DbContextPool([NotNull] DbContextOptions options) { _maxSize = options.FindExtension<CoreOptionsExtension>()?.MaxPoolSize ?? DefaultPoolSize; options.Freeze(); _activator = CreateActivator(options); if (_activator == null) { //這裡拋出上述異常資訊 throw new InvalidOperationException( CoreStrings.PoolingContextCtorError(typeof(TContext).ShortDisplayName())); } }
private static Func<TContext> CreateActivator(DbContextOptions options) { var constructors = typeof(TContext).GetTypeInfo().DeclaredConstructors .Where(c => !c.IsStatic && c.IsPublic) .ToArray(); if (constructors.Length == 1) { var parameters = constructors[0].GetParameters(); if (parameters.Length == 1 && (parameters[0].ParameterType == typeof(DbContextOptions) || parameters[0].ParameterType == typeof(DbContextOptions<TContext>))) { return Expression.Lambda<Func<TContext>>( Expression.New(constructors[0], Expression.Constant(options))) .Compile(); } } return null; }
上述對於實例池是通過表達式來構建的實例池,但是在此之前會做一步驗證構造函數參數只能有一個且為DbContextOptions,否則將拋出異常,為何要如此設計呢?我們再來看看在調用上下文實例池到底做了什麼呢?如下我只列舉出關鍵資訊:
public static IServiceCollection AddDbContextPool<TContextService, TContextImplementation>( [NotNull] this IServiceCollection serviceCollection, [NotNull] Action<IServiceProvider, DbContextOptionsBuilder> optionsAction, int poolSize = 128) where TContextImplementation : DbContext, TContextService where TContextService : class { AddCoreServices<TContextImplementation>( serviceCollection, (sp, ob) => { ...... }, ServiceLifetime.Singleton); ...... }
原來在調用實例池時,添加的所以內部服務都是單例,所以我們可以大膽得出結論:在注入上下文實例池時,添加的內部核心服務是單例,而我們注入的實例可能為其他類型,所以EntityFramework Core做了限定,構造函數只能包含DbContextOptions。那麼我們在上下文中怎樣才能使用我們注入的實例呢?其實EntityFramework Core考慮到有這樣的需求,所以給出了對應解決方案,在上下文中存在GetService方法,是不是很熟悉,不過需要我們導入命名空間【Microsoft.EntityFrameworkCore.Infrastructure】,直接在對應方法中獲取注入的實例,這樣就繞過了上下文構造函數,如下:
public string Print() { return this.GetService<IHello>().Say(); }
哎呀,本以為找到了良藥,結果又報錯了,這是為何呢?要是我們將注入的實例修改為單例結果將是好使的,我已經親自驗證過,這裡就不再浪費篇幅,根本原因在哪裡呢?此時我們再來看看上述GetService的實現是怎樣的呢?
public static TService GetService<TService>([CanBeNull] IInfrastructure<IServiceProvider> accessor) { object service = null; if (accessor != null) { var internalServiceProvider = accessor.Instance; service = internalServiceProvider.GetService(typeof(TService)) ?? internalServiceProvider.GetService<IDbContextOptions>() ?.Extensions.OfType<CoreOptionsExtension>().FirstOrDefault() ?.ApplicationServiceProvider ?.GetService(typeof(TService)); if (service == null) { throw new InvalidOperationException( CoreStrings.NoProviderConfiguredFailedToResolveService(typeof(TService).DisplayName())); } } return (TService)service; }
是否有種恍然大悟的感覺,這裡做了判斷,因為在注入上下文實例池時,也注入了核心服務且為單例,但是我們在startup中注入的實例有可能不是單例,比如為scope時,此時會將我們注入的實例通過GetService獲取時作為內部服務,所以會出現無法解析的情況並拋出異常,所以為了解決這個問題,我們必須明確告訴EF Core對於哪些ServiceProvider使用內部服務,除此之外,將通過上述ApplicationServiceProvider來獲取而不包括內部服務,將內部服務和外部服務做一個明確的區分即可,在EntityFramework Core中對於內部服務的註冊,已經通過擴展方法進行了封裝,我們只需手動調用即可,最終解決方案如下:
//手動註冊針對SQL Server的內部服務 services.AddEntityFrameworkSqlServer(); //內部服務使用對應ServiceProvider services.AddDbContextPool<EFCoreDbContext>((serviceProvider, options) => { options.UseInternalServiceProvider(serviceProvider); options.UseSqlServer(@"Server=.;Database=EFCoreTest;Trusted_Connection=True;"); }); services.AddScoped<IHello, Hello>();
總結
本文是以3.x版本演示,對於2.x版本也同樣適用,所以不要認為直接通過GetService沒拋出異常而認為一切正常,瞎貓碰上死耗子,正是恰好碰到注入的實例為單例而繞過了異常的出現,所以上下構造函數可以注入實例嗎,答案是不一定,若為實例池肯定不行,希望通過本文的詳細描述能給需要在上下文構造函數中注入實例的童鞋一點力所能及的幫助,探究其問題的本質才能有所成長,感謝您的閱讀。