【EF Core】通過 DbContext 選項擴展框架
本來老周計劃在 10 月 1 日或 2 日寫這篇水文的,沒打算出去玩(確實沒啥好玩)。不過因為買的運動相機到手,急著想試試效果,于是就備了些干糧,騎著山地車在外面鬼混了一天。10 月 2 日,家里來了三位熱愛學習的小妹妹,必須傳道授業解惑。10 月 3 日去表弟家里挑一只戰斗力強的貍花貓,負責家里的治安。4、5 日清洗電風扇和一臺有霉味的圓柱空調,順便把家里的門窗都清洗一下。只好等到中秋節才來寫文章。
EF Core 內部使用了 IoC 容器,使其支持依賴注入,理論上也很容易擴展。不過,框架有緩存自己的服務列表,咱們無法直接訪問服務容器。目前階段,EF Core 還不能傳遞咱們自己的 App Services——初始化時它會直接改為 null。
var cacheKey = options; var extension = options.FindExtension<CoreOptionsExtension>(); if (extension?.ApplicationServiceProvider != null) { cacheKey = ((DbContextOptions)options).WithExtension(extension.WithApplicationServiceProvider(null)); }
所以,就算你有本事往 Options 里面塞 App Services 也不起作用,人家直接給干成 null 了。微軟社區團隊表示將來會支持的。
先不要灰心,并不是不能擴展的,還有一個擴展點可以利用—— DbContext 的選項類。
其實,DbContext 選項類是由一組 IDbContextOptionsExtension 服務構成的。所以,咱們如果實現這個服務接口,然后放進選項類的擴展列表中,也能實現擴展 EF Core 的功能。先來認識一下,IDbContextOptionsExtension 接口規定了哪些成員。
1、Info 屬性:返回類型為 DbContextOptionsExtensionInfo。注意各位,這是個抽象類,所以你必須實現自己的 Info,主要用于返回你正在編寫的擴展的相關信息。這個類咱們后面再討論。
2、ApplyDefaults 方法:這個方法有個默認實現,就是 return this。其作用是根據參數傳入的 DbContextOptions(另一個選項類實例),給當前擴展設置一些默認值。這個一般用于:需要根據選項來設置某些參數值,比如,SqlServerOptionsExtension 類。
public virtual IDbContextOptionsExtension ApplyDefaults(IDbContextOptions options) { if (ExecutionStrategyFactory == null && (EngineType == SqlServerEngineType.AzureSql || EngineType == SqlServerEngineType.AzureSynapse || UseRetryingStrategyByDefault)) { return WithExecutionStrategyFactory(c => new SqlServerRetryingExecutionStrategy(c)); } return this; }
3、ApplyServices 方法:重點來了。對就是它,實現它,你就能向服務容器添加自定義的服務了。
4、Validate 方法:驗證一下當前 DbContextOptions 的值是否符合你的要求,如果驗證不通過,直接拋異常就行了。如果不需要驗證,留空即可。
現在咱們再來認識一下 DbContextOptionsExtensionInfo 抽象類。
1、LogFragment 屬性:返回一個字符串,在記錄日志時,這個字符串會出現在日志里。至于說是什么字符串,你可自己決定。
2、Extension 屬性:返回與當前信息類相關的 IDbContextOptionsExtension 對象。
3、ShouldUseSameServiceProvider 方法:這個方法其實是在 DbContextOptions 類的 Equals 方法中作為判斷兩個 DbContextOptions 實例的配置是否相同的條件之一。意思就是如果結果是 true,表明所有配置相同的 DbContextOptions 不需要初始化新的服務容器。
4、GetServiceProviderHashCode 與 PopulateDebugInfo 方法:.NET CLR 對象的相等判斷除了 Equals 方法,還有 GetHashCode 方法,看看是否返回相同的哈希。GetServiceProviderHashCode 方法你可以自定義返回的哈希值,DbContextOptions 類的 GetHashCode 方法中也調用了此方法。
public override int GetHashCode() { var hashCode = new HashCode(); foreach (var (type, value) in _extensionsMap) { hashCode.Add(type); hashCode.Add(value.Extension.Info.GetServiceProviderHashCode()); } return hashCode.ToHashCode(); }
可見,如果 GetServiceProviderHashCode 方法返回的值改變,就會影響到 DbContextOptions 對象的相等判斷。同樣,GetServiceProviderHashCode 方法會和 PopulateDebugInfo 方法搭配用。PopulateDebugInfo 方法的參數是一個字典,開發者可以往里面設置一些自定義的 Key-Value 元素,這些元素通常表示當前擴展中被更改的值。
PopulateDebugInfo 方法中向字典添加的值也會被傳遞給 ServiceProviderDebugInfo 事件,事件相關的數據被封裝到 ServiceProviderDebugInfoEventData 類中。
不過,但是,當你開啟日志功能后,你會發現,ServiceProviderDebugInfo 事件根本不會輸出到日志中。Github 上有人提過這事,但沒人回答。在本文后面,老周會告訴你如何解決此問題。
----------------------------------------------------------------------------------------------------------------------------------------------------
上面是對知識點的簡單理論介紹。說簡單一點,就是你想擴展 EF Core,就是實現 IDbContextOptionsExtension 接口,然后在 ApplyServices 方法中把你的服務放進容器。
理論總是抽象的,咱們動手練一練就好了。
第一步,老規矩,隨便寫個實體,然后從 DbContext 繼承一個你的上下文。
public class Pet { public int Id { get; set; } public string Name { get; set; } = "未知生物"; public int? Age { get; set; } } public class DemoDbContext : DbContext { // 公共屬性:數據集 public DbSet<Pet> Pets { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder opBuilder) { opBuilder.UseSqlite("data source=:memory:") // 開啟日志 .LogTo(log => Console.WriteLine(log)); } }
第二步,既然咱們要擴展,當然要寫服務類型了。
public interface IHelloWorld { void SayHello(string? who); } public class DemoHelloWorld : IHelloWorld { public void SayHello(string? who) { Console.WriteLine("你好,{0}", who ?? "宇宙人"); } }
當然了,通過構造函數的依賴注入,你是可以訪問 EF Core 內部的服務的。這里為了演示的簡單,就沒有注入任何東西。
第三步,重點來了,實現那個,那個很辣眼睛的接口。
public class DemoDbContextOptionsExtension : IDbContextOptionsExtension { // 擴展信息 private MyExtInfo? _info; // 這個屬性用于返回擴展信息。 // 返回的類型是 DbContextOptionsExtensionInfo,不需要訪問 MyExtInfo 類 public DbContextOptionsExtensionInfo Info => _info ??= new MyExtInfo(this); public void ApplyServices(IServiceCollection services) { // 這里,添加你的服務 services.AddScoped<IHelloWorld, DemoHelloWorld>(); } public void Validate(IDbContextOptions options) { // 不需要驗證,空方法 } // 一個隨機整數,模擬選項改變 public int RandValue { get; set; } // 這個類私有化就可以了,因為對外公開的是 DbContextOptionsExtensionInfo 類型 private class MyExtInfo : DbContextOptionsExtensionInfo { // 構造函數 public MyExtInfo(IDbContextOptionsExtension extension) : base(extension) { } // 替換一下基類成員,方便獲取 new public DemoDbContextOptionsExtension Extension => (DemoDbContextOptionsExtension)base.Extension; // 此處要返回 false,因為咱們這個不是數據庫提供者 public override bool IsDatabaseProvider => false; // 自定義日志輸出 public override string LogFragment => $"這是個大擴展 - { Extension.RandValue.ToString()}"; // 使用 _myRandValue 的哈希, public override int GetServiceProviderHashCode() { return Extension.RandValue.GetHashCode(); } public override void PopulateDebugInfo(IDictionary<string, string> debugInfo) { // 設置調試信息 debugInfo["MyExtension:RandomValue"] = Extension.RandValue.ToString(); } public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) { // 可以直接 retrun true return other is MyExtInfo; } } }
第四步,將自定義擴展添加到 DbContextOptions 的擴展集合中有點麻煩,所以一般要封裝一個擴展方法,方便調用。
public static class DemoDbContextOptionsBuilderExtensions { public static DbContextOptionsBuilder UseDemoExt( this DbContextOptionsBuilder builder) { // 在添加前應當查找一下,避免重復添加 var myext = builder.Options.FindExtension<DemoDbContextOptionsExtension>(); // 如果沒有,就new一個 myext ??= new DemoDbContextOptionsExtension(); // 設置一下隨機數據,模擬配置改變 myext.RandValue = Random.Shared.Next(100, 9999999); // 添加到擴展集合中(注意類型轉換) ((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(myext); return builder; } }
a、要養成先找后加的習慣,即先 Find 一下擴展是不是已在集合中,然后再調用 AddOrUpdateExtension 方法添加到擴展集合中。
b、由于此方法是顯式實現了 IDbContextOptionsBuilderInfrastructure 接口,所以要先把 builder 轉換為 IDbContextOptionsBuilderInfrastructure 接口類型再調用 AddOrUpdateExtension 方法。
第五步,回過頭去修改 DemoDbContext 類。
public class DemoDbContext : DbContext { …… protected override void OnConfiguring(DbContextOptionsBuilder opBuilder) { opBuilder.UseSqlite("data source=:memory:") // 開啟日志 .LogTo(log => Console.WriteLine(log)) // 使用自定義的擴展 .UseDemoExt(); } // 測試服務 public void Greeting(string who) { IHelloWorld sv = this.GetService<IHelloWorld>(); sv.SayHello(who); } }
第六步,實例化上下文對象,運行,實驗一下。
static void Main(string[] args) { using var ctx = new DemoDbContext(); ctx.Greeting("小王"); }
運行結果如下圖所示。

很顯然,ServiceProviderDebugInfo 事件沒有日志輸出的。
現在,老周就說一下如何讓它輸出這個事件。
方法:使用 .NET Logging API。比如,咱們要使日志輸出到控制臺,需要添加 Microsoft.Extensions.Logging.Console 包的引用。
在上下文類中,定義 ILoggerFactory 類型的字段,并用 LoggerFactory.Create 方法創建實例。
public class DemoDbContext : DbContext { // 靜態成員 static ILoggerFactory logFac = LoggerFactory.Create(lb => { lb.AddConsole(); lb.SetMinimumLevel(LogLevel.Trace); }); …… protected override void OnConfiguring(DbContextOptionsBuilder opBuilder) { opBuilder.UseSqlite("data source=:memory:") // 開啟日志 //.LogTo(log => Console.WriteLine(log)) .UseLoggerFactory(logFac) // 使用自定義的擴展 .UseDemoExt(); } …… }
SetMinimumLevel 方法將日志級別設置為 Debug 或 Trace。
在 OnConfiguring 方法中,使用 UseLoggerFactory 方法應用 LoggerFactory 對象。
修改之后,重新運行程序。結果如下。

好了,今天就水到這里吧。

浙公網安備 33010602011771號