最近使用.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; }
}
}

Dept

 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 }

Emp

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

MyDbContext

  其中,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
};
}
}
}

EfConsoleLoggerFactory

  然后,在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();
}
}
}

Program

  运行之后,打出日志:

  

  通过努力查找,发现这个日志是在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类型,查看ServiceProviderCache的GetOrAdd,其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();
}
}
}

MyDbContext

  我们将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);
}

  

  

.net core中EFCore发出警告:More than twenty 'IServiceProvider' instances have been created for internal use by Entity Framework的更多相关文章

  1. 在ASP.NET Core中如何支持每个租户数据存储策略的数据库

    在ASP.NET Core中如何支持每个租户数据存储策略的数据库 不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译自: ht ...

  2. [翻译]在 .NET Core 中的并发编程

    原文地址:http://www.dotnetcurry.com/dotnet/1360/concurrent-programming-dotnet-core 今天我们购买的每台电脑都有一个多核心的 C ...

  3. .NET Core 中的并发编程

    今天我们购买的每台电脑都有一个多核心的 CPU,允许它并行执行多个指令.操作系统通过将进程调度到不同的内核来发挥这个结构的优点. 然而,还可以通过异步 I/O 操作和并行处理来帮助我们提高单个应用程序 ...

  4. Working with Data » 使用Visual Studio开发ASP.NET Core MVC and Entity Framework Core初学者教程

    原文地址:https://docs.asp.net/en/latest/data/ef-mvc/intro.html The Contoso University sample web applica ...

  5. Entity Framework Core 命名约定

    本文翻译自<Entity Framework Core: Naming Convention>,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 注意:我使用的是 Entity ...

  6. Entity Framework Core(EF Core) 最简单的入门示例

    目录 概述 基于 .NET Core 的 EF Core 入门 创建新项目 更改当前目录 安装 Entity Framework Core 创建模型 创建数据库 使用模型 基于 ASP.NET Cor ...

  7. Entity Framework Core 入门(2)

    安装 EF Core 将 EF Core 添加到不同平台和常用 IDE 中的应用程序的所需步骤汇总. 分步入门教程 无需具备 Entity Framework Core 或任何特定 IDE 的原有知识 ...

  8. 【EF】Entity Framework Core 命名约定

    本文翻译自<Entity Framework Core: Naming Convention>,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 注意:我使用的是 Entity ...

  9. 在Linq to sql 和 Entity framework 中使用lambda表达式实现left join

    在Linq to sql 和 Entity framework 中使用lambda表达式实现left join 我们知道lambda表达式在Linq to sql 和 Entity framework ...

随机推荐

  1. Linux下查看JDK安装路径

    在安装好Git.JDK和jenkins之后,就需要在jenkins中进行对应的设置,比如在全局工具配置模块,需要写入JDK的安装路径. 这篇博客,介绍几种常见的在Linux中查看JDK路径的方法... ...

  2. 出现 CannotAcquireLockException 异常

    项目出现  CannotAcquireLockException异常 原因: 百度了一下,是由于 Spring 事务嵌套造成死锁 结合自己的, handleWithdraw 方法底层有调用 其他 se ...

  3. 【Java 调优】Java性能优化

    Java性能优化的50个细节(珍藏版) 1. 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: ...

  4. 使用匿名内部类和lamda的方式创建线程

    1.匿名内部类的方式 1 /** 2 *匿名内部类的方式启动线程 3 */ 4 public class T2 { 5 public static void main(String[] args) { ...

  5. Mybatis-Plus默认主键策略导致自动生成19位长度主键id的坑

    原创/朱季谦 某天检查一位离职同事写的代码,发现其对应表虽然设置了AUTO_INCREMENT自增,但页面新增功能生成的数据主键id很诡异,长度达到了19位,且不是从1开始递增的-- 我检查了一下,发 ...

  6. 【简】题解 AWSL090429 【原子】

    预处理出每个原子最近的不能合并的位置 枚举当前位置和前面断开的位置合并 发现还是不能过 考虑用选段树优化 但是因为每次转移的最优点是在前面可以合并的范围内 dp值加上当前的到该点的最大值 因为每个位置 ...

  7. Java(运算符)

    运算符 Java语言支持的运算符: 算术运算符:+,-,*,/,%(取余.求余)[模运算],++(自增),--(自减) 赋值运算符:= 关系运算符:>,<,>=(大于等于),< ...

  8. Tableau如何绘制双柱折线组合图

    一.数据准备如下所示 二.将日期拖拽至列,销售额拖拽至行,结果如下所示 三.右键日期排序-选择手动排序 四.将指标拖拽至标记卡上 五.创建计算字段增长率 SUM(IF YEAR(日期)=2017 th ...

  9. Iphone5, 6 and 6Plus尺寸

    1.iPhone5分辨率320x568,像素640x1136,@2x 2.iPhone6分辨率375x667,像素750x1334,@2x 3.iPhone6 Plus分辨率414x736,像素124 ...

  10. 转:Java IO

    转自:http://www.cnblogs.com/rollenholt/archive/2011/09/11/2173787.html [案例1]创建一个新文件 ? 1 2 3 4 5 6 7 8 ...