深刻研究EF Core AddDbContext 引發的內存泄露的緣由

前兩天逛園子,看到 @Jeffcky 發的這篇文章《 EntityFramework Core依賴注入上下文方式不一樣形成內存泄漏了解一下》。
一開始只是粗略的掃了一遍沒仔細看,只是以爲是屢次CreateScope後獲取實例形成的DbContext沒法複用。
由於AddDbContext默認的生命週期是Scoped的,每次都建立一個新的Scope並從它的ServiceProvider屬性中獲取的依賴注入實例是不能共享的。
但我來我仔細看了幾遍文章和下面的評論,也在本地建了項目實際測試了,確實如文章所說的那樣。
因而乎,我就來了興趣,就去EF Core的源代碼中找到AddDbContext()的內部實現並把測試項目進行了以下改造:
 
1、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 }

 

2、上一步中引用的DbContextOptionsFactory<T>方法,放到Main方法後面便可html

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;
}

 

3、EFCoreDbContext也作一些更改,不須要重寫OnConfiguring方法,構造方法參數類型改成DbContextOptions<EFCoreDbContext>git

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()

  和github

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();
這句,這行代碼若是放到循環外就不會內存泄露,移到循環內就會內存泄露。
 
到此,咱們找到三個可致使內存泄露的地方
  1. 循環內屢次調用BuildServiceProvider();
  2. services.AddMemoryCache()
  3. 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() 這句移動到循環內/外便可)。api

詳細信息以下圖:緩存

  • 內存泄露時(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; }
}
View Code

 

再次運行項目,截圖以下:app

  • 內存泄露時(BuildServiceProvider語句位於循環內)
第一次

第二次ide

 

 第三次

 第四次測試

 

這不是考眼力看圖找不一樣,就不難爲你們了,我作些標註,醜是醜了點,但能說明問題就好。
圖中打印的信息,由上到下愈來愈具體,那麼反過來就是最下面的標註爲1的(藍色框內)的部分變化引用標註2的總體HashCore變化,再引發3變化,最終引發4生成的緩存key變化。
 
很意外,致使生成的key不一樣的緣由竟然是日誌和內存緩存。也就是說是由每次從ApplicationServiceProvider獲取的日誌和內存緩存對象都不一樣引發的。
而CoreOptionsExtension.ApplicationServiceProvider的值就是在builder.UseApplicationServiceProvider(applicationServiceProvider)時設置給它的,也是咱們獲取EFCoreDbContext實例的那個ServiceProvider。
 
  • 沒有內存泄露時(BuildServiceProvider語句位於循環外)

能夠看到,雖然也有一些變化的地方,但變更的值沒有參與計算key,只有上圖我圈的部分才參與了key生成,因此緩存能夠獲得重用。ui

 


 

到如今,咱們能夠獲得最終的結論,致使內存泄露的緣由是:
在循環內部屢次調用BuildServiceProvider(),致使EF Core內部CoreOptionsExtension.ApplicationServiceProvider在循環時每次取得IMemoryCache和ILoggerFactory的實例都不一樣。
而這兩個對象的HashCode值是參與了EF Core緩存key生成的,因此致使每次生成的key都不同,緩存數據無法獲得複用。
 
爲何屢次調用BuildServiceProvider()會致使每次獲取的IMemoryCache和ILoggerFactory實例會不相同呢?
 
緣由也簡單,IMemoryCache和ILoggerFactory默認註冊的都是Singleton(參考 文檔源碼)。
不是說註冊爲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; }
    }
View Code

 

 一個小問題:生成緩存key中的 397 是什麼?
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)

相關文章
相關標籤/搜索