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中使用不就完事了么,實踐是檢驗真理的唯一標準,我們來試試,定義如下接口:
接下來則是注入該接口,如下:
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>))){returnExpression.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,?TContextServicewhere?TContextService?:?class {AddCoreServices<TContextImplementation>(serviceCollection,(sp,?ob)?=>{......},ServiceLifetime.Singleton);......????}原來在調用實例池時,添加的所以內部服務都是單例,所以我們可以大膽得出結論:在注入上下文實例池時,添加的內部核心服務是單例,而我們注入的實例可能為其他類型,所以EntityFramework Core做了限定,構造函數只能包含DbContextOptions。那么我們在上下文中怎樣才能使用我們注入的實例呢?其實EntityFramework Core考慮到有這樣的需求,所以給出了對應解決方案,在上下文中存在GetService方法,是不是很熟悉,不過需要我們手動導入命名空間,直接在對應方法中獲取注入的實例,這樣就繞過了上下文構造函數,如下:
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沒拋出異常而認為一切正常,瞎貓碰上死耗子,正是恰好碰到注入的實例為單例而繞過了異常的出現,所以上下構造函數可以注入實例嗎,答案是不一定,若為實例池肯定不行,希望通過本文的詳細描述能給需要在上下文構造函數中注入實例的童鞋一點力所能及的幫助,探究其問題的本質才能有所成長,感謝您的閱讀。?
總結
以上是生活随笔為你收集整理的EntityFramework Core 3.x上下文构造函数可以注入实例呢?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 想基于K8s按需扩展应用程序,可从这几方
- 下一篇: IO 模型知多少