zoukankan      html  css  js  c++  java
  • 深入研究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)

  • 相关阅读:
    AtCoder Beginner Contest 167
    AtCoder Beginner Contest 166
    AtCoder Beginner Contest 165
    AtCoder Beginner Contest 164
    AtCoder Beginner Contest 163
    AtCoder Beginner Contest 162
    AtCoder Beginner Contest 161
    AtCoder Beginner Contest 160
    AtCoder Beginner Contest 159
    自定义Mybatis自动生成代码规则
  • 原文地址:https://www.cnblogs.com/weapon/p/9121143.html
Copyright © 2011-2022 走看看