• .net core中EFCore发出警告:More than twenty 'IServiceProvider' instances have been created for internal use by Entity Framework


     

     

      最近使用.net core k开发时,碰到个问题,Ef使用中程序发出了一个警告:

      More than twenty 'IServiceProvider' instances have been created for internal use by Entity Framework. This is commonly caused by injection of a new singleton service instance into every DbContext instance. For example, calling UseLoggerFactory passing in a new instance each time--see https://go.microsoft.com/fwlink/?linkid=869049 for more details. Consider reviewing calls on 'DbContextOptionsBuilder' that may require new service providers to be built.

       这个警告是说,我们创建了超过20个的IServiceProvider用于EF的内部使用,提醒我们程序是不是出现了问题,让我们查看DbContextOptionsBuilder是不是有问题。

      本来这只是个警告,一般来说没什么问题,在好奇心的驱使下,又是百度,又是谷歌,又是MSDN,又是GitHub看源码,发现这个好像是.net ef core的一个BUG,如果置之不管,时间久了可能导致内存溢出,而且目前确认有人因为这个导致内存溢出了。

      现在,我们来重现这个警告:

      首先,我们创建一个控制台程序,当然也可以是api项目或者web mvc项目,然后在Nuget中安装以下包(博主使用的.net core 2.2的版本,mysql数据库):

      Microsoft.EntityFrameworkCore(EF框架包)

      Microsoft.Extensions.Logging.Console(控制台日志输出)

      Pomelo.EntityFrameworkCore.MySql(mysql数据库连接)

      Microsoft.NETCore.App(这个应该是默认会带的,没有就加上)

      

      安装完成之后,需要创建一个数据库,随便建立一两个表,这里建两个表,对应实体如下:

    复制代码
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace DemoConsole2
    {
        public class Dept
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string Description { get; set; }
        }
    }
    复制代码
    复制代码
     1 using System;
     2 using System.Collections.Generic;
     3 using System.Text;
     4 
     5 namespace DemoConsole2
     6 {
     7     public class Emp
     8     {
     9         public int Id { get; set; }
    10         public string Name { get; set; }
    11         public int Age { get; set; }
    12         public DateTime? HireDate { get; set; }
    13         public int DeptId { get; set; }
    14     }
    15 }
    复制代码

      DbContext如下:  

    复制代码
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Diagnostics;
    using Microsoft.Extensions.Logging.Console;
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace DemoConsole2
    {
        public class MyDbContext : DbContext
        {
            public DbSet<Emp> Emps { get; set; }
            public DbSet<Dept> Depts { get; set; }
    
            public MyDbContext(DbContextOptions options) : base(options)
            {
    
            }
    
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseLoggerFactory(new EfConsoleLoggerFactory());
                base.OnConfiguring(optionsBuilder);
            }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                var empBuilder = modelBuilder.Entity<Emp>();
                empBuilder.ToTable(typeof(Emp).Name);
                empBuilder.HasKey(nameof(Emp.Id));
                empBuilder.Property(nameof(Emp.Id)).ValueGeneratedOnAdd();
    
    
                var deptBuilder = modelBuilder.Entity<Dept>();
                deptBuilder.ToTable(typeof(Dept).Name);
                deptBuilder.HasKey(nameof(Dept.Id));
                deptBuilder.Property(nameof(Dept.Id)).ValueGeneratedOnAdd();
            }
        }
    }
    复制代码

      其中,EfConsoleLoggerFactory是用来输出控制台日志用的:  

    复制代码
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Logging.Console;
    using Microsoft.Extensions.Options;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace DemoConsole2
    {
        public class EfConsoleLoggerFactory : LoggerFactory
        {
            public EfConsoleLoggerFactory() : base(GetLoggerProviders(), GetFilterOptions())
            {
            }
    
            public static ILoggerProvider[] GetLoggerProviders()
            {
                ServiceCollection service = new ServiceCollection();
                service.Configure<ConsoleLoggerOptions>(options => { });
                var serviceProvider = service.BuildServiceProvider();
                ConsoleLoggerProvider consoleLoggerProvider = new ConsoleLoggerProvider(serviceProvider.GetService<IOptionsMonitor<ConsoleLoggerOptions>>());
                return new ILoggerProvider[] { consoleLoggerProvider };
            }
            public static LoggerFilterOptions GetFilterOptions()
            {
                return new LoggerFilterOptions()
                {
                    MinLevel = LogLevel.Warning
                };
            }
        }
    }
    复制代码

      然后,在Program中就可以测试了:  

    复制代码
    using Microsoft.EntityFrameworkCore;
    using System;
    
    namespace DemoConsole2
    {
        class Program
        {
            static void Main(string[] args)
            {
                var builder = new DbContextOptionsBuilder<MyDbContext>();
                builder.UseMySql("server=192.168.209.128;port=3306;database=test;uid=root;pwd=123456;CharSet=utf8");
                for (var i = 0; i < 20; i++)
                {
                    using (var context = new MyDbContext(builder.Options))
                    {
                        context.SaveChanges();
                    }
                }
    
                Console.ReadKey();
            }
        }
    }
    复制代码

      运行之后,打出日志:

      

      通过努力查找,发现这个日志是在ServiceProviderCache类的GetOrAdd方法中打印出来的,GitHub源码地址:https://github.com/aspnet/EntityFrameworkCore/blob/release/2.2/src/EFCore/Internal/ServiceProviderCache.cs#L115

      这里截个图:

      

       箭头处就是打印日志的地方,但是它是要缓存个数大于等于20时才大于,所以,上面我们在Program中的循环才使用了20次,少于20次都不会有这个警告。

      通过查看ServiceProviderCache,我们发现_configurations是一个ConcurrentDictionary,但不是静态的,但是ServiceProviderCache有一个静态实例,想必EFcore里面都是使用这个静态实例,这样一来,_configurations和静态就没什么区别了

      _configurations的key是long类型,查看ServiceProviderCacheGetOrAdd,其key值规则如下:

      

       其中options是一个IDbContextOptions对象,它有一个Extensions属性,而key的值就是由这些Extensions属性决定的,而Extensions属性中,一般都有一个重要的Extension:CoreOptionsExtension

      这个CoreOptionsExtension就厉害了,像服务,日志等等都是在它里面配置

      回到我们的测试项目,再看看我们的DbContext,在OnConfiguring方法中:

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseLoggerFactory(new EfConsoleLoggerFactory());
                base.OnConfiguring(optionsBuilder);
            }

      这里,我们每次实例化一个EfConsoleLoggerFactory对象,使用DbContextOptionsBuilder的UseLoggerFactory方法加载,可以去看看UseLoggerFactory的源码,它其实是将ILoggerFactory对象放到CoreOptionsExtension的LoggerFactory中,这样就导致了CoreOptionsExtension对象的变化,上面说了,ServiceProviderCache_configurations的key值依赖于IDbContextOptions的Extensions,而CoreOptionsExtension就在这些Extensions中,这样就自然引起了key的变化,这样,_configurations就会重新缓存一个对象,而OnConfiguring方法在DbContext每次实例化后,在使用这个新的DbContext时都会调用(如上面的Program的context.SaveChanges()就会去调用OnConfiguring),这样就会不停地增加_configurations中缓存的数量,直至内存溢出。

      知道问题的原因,现在,我们修改一下MyDbContext类:

    复制代码
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Diagnostics;
    using Microsoft.Extensions.Logging.Console;
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace DemoConsole2
    {
        public class MyDbContext : DbContext
        {
            public DbSet<Emp> Emps { get; set; }
            public DbSet<Dept> Depts { get; set; }
    
            public MyDbContext(DbContextOptions options) : base(options)
            {
    
            }
    
            static readonly EfConsoleLoggerFactory loggerFactory = new EfConsoleLoggerFactory();
    
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseLoggerFactory(loggerFactory);
                base.OnConfiguring(optionsBuilder);
            }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                var empBuilder = modelBuilder.Entity<Emp>();
                empBuilder.ToTable(typeof(Emp).Name);
                empBuilder.HasKey(nameof(Emp.Id));
                empBuilder.Property(nameof(Emp.Id)).ValueGeneratedOnAdd();
    
    
                var deptBuilder = modelBuilder.Entity<Dept>();
                deptBuilder.ToTable(typeof(Dept).Name);
                deptBuilder.HasKey(nameof(Dept.Id));
                deptBuilder.Property(nameof(Dept.Id)).ValueGeneratedOnAdd();
            }
        }
    }
    复制代码

      我们将EfConsoleLoggerFactory放到一个静态字段中,这样可以保证每次调用optionsBuilder.UseLoggerFactory(loggerFactory)方法时使用的是同一个对象,从而不会引起_configurations的key的变化,然后运行发现确实没有这个警告出现了。

      通过调试,可以看到ServiceProviderCache的静态属性Instance中,_configurations中的数量确实没有增加。

      需要提一下的是,上面的例子是通过LoggerFactory的变化来说明这个问题产生的原因,事实上,基本所有引起IDbContextOptions的Extensions变化都会导致_configurations重新缓存,所以以后再使用DbContextOptionsBuilder就要小心了,毕竟正常来说,我们都是通过DbContextOptionsBuilder去修改DbContext的配置,从而影响到EFcore内部的一些配置。

      这里提示一下,一般的,我们会使用到DbContextOptionsBuilder的地方有3个:

      (1)、DbContext的OnConfiguring方法,

      (2)、IServiceCollection的拓展方法AddDbContext(方法实现在EntityFrameworkServiceCollectionExtensions中)

      (3)、在使用new 实例化DbContext对象时,如上面Program中的DbContextOptionsBuilder<T>就是DbContextOptionsBuilder的一个子类

      当然还有其它地方或者方式可以使用更新DbContextOptionsBuilder对象,如果找不到了怎么办呢?

      经过自己的验证,发现3个可行的方法:

      (1)、在DbContext的OnConfiguring中使用反射,当ServiceProviderCache.Instance对象中_configuration缓存个数大于指定数量时,就干掉一部分  

    复制代码
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                var field = typeof(ServiceProviderCache).GetField("_configurations", BindingFlags.NonPublic | BindingFlags.Instance);
                var configurations = (ConcurrentDictionary<long, (IServiceProvider ServiceProvider, IDictionary<string, string> DebugInfo)>)field.GetValue(ServiceProviderCache.Instance);
                if (configurations.Count > 10)
                {
                    configurations.Clear();
                }
    
                optionsBuilder.UseLoggerFactory(new EfConsoleLoggerFactory());
                base.OnConfiguring(optionsBuilder);
            }
    复制代码

      (2)、使用内部服务

        查看ServiceProviderCache的GetOrAdd的源码发现,如果存在内部服务,则不会使用缓存,而内部服务是使用DbContextOptionsBuilder对象添加进去的,上面提到使用DbContextOptionsBuilder的三个地方都可以使用,如:

    复制代码
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                ServiceCollection services = new ServiceCollection();
                services.AddEntityFrameworkMySql();
                services.AddLogging();
                services.AddTransient<ILoggerFactory, EfConsoleLoggerFactory>();
                var internalServiceProvider = services.BuildServiceProvider();
    
                optionsBuilder.UseApplicationServiceProvider(internalServiceProvider);
                //optionsBuilder.UseLoggerFactory(internalServiceProvider.GetService<ILoggerFactory>());
                base.OnConfiguring(optionsBuilder);
            }
    复制代码

      需要注意的是,如果使用内部服务,那么这个服务必须包含其所需的依赖服务对象,比如上面的EfConsoleLoggerFactory,只需要将它将它注入到服务中,而不用调用UseApplicationServiceProvider方法,如果调用,将会抛出异常

      (3)、升级.net core到3.0

       .net core 3.0中CoreOptionsExtension提供了一个ServiceProviderCachingEnabled属性,用于设置是否启用缓存,这个算是暂时解决了内存溢出的问题:  

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.EnableServiceProviderCaching(false);
                optionsBuilder.UseLoggerFactory(new EfConsoleLoggerFactory());
                base.OnConfiguring(optionsBuilder);
            }

      

      

     
     
     
  • 相关阅读:
    ubuntu14.4开启ftp服务
    ubuntu14.4安装gtx970显卡驱动的艰辛历程
    jquery.dataTables的用法
    win7上安装theano keras深度学习框架
    使用BeanUtils设置属性转换String到Date类型
    keras在win7下环境搭建
    Python-try except else finally有return时执行顺序探究
    MySQL-EXPLAIN用法详解
    PHP-Windows下搭建Nginx+PHP环境
    PHP-php.ini中文版
  • 原文地址:https://www.cnblogs.com/jiangyunfeng/p/12572529.html
Copyright © 2020-2023  润新知