• 深入研究EF Core AddDbContext 引起的内存泄露的原因


    前两天逛园子,看到 @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();
    这句,这行代码如果放到循环外就不会内存泄露,移到循环内就会内存泄露。
     
    到此,我们找到三个可导致内存泄露的地方
    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() 这句移动到循环内/外即可)。

    详细信息如下图:

    • 内存泄露时(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($"
    打印CoreOptionsExtension的一些HashCode
    " +
                    $"GetServiceProviderHashCode:{x.GetServiceProviderHashCode()} 
    " +
                    $"HashCode:{x.GetHashCode()} 
    " +
                    $"ApplicationServiceProvider HashCode:{x.ApplicationServiceProvider?.GetHashCode()} 
    " +
                    $"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($"
    模拟生成GetServiceProviderHashCode:{hashCode}");
                if (x.GetServiceProviderHashCode() == hashCode)
                {
                    Console.WriteLine($"模拟生成的GetServiceProviderHashCode和GetServiceProviderHashCode()获取的一致");
                }
    
                //打印GetServiceProviderHashCode的生成步骤,对比差异
                Console.WriteLine($"
    影响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

    再次运行项目,截图如下:

    • 内存泄露时(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实例会不相同呢?
     
    原因也简单,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($"
    打印CoreOptionsExtension的一些HashCode
    " +
                        $"GetServiceProviderHashCode:{x.GetServiceProviderHashCode()} 
    " +
                        $"HashCode:{x.GetHashCode()} 
    " +
                        $"ApplicationServiceProvider HashCode:{x.ApplicationServiceProvider?.GetHashCode()} 
    " +
                        $"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($"
    模拟生成GetServiceProviderHashCode:{hashCode}");
                    if (x.GetServiceProviderHashCode() == hashCode)
                    {
                        Console.WriteLine($"模拟生成的GetServiceProviderHashCode和GetServiceProviderHashCode()获取的一致");
                    }
    
                    //打印GetServiceProviderHashCode的生成步骤,对比差异
                    Console.WriteLine($"
    影响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)

  • 相关阅读:
    除去String字符串里面指定的字符串
    JSON总结(java篇)
    关于tomcat文件下载配置
    在多行列表中id同名的<a>标签点击事件处理方法
    java Properties异常:Malformed uxxxx encoding.
    关于An association from the table refers to an unmapped class
    Java实现Mysql数据库自动备份
    Could not publish server configuration for Tomcat v6.0 Server at localhost.
    MySQL备份命令mysqldump参数说明与示例
    图解 | 原来这就是网络
  • 原文地址:https://www.cnblogs.com/weapon/p/9121143.html
Copyright © 2020-2023  润新知