前兩天逛園子,看到 @Jeffcky 發的這篇文章《
EntityFramework Core依賴注入上下文方式不同造成內存泄漏了解一下》。
一開始只是粗略的掃了一遍沒仔細看,只是覺得是多次CreateScope后獲取實例造成的DbContext無法復用。
因為AddDbContext默認的生命周期是Scoped的,每次都創建一個新的Scope並從它的ServiceProvider屬性中獲取的依賴注入實例是不能共享的。
但我來我仔細看了幾遍文章和下面的評論,也在本地建了項目實際測試了,確實如文章所說的那樣。
於是乎,我就來了興趣,就去EF Core的源代碼中找到AddDbContext()的內部實現並把測試項目進行了如下改造:
一、Main方法內部:
1 var services = new ServiceCollection(); 2 //方式一 AddDbContext注冊方式,會引起內存泄露 3 //services.AddDbContext<EFCoreDbContext>(options => options.UseSqlServer("connectionString")); 4 5 //方式二 使用AddScoped模擬AddDbContext注冊方式,new EFCoreDbContext()時參數由DI提供,會引起內存泄露 6 services.AddMemoryCache(); // 手動高亮點1 7 8 Action<DbContextOptionsBuilder> optionsAction = o => o.UseSqlServer("connectionString"); 9 Action<IServiceProvider, DbContextOptionsBuilder> optionsAction2 = (sp, o) => optionsAction.Invoke(o); 10 11 services.TryAdd(new ServiceDescriptor(typeof(DbContextOptions<EFCoreDbContext>), 12 p => DbContextOptionsFactory<EFCoreDbContext>(p, optionsAction2), 13 ServiceLifetime.Scoped)); 14 services.Add(new ServiceDescriptor(typeof(DbContextOptions), 15 p => p.GetRequiredService<DbContextOptions<EFCoreDbContext>>(), 16 ServiceLifetime.Scoped)); 17 18 services.AddScoped(s => new EFCoreDbContext(s.GetRequiredService<DbContextOptions<EFCoreDbContext>>())); 19 20 //方式三 直接使用AddScoped,new EFCoreDbContext()時參數自己提供。不會引起內存泄露 21 //var options = new DbContextOptionsBuilder<EFCoreDbContext>() 22 // .UseSqlServer("connectionString") 23 // .Options; 24 //services.AddScoped(s => new EFCoreDbContext(options)); 25 26 //為了排除干擾,去掉靜態ServiceLocator 27 //ServiceLocator.Init(services); 28 //for (int i = 0; i < 1000; i++) 29 //{ 30 // var test = new TestUserCase(); 31 // test.InvokeMethod(); 32 //} 33 34 //去掉靜態ServiceLocator后的代碼 35 var rootServiceProvider = services.BuildServiceProvider(); // 這一句放在循環外就可避免內存泄露,挪到循環內就會內存泄露 36 for (int i = 0; i < 1000; i++) 37 { 38 using (var test = new TestUserCase(rootServiceProvider)) 39 { 40 test.InvokeMethod(); 41 } 42 }
二、上一步中引用的DbContextOptionsFactory<T>方法,放到Main方法后面即可
private static DbContextOptions<TContext> DbContextOptionsFactory<TContext>(IServiceProvider applicationServiceProvider, Action<IServiceProvider, DbContextOptionsBuilder> optionsAction) where TContext : DbContext { var builder = new DbContextOptionsBuilder<TContext>( new DbContextOptions<TContext>(new Dictionary<Type, IDbContextOptionsExtension>())); builder.UseApplicationServiceProvider(applicationServiceProvider); // 手動高亮點2 optionsAction?.Invoke(applicationServiceProvider, builder); return builder.Options; }
三、EFCoreDbContext也做一些更改,不需要重寫OnConfiguring方法,構造方法參數類型改為DbContextOptions<EFCoreDbContext>
public class EFCoreDbContext : DbContext { public EFCoreDbContext(DbContextOptions<EFCoreDbContext> options) : base(options) { } public DbSet<TestA> TestA { get; set; } }
經過上面幾步改造以后,不使用AddDbContext()也可重現使用AddDbContext()時的內存泄露。
我們來對比一下用AddDbContext和AddScoped(這里的AddScoped指的是原先的AddScoped方式,並非我們改造過的AddScoped)有什么不同。可以很容易的找到兩個可疑的地方:
services.AddMemoryCache()
和
builder.UseApplicationServiceProvider(applicationServiceProvider);
也就是我在上面代碼中我添加了 //手動高亮 字樣的那兩行代碼。
跟據命名我們大致可以猜到這兩行代碼的作用,用於內存中緩存和將當前使用的ServiceProvider設置為ApplicationServiceProvider(該Application不是指的整個應用程序,而是EF Core Application)。
經測試,這兩行代碼去掉任意一行都不會引起內存泄露。
而 UseApplicationServiceProvider 是EF Core2.0才引入的(見官方
API文檔),
這也印證了 @geek_power 在文章下面留言中說的“這個問題只在EF Core2.0中才有”。
他的原話是“我測試過,Asp.net core並沒有這個問題,EF6.x和EF core1.0也沒這個問題,只有.net core console + EF core2.0會出現內存泄露。
經過測試是Microsoft.Extensions.DependencyInjection1.0升級到Microsoft.Extensions.DependencyInjection2.0造成的,只在console出現。”
這句話中的Asp.net core沒有這個問題是有誤導的,經測試,這個問題在ASP.NET Core中照樣是有的,只不過平時大家在使用ASP.NET Core使用DI時一般都是直接獲取IServiceProvider的實例,而不會直接用到ServiceCollection,更不會循環多次調用BuildServiceProvider。
就算在ASP.NET Core內部使用了ServiceCollection,一般也是用戶自己新創建的,和Startup.ConfigureServices(IServiceCollection services)內的services沒有關系。
而EF Core的注冊到一般也是注冊到services的,所以用戶自己創建的ServiceCollection也就和EF Core扯不上關系,更不會引起EF Core內存泄露了。
關於,ASP.NET Core中復現內存泄露,我后面會給出測試代碼。
另外,為了排除干擾,我把原測試中的在靜態中傳遞ServiceCollection或ServiceProvider的ServiceLocator去掉,改為在new TestUserCase()直接傳參。
因為微軟在官方給出的使用依賴注入的建議其中有兩項就是: 避免靜態訪問服務 應用程序代碼中避免服務位置(ServiceLocator) 文檔地址:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1
改動后的代碼上面已經給出,但有一句要特別注意一下,就是
var rootServiceProvider = services.BuildServiceProvider();
這句,這行代碼如果放到循環外就不會內存泄露,移到循環內就會內存泄露。
到此,我們找到三個可導致內存泄露的地方
- 循環內多次調用BuildServiceProvider();
- services.AddMemoryCache()
- builder.UseApplicationServiceProvider(applicationServiceProvider);
這三個項,只要其它任何一項不滿足,都不會出現內存泄露。換句話說就是,這三個條件必須全部滿足才會導致內存泄露。
那么我們可以得到一個初步的猜想。
內存泄露是由內存緩存引起的,緩存使用的key與當前使用的ServiceProvider有關,而多次調用BuildServiceProvider()后生成的ServiceProvider又不同的,從而導致一直在添加新的緩存而從來沒有從緩存中獲取過。
要怎么證實呢?
於是我做了以下操作:
首先,我排出了所有資源沒釋放的原因,對所有創建的對象進行了顯示的資源回收,沒任何效果(這些步驟可有可無,不影響測試結果)。
然后,排除了數據庫連接沒有關閉的原因,使用SQL Server Profiler查看數據庫連接情況,每次都是正常關閉的。
再然后,使用jetbrains dotMemory查看內存占用,發現增加的內存的確都是緩存數據並且得到一個重要的線索,Microsoft.EntityFrameworkCore.Internal.ServiceProviderCache.
先前在研究UseApplicationServiceProvider的時,閱讀EF Core的源代碼見過這貨。
其中有這樣一段代碼
//EF Core內部生成緩存key的代碼 //代碼位置:Microsoft.EntityFrameworkCore.Internal.ServiceProviderCache //所在方法:IServiceProvider GetOrAdd(IDbContextOptions options, bool providerRequired) var key = options.Extensions .OrderBy(e => e.GetType().Name) .Aggregate(0L, (t, e) => (t * 397) ^ ((long)e.GetType().GetHashCode() * 397) ^ e.GetServiceProviderHashCode());
可以看到方法簽名的其中一個參數是IDbContextOptions類型,而且key也是用它計算的。
那是不是我們可以在自己的代碼中模擬生成一個key呢?
於是我在EFCoreDbContext的構造方法內添加了如下代碼
var key = options.Extensions .OrderBy(e => e.GetType().Name) .Aggregate(0L, (t, e) => (t * 397) ^ ((long)e.GetType().GetHashCode() * 397) ^ e.GetServiceProviderHashCode()); Console.WriteLine($"EF Core當前DbContextOptions實例生成的緩存key為:{key}");
果然,得到的結果是:內存泄露時每次打印到的key值都不一樣,沒有內存泄露時打印出來的都一樣(測試代碼快速切換內存泄露/沒有內存泄露方法,將前面提到的 var rootServiceProvider = services.BuildServiceProvider() 這句移動到循環內/外即可)。
詳細信息如下圖:
- 內存泄露時(BuildServiceProvider語句位於循環內)
第一次


第二次



第三次


- 沒有內存泄露時(BuildServiceProvider語句位於循環外)






不過,這只是一個最終的key計算方案,並不能看到具體是那里不同導致的生成的key值不一樣的。
所以繼續改造代碼,一步步的跟蹤這個key生成的每一步,並打印出來。細節就不一一表述了,直接給出完整EFCoreDbContext代碼:

public class EFCoreDbContext : DbContext { public EFCoreDbContext(DbContextOptions<EFCoreDbContext> options) : base(options) { //模擬生成EF Core 緩存key var key = options.Extensions .OrderBy(e => e.GetType().Name) .Aggregate(0L, (t, e) => (t * 397) ^ ((long)e.GetType().GetHashCode() * 397) ^ e.GetServiceProviderHashCode()); Console.WriteLine($"EF Core當前DbContextOptions實例生成的緩存key為:{key}"); //打印一下影響生成緩存key值的對象名、HashCore、自定義的ServiceProviderHashCode var oExtensions = options.Extensions.OrderBy(e => e.GetType().Name); Console.WriteLine($"打印引起key變化的IDbContextOptionsExtension實例列表"); foreach (var item in oExtensions) { Console.WriteLine($"item name:{item.GetType().Name} HashCode:{item.GetType().GetHashCode()} ServiceProviderHashCore:{item.GetServiceProviderHashCode()}"); } //從上一步打印結果來看,oExtensions內包含兩個對象,SqlServerOptionsExtension和CoreOptionsExtension //SqlServerOptionsExtension的HashCode和ServiceProviderHashCode每次都一樣,不是變化因素,不再跟蹤 //CoreOptionsExtension 用來表示由EF Core 管理的選項,而不是由數據庫提供商或擴展管理的選項。 //前面提到過的 builder.UseApplicationServiceProvider(applicationServiceProvider); //就是把當前使用的 ServiceProvider 賦值到 CoreOptionsExtension .ApplicationServiceProvider var coreOptionsExtension = options.FindExtension<CoreOptionsExtension>(); if (coreOptionsExtension != null) { var x = coreOptionsExtension; Console.WriteLine($"\n打印CoreOptionsExtension的一些HashCode\n" + $"GetServiceProviderHashCode:{x.GetServiceProviderHashCode()} \n" + $"HashCode:{x.GetHashCode()} \n" + $"ApplicationServiceProvider HashCode:{x.ApplicationServiceProvider?.GetHashCode()} \n" + $"InternalServiceProvider HashCode:{x.InternalServiceProvider?.GetHashCode()}"); //模擬GetServiceProviderHashCode的生成過程 var memoryCache = x.MemoryCache ?? x.ApplicationServiceProvider?.GetService<IMemoryCache>(); var loggerFactory = x.LoggerFactory ?? x.ApplicationServiceProvider?.GetService<ILoggerFactory>(); var isSensitiveDataLoggingEnabled = x.IsSensitiveDataLoggingEnabled; var warningsConfiguration = x.WarningsConfiguration; var hashCode = loggerFactory?.GetHashCode() ?? 0L; hashCode = (hashCode * 397) ^ (memoryCache?.GetHashCode() ?? 0L); hashCode = (hashCode * 397) ^ isSensitiveDataLoggingEnabled.GetHashCode(); hashCode = (hashCode * 397) ^ warningsConfiguration.GetServiceProviderHashCode(); if (x.ReplacedServices != null) { hashCode = x.ReplacedServices.Aggregate(hashCode, (t, e) => (t * 397) ^ e.Value.GetHashCode()); } Console.WriteLine($"\n模擬生成GetServiceProviderHashCode:{hashCode}"); if (x.GetServiceProviderHashCode() == hashCode) { Console.WriteLine($"模擬生成的GetServiceProviderHashCode和GetServiceProviderHashCode()獲取的一致"); } //打印GetServiceProviderHashCode的生成步驟,對比差異 Console.WriteLine($"\n影響GetServiceProviderHashCode值的因素"); Console.WriteLine($"loggerFactory:{loggerFactory?.GetHashCode() ?? 0L}"); Console.WriteLine($"memoryCache:{memoryCache?.GetHashCode() ?? 0L}"); Console.WriteLine($"isSensitiveDataLoggingEnabled:{isSensitiveDataLoggingEnabled.GetHashCode()}"); Console.WriteLine($"warningsConfiguration:{warningsConfiguration.GetServiceProviderHashCode()}"); } } public DbSet<TestA> TestA { get; set; } }
再次運行項目,截圖如下:
- 內存泄露時(BuildServiceProvider語句位於循環內)
第一次
第二次
第三次

第四次
這不是考眼力看圖找不同,就不難為大家了,我做些標注,丑是丑了點,但能說明問題就好。
圖中打印的信息,由上到下越來越具體,那么反過來就是最下面的標注為1的(藍色框內)的部分變化引用標注2的整體HashCore變化,再引起3變化,最終引起4生成的緩存key變化。
很意外,導致生成的key不同的原因居然是日志和內存緩存。也就是說是由每次從ApplicationServiceProvider獲取的日志和內存緩存對象都不同引起的。
而CoreOptionsExtension.ApplicationServiceProvider的值就是在builder.UseApplicationServiceProvider(applicationServiceProvider)時設置給它的,也是我們獲取EFCoreDbContext實例的那個ServiceProvider。
- 沒有內存泄露時(BuildServiceProvider語句位於循環外)

可以看到,雖然也有一些變化的地方,但變動的值沒有參與計算key,只有上圖我圈的部分才參與了key生成,所以緩存可以得到重用。
到現在,我們可以得到最終的結論,導致內存泄露的原因是:
在循環內部多次調用BuildServiceProvider(),導致EF Core內部CoreOptionsExtension.ApplicationServiceProvider在循環時每次取得IMemoryCache和ILoggerFactory的實例都不同。
而這兩個對象的HashCode值是參與了EF Core緩存key生成的,所以導致每次生成的key都不一樣,緩存數據沒法得到復用。
為什么多次調用BuildServiceProvider()會導致每次獲取的IMemoryCache和ILoggerFactory實例會不相同呢?
不是說注冊為Singleton的類型在任何地方取出來都是同一個實例了,它也是有前提條件的,那就是:
只有在同一個Root ServiceProvider下取得的實例才是唯一的。
如果多次調用BuildServiceProvider()創建了多個Root ServiceProvider,那么從不同的Root ServiceProvider中取得的實例是不同的。
這也可解釋了另外一個問題,“好像這一切都只發生在控制台應用程序中,ASP.NET Core不管怎么玩都沒有問題”。
經過測試,在ASP.NET Core中這樣寫也會有問題的。
只是因為在ASP.NET Core中我們一般很少直接用到IServiceCollection,大多數時候都是直接通過構造方法注入IServiceProvider的,更不會多次調用services.BuildServiceProvider();
並且默認情況下也只有在Startup.ConfigureServices(IServiceCollection services)才會用到它,而我們幾乎不會把除注冊服務外的其他代碼寫到這的。。
關於@geek_power 《
Microsoft.Extensions.DependencyInjection不同版本導致EF出現內存泄露》提到的問題,我表示懷疑。
他文章中方案二提到:在EF6 + Microsoft.Extensions.DependencyInjection1.0 或 EF Core1.0 + Microsoft.Extensions.DependencyInjection1.0 中即使只調用了一次BuildServiceProvider()也會出現內存泄露。
而且我使用 EF Core1.0 + Microsoft.Extensions.DependencyInjection1.0 實際測試過,沒發現有任何問題。EF6 + Microsoft.Extensions.DependencyInjection1.0就沒測了。
完整的測試代碼

class Program { static void Main(string[] args) { var services = new ServiceCollection(); services.AddLogging(); //test 1 var options = new DbContextOptionsBuilder<EFCoreDbContext>() .UseSqlServer(Config.connectionString) .Options; ////test 2 模擬 AddDbContext //services.AddMemoryCache(/*c=> { c.ExpirationScanFrequency = new TimeSpan(0,0,5);c.CompactionPercentage = 1;c.SizeLimit = 20000; }*/); //Action<DbContextOptionsBuilder> optionsAction = o => o.UseSqlServer(Config.connectionString); //Action<IServiceProvider, DbContextOptionsBuilder> optionsAction2 = (sp, o) => optionsAction.Invoke(o); //services.TryAdd(new ServiceDescriptor(typeof(DbContextOptions<EFCoreDbContext>), p => //{ // Console.WriteLine($"正在從ServiceProvider[{p.GetHashCode().ToString()}]中獲取/創建DbContextOptions<EFCoreDbContext>實例"); // //Console.ReadKey(); // return DbContextOptionsFactory<EFCoreDbContext>(p, optionsAction2); //}, ServiceLifetime.Scoped)); //services.Add(new ServiceDescriptor(typeof(DbContextOptions), p => p.GetRequiredService<DbContextOptions<EFCoreDbContext>>(), ServiceLifetime.Scoped)); //這兩個注冊方式二選一, 使用第一行表示啟用test 1, 使用第二行表示啟用test 2 //services.AddScoped(s => new EFCoreDbContext(options)); //services.AddScoped(s => new EFCoreDbContext(s.GetRequiredService<DbContextOptions<EFCoreDbContext>>())); services.AddScoped<IMemoryCacheTest, MemoryCacheTest>(); services.AddDbContext<EFCoreDbContext>((p, o) => { Console.WriteLine($"UseInternalServiceProvider[{p.GetHashCode().ToString()}]"); o.UseSqlServer(Config.connectionString); }); //services.AddEntityFrameworkSqlServer().AddDbContext<EFCoreDbContext>((p,o)=> { // Console.WriteLine($"UseInternalServiceProvider[{p.GetHashCode().ToString()}]"); // o.UseSqlServer(Config.connectionString).UseInternalServiceProvider(p); }); ILogger log; var rootServiceProvider = services.BuildServiceProvider(); for (int i = 0; i < 10; i++) { Console.WriteLine($"rootServiceProvider[{rootServiceProvider.GetHashCode().ToString()}]"); //log = rootServiceProvider.GetService<ILoggerFactory>().AddConsole().CreateLogger<Program>(); //log.LogInformation("日志輸出正常"); using (var test = new TestUserCase(rootServiceProvider)) { test.InvokeMethod(); } } //rootServiceProvider.Dispose(); //rootServiceProvider = null; Console.WriteLine("執行完畢,請按任意鍵繼續..."); Console.ReadKey(); } private static DbContextOptions<TContext> DbContextOptionsFactory<TContext>(IServiceProvider applicationServiceProvider, Action<IServiceProvider, DbContextOptionsBuilder> optionsAction) where TContext : DbContext { var builder = new DbContextOptionsBuilder<TContext>( new DbContextOptions<TContext>(new Dictionary<Type, IDbContextOptionsExtension>())); Console.WriteLine($"將ServiceProvider[{applicationServiceProvider.GetHashCode().ToString()}]設置為ApplicationServiceProvider"); //Console.ReadKey(); builder.UseApplicationServiceProvider(applicationServiceProvider); optionsAction?.Invoke(applicationServiceProvider, builder); return builder.Options; } } //調試時使用查看一下當前系統內的緩存狀態 public interface IMemoryCacheTest { void Test(); } public class MemoryCacheTest : IMemoryCacheTest { private IMemoryCache _cache; public MemoryCacheTest(IMemoryCache memoryCache) { _cache = memoryCache; } public void Test() { var x = _cache.GetType(); } } public class TestUserCase : IDisposable { //private IServiceCollection services; private IServiceScope serviceScope; private IServiceProvider _serviceProvider; private EFCoreDbContext _context; public TestUserCase(/*IServiceCollection services,*/IServiceProvider serviceProvider) { //this.services = services; _serviceProvider = serviceProvider; } public void InvokeMethod() { //_serviceProvider = services.BuildServiceProvider(); using (serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { var internalServiceProvider = serviceScope.ServiceProvider; Console.WriteLine($"獲取一個新的ServiceProvider[{internalServiceProvider.GetHashCode().ToString()}]"); Console.WriteLine($"獲取一個新的ServiceProviderType[{internalServiceProvider.GetType().GetHashCode().ToString()}]"); var memoryCache = internalServiceProvider?.GetService<IMemoryCache>(); var loggerFactory = internalServiceProvider?.GetService<ILoggerFactory>(); Console.WriteLine($"當前ServiceProvider.GetService<IMemoryCache>():{memoryCache.GetHashCode().ToString()}"); Console.WriteLine($"當前ServiceProvider.GetService<ILoggerFactory>():{loggerFactory.GetHashCode().ToString()}"); //using (_context = internalServiceProvider.GetRequiredService<EFCoreDbContext>()) //{ _context = internalServiceProvider.GetRequiredService<EFCoreDbContext>(); Printf(_serviceProvider, internalServiceProvider, _context, serviceScope); //} var cacheTest = _serviceProvider.GetRequiredService<IMemoryCacheTest>(); cacheTest.Test(); //(internalServiceProvider as IDisposable)?.Dispose(); //internalServiceProvider = null; } } public void Printf(IServiceProvider sp, IServiceProvider _serviceProvider, EFCoreDbContext _context, IServiceScope _serviceScope) { for (var i = 0; i < 100; i++) { var testA = _context.TestA.AsNoTracking().FirstOrDefault(); //_context.TestA.Add(new TestA() { Name = "test"}); //_context.SaveChanges(); Console.WriteLine($"RootSP:{sp.GetHashCode()} CurrentSP:{_serviceProvider.GetHashCode()} DbContext:{_context?.GetHashCode()} Index:{i}"); } } private bool disposed = false; public void Dispose() { Console.WriteLine($"{this.GetType().Name}.Dispose()..."); Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!this.disposed) { // Note disposing has been done. disposed = true; //serviceScope?.Dispose(); //serviceScope = null; //// ////(_serviceProvider as IDisposable)?.Dispose(); ////_serviceProvider = null; //_context?.Dispose(); //_context = null; } } } public class EFCoreDbContext : DbContext { public EFCoreDbContext(DbContextOptions<EFCoreDbContext> options) : base(options) { //模擬生成EF Core 緩存key var key = options.Extensions .OrderBy(e => e.GetType().Name) .Aggregate(0L, (t, e) => (t * 397) ^ ((long)e.GetType().GetHashCode() * 397) ^ e.GetServiceProviderHashCode()); Console.WriteLine($"EF Core當前DbContextOptions實例生成的緩存key為:{key}"); //打印影響生成緩存key值的對象名、HashCore、自定義的ServiceProviderHashCode var oExtensions = options.Extensions.OrderBy(e => e.GetType().Name); Console.WriteLine($"打印引起key變化的IDbContextOptionsExtension實例列表"); foreach (var item in oExtensions) { Console.WriteLine($"item name:{item.GetType().Name} HashCode:{item.GetType().GetHashCode()} ServiceProviderHashCore:{item.GetServiceProviderHashCode()}"); } //從上一步打印結果來看,oExtensions內包含兩個對象,SqlServerOptionsExtension和CoreOptionsExtension //SqlServerOptionsExtension的HashCode和ServiceProviderHashCode每次都一樣,不是變化因素,不再跟蹤 //CoreOptionsExtension 用來表示由EF Core 管理的選項,而不是由數據庫提供商或擴展管理的選項。 //上面的代碼中 builder.UseApplicationServiceProvider(applicationServiceProvider); 這句就是把當前 ServiceProvider 設置到該類型實例的 ApplicationServiceProvider 屬性 var coreOptionsExtension = options.FindExtension<CoreOptionsExtension>(); if (coreOptionsExtension != null) { var x = coreOptionsExtension; Console.WriteLine($"\n打印CoreOptionsExtension的一些HashCode\n" + $"GetServiceProviderHashCode:{x.GetServiceProviderHashCode()} \n" + $"HashCode:{x.GetHashCode()} \n" + $"ApplicationServiceProvider HashCode:{x.ApplicationServiceProvider?.GetHashCode()} \n" + $"InternalServiceProvider HashCode:{x.InternalServiceProvider?.GetHashCode()}"); //模擬GetServiceProviderHashCode的生成過程 var memoryCache = x.MemoryCache ?? x.ApplicationServiceProvider?.GetService<IMemoryCache>(); var loggerFactory = x.LoggerFactory ?? x.ApplicationServiceProvider?.GetService<ILoggerFactory>(); var isSensitiveDataLoggingEnabled = x.IsSensitiveDataLoggingEnabled; var warningsConfiguration = x.WarningsConfiguration; var hashCode = loggerFactory?.GetHashCode() ?? 0L; hashCode = (hashCode * 397) ^ (memoryCache?.GetHashCode() ?? 0L); hashCode = (hashCode * 397) ^ isSensitiveDataLoggingEnabled.GetHashCode(); hashCode = (hashCode * 397) ^ warningsConfiguration.GetServiceProviderHashCode(); if (x.ReplacedServices != null) { hashCode = x.ReplacedServices.Aggregate(hashCode, (t, e) => (t * 397) ^ e.Value.GetHashCode()); } Console.WriteLine($"\n模擬生成GetServiceProviderHashCode:{hashCode}"); if (x.GetServiceProviderHashCode() == hashCode) { Console.WriteLine($"模擬生成的GetServiceProviderHashCode和GetServiceProviderHashCode()獲取的一致"); } //打印GetServiceProviderHashCode的生成步驟,對比差異 Console.WriteLine($"\n影響GetServiceProviderHashCode值的因素"); Console.WriteLine($"loggerFactory:{loggerFactory?.GetHashCode() ?? 0L}"); Console.WriteLine($"memoryCache:{memoryCache?.GetHashCode() ?? 0L}"); Console.WriteLine($"isSensitiveDataLoggingEnabled:{isSensitiveDataLoggingEnabled.GetHashCode()}"); Console.WriteLine($"warningsConfiguration:{warningsConfiguration.GetServiceProviderHashCode()}"); } } public DbSet<TestA> TestA { get; set; } } public class TestA { public long Id { get; set; } public string Name { get; set; } }
stackoverflow上有人提過這個問題,大該意思是它是一個“恰到好處”的素數,夠用也不至於太大,使用素數的原因是因為這樣生成的HashCode重復率低。
https://stackoverflow.com/questions/102742/why-is-397-used-for-resharper-gethashcode-override
補充:
EF Core內部對這種錯誤的使用方法是有警告提示的,大家看我測試代碼開啟了控制台日志打印,是因為我看到這ServiceProviderCache內有有這幾行代碼,我想打印出來看看提示內容是什么。

循環20次以上就可看到這樣的提示。
為英文不好的同學獻上google翻譯(google真TM機智,把microsoft.com翻譯成google.com)