前言
今天讨論的話題來自一位微信好友遇到問題後請求我的幫助,當然他的意圖并不是本文标題,隻是我将其根本原因進行了一個概括,接下來我們一起來探索标題的問号最終的答案是怎樣的呢?
上下文構造函數是否可以注入執行個體?
老規矩,首先我們定義如下上下文
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沒抛出異常而認為一切正常,瞎貓碰上死耗子,正是恰好碰到注入的執行個體為單例而繞過了異常的出現,是以上下構造函數可以注入執行個體嗎,答案是不一定,若為執行個體池肯定不行,希望通過本文的較長的描述能給需要在上下文構造函數中注入執行個體的童鞋一點力所能及的幫助,探究其問題的本質才能有所成長,感謝您的閱讀。
你所看到的并非事物本身,而是經過诠釋後所賦予的意義