为什么 GetService 创建的对象没有被销毁?

Why is an object created by GetService not being destructed?

我正在编写一个针对 dotnet 核心框架 3.1 的应用程序。我使用依赖注入来配置数据库上下文等。在我的 Program.cs 中,我有以下代码:

var host = new HostBuilder()
    .ConfigureHostConfiguration(cfgHost =>
    {
        ...
    })
    .ConfigureAppConfiguration((hostContext, configApp) =>
    {
        ....
    })
    .ConfigureServices((hostContext, services) =>
    {
        ...
        services.AddDbContext<MyHomeContext>(options =>
        {
            options.UseNpgsql(hostContext.Configuration.GetConnectionString("DbContext"));
        }, ServiceLifetime.Transient);
        ...
    })
    .ConfigureLogging((hostContext, logging) =>
    {
        ...    
    })
    .Build();

我把host传给另一个class。在另一个 class 中,作为较长方法的一部分,我有以下代码:

    using (var context = Host.Services.GetService(typeof(MyHomeContext)) as MyHomeContext)
    {
        StatusValues = context.Status.ToDictionary(kvp => kvp.Name, kvp => kvp.Id);
    }
    GC.Collect();
    GC.Collect();

GC.Collect 调用用于测试/调查目的。在 MyHomeContext 中,出于测试目的,我实现了析构函数和 Dispose() 的重写。 Dispose() 被调用,但析构函数永远不会被调用。 这会导致我创建的每个 MyHomeContext 实例发生内存泄漏。

我错过了什么?我该怎么做才能确保 MyHomeContext 的实例在我不再需要时被删除。

我转向这个工具是因为以下几个原因:

当我用 new MyHomeContext() 替换 Host.Services.GetService(typeof(MyHomeContext)) as MyHomeContext 时,正在调用 MyHomeContext 的析构函数。在我看来,依赖注入框架中的某些东西持有对该对象的引用。这是真的?如果是这样,我如何告诉它释放它?

很难很好地回答您的问题,因为有很多误解需要解决。以下是一些需要注意的事项:

  • 运行 在调试器中的未优化(调试构建).NET 应用程序的行为与未附加调试器的优化应用程序有很大不同。首先,在调试时,方法的所有变量将始终保持引用状态。这意味着对 GC.Collect() 的任何调用都无法清除由同一方法引用的 context 变量。
  • Dispose Pattern is implemented correctly, a call to the finalizer will be suppressed by the class when its Dispose method is called. This is done by calling GC.SuppressFinalize。 Entity Framework 的 DbContext 正确实现了 Dispose 模式,这也会导致您看不到终结器被命中。
  • 在名为 finalizer thread. This means that even if your context was de-referenced and was eligible for garbage collection, the finalizer is unlikely to be called immediately after the calls to GC.Collect(). You can, however, halt your application and wait until the finalizers are called by calling GC.WaitForPendingFinalizers() 的后台线程上调用终结器(析构函数)。调用 WaitForPendingFinalizers 几乎不是你想在生产中做的事情,但它可以用于测试和基准测试目的。

除了这些 CLR 特定部分之外,这里还有一些关于 DI 部分的反馈:

  • 不应直接处置从 DI 容器解析的服务。相反,由于 DI 容器控制着它的创建,你也应该让它控制它的销毁。
  • 执行此操作的方法(使用 MS.DI)是创建一个 IServiceScope。服务缓存在这样的范围内,当范围被释放时,它会确保其缓存的一次性服务也被释放,并且它会确保以相反的创建顺序完成。
  • 直接从根容器(Host.Services 在您的情况下)请求服务不是一个好主意,因为它会导致作用域服务(例如您的 DbContext)缓存在根容器中。这使他们有效地成为单身人士。换句话说,同一个 DbContext 实例将在应用程序期间重复使用,无论您从 Host.Services 请求它的频率如何。这可能导致 all sorts of hard to debug problems。同样,解决方案是创建一个范围并从该范围解析。例子:
    var factory = Host.Services.GetRequiredService<IServiceScopeFactory>();
    using (var scope = factory.CreateScope())
    {
        var service = scope.ServiceProvider.GetRequiredService<ISomeService>();
        service.DoYourMagic();
    }
    
  • 请注意,使用 ASP.NET Core 时,您通常不必手动创建新范围——每个 Web 请求都会自动获得自己的范围。您的所有 类 都是从范围自动请求的,并且该范围会在网络请求结束时自动清理。