我的 ASP.NET Core 应用程序(.NET 7、EF Core 7、Npgsql PostgreSQL 7)有问题。
作为示例,我编写了一个简单的 Web 服务来重现该问题:
为什么会发生这种情况?创建范围是唯一正确的解决方案吗?
控制器:
[ApiController]
[Route("[controller]/[action]")]
public class TestController : ControllerBase
{
private IServiceProvider ServiceProvider { get; }
public TestController(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
private const int IterationsCount = 20_000;
[HttpGet]
public async Task<IActionResult> Leak()
{
for (var i = 0; i < IterationsCount; i++)
{
using var context = ServiceProvider.GetRequiredService<TestContext>();
var a = await context.TestEntitys.FirstOrDefaultAsync();
}
GC.Collect();
return Ok("There is leak");
}
[HttpGet]
public async Task<IActionResult> NoLeak()
{
for (var i = 0; i < IterationsCount; i++)
{
using var scope = ServiceProvider.CreateScope();
using var context = scope.ServiceProvider.GetRequiredService<TestContext>();
var a = await context.TestEntitys.FirstOrDefaultAsync();
}
GC.Collect();
return Ok("There is NO leak");
}
}
背景:
public class TestContext : DbContext
{
public TestContext(DbContextOptions<TestContext> options) : base(options)
{
}
public DbSet<TestEntity> TestEntitys { get; set; } = null!;
}
public class TestEntity
{
public int Id { get; set; }
}
程序.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<TestContext>(opts =>
{
var connectionString = builder.Configuration.GetConnectionString(typeof(TestContext).Name);
opts.UseNpgsql(connectionString).UseSnakeCaseNamingConvention();
}, ServiceLifetime.Transient, ServiceLifetime.Singleton);
var app = builder.Build();
var context = app.Services.GetRequiredService<TestContext>();
await context.Database.MigrateAsync();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapControllers();
app.Run();
项目文件:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.15" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
</Project>
您的两个示例将具有完全不同的行为,因为您将 DbContext 的范围设定为 Transient。
在使用公共注入作用域的第一个示例中,如果您有 20,000 次迭代,您将实例化 20,000 个 DbContext 实例,每个实例都跟踪正在加载的实体。在收集请求范围之前,不会收集这些上下文。当您在循环中处置每个 DbContext 实例时,生命周期作用域仍然跟踪对 Transient 实例的引用,因此 GC 仅在作用域处置后才会释放它们。
在第二个示例中,您将在每次迭代中构造和处置内部作用域。这意味着在这种情况下,外部作用域正在跟踪每个内部作用域的引用,但在每次迭代中,都会创建、处置单个 DbContext 实例,并且可以在处置迭代本身内的作用域时收集该实例,从而释放任何引用。
如果切换到使用共享生命周期作用域而不是瞬态作用域,第一个示例的内存使用将更接近第二个示例。不过,行为上会有所不同,因为循环中对同一项目的请求将返回跟踪的实例,除非使用
AsNoTracking
进行查询或在 DbContext 上禁用跟踪。