Why is an object created by GetService not being destructed?

1.9k views Asked by At

I am writing an application targeting the dotnet core framework 3.1. I use dependency injection to configure, among others, the database context. In my Program.cs I have the following code:

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

I pass host to another class. In that other class I have, as part of a longer method, the following code:

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

The GC.Collect calls are there for testing / investigation purposes. In MyHomeContext I, for testing purposes, implemented a destructor and an override of Dispose(). Dispose() gets called, but the destructor never gets called. This results in a memory leak for every instance of MyHomeContext I create.

What am I missing? What can I do the make sure the the instance of MyHomeContext gets deleted when I no longer need it.

I moved to this implement because of a few reasons:

  • I only need a database connection for a short amount of time.
  • I insert a lot of data (not in the above reduced example / test code), resulting in the DbContext keeping a large cache. I expected disposing the object would free the memory, but now I only made it worse :(

When I replace Host.Services.GetService(typeof(MyHomeContext)) as MyHomeContext by new MyHomeContext() the destructor of MyHomeContext is being called. Seems, to me, that something in the dependency injection framework is holding a reference to the object. Is this true? If so, how can I tell it to release it?

1

There are 1 answers

2
Steven On BEST ANSWER

It's really hard to give a good answer to your question, because there are quite a few misconceptions that need to be addressed. Here are a few pointers for things to look out for:

  • Non-optimized (debug build) .NET applications that run in the debugger behave quite differently from optimized applications with no debugger attached. For one, when debugging, all variables of a method will always stay referenced. This means that any call to GC.Collect() will not be able to clean up the context variable that is referenced by that same method.
  • When the 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's DbContext correctly implements the Dispose Pattern, which would also cause you not to see your finalizer being hit.
  • Finalizers (destructors) are called on a background thread called the 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(). Calling WaitForPendingFinalizers is hardly ever something you want to do in production, but it can be useful for testing and benchmarking purposes.

Apart from these CLR specific parts, here's some feedback on the DI part:

  • Services resolved from the DI Container should not be disposed directly. Instead, since the DI Container is in control over its creation, you should let it be in control over its destruction as well.
  • The way to do this (with MS.DI) is by creating an IServiceScope. Services are cached within such scope and when the scope is disposed of, it will ensure its cached disposable services are disposed of as well, and it will ensure this is done in opposite order of creation.
  • Requesting services directly from the root container (Host.Services in your case) is a bad idea, because it causes scoped services (such as your DbContext) to be cached in the root container. This causes them to effectively become singletons. In other words, the same DbContext instance will be reused for the duration of the application, no matter how often you request it from the Host.Services. This can lead to all sorts of hard to debug problems. The solution is, again, to instead create a scope and resolve from that scope. Example:
    var factory = Host.Services.GetRequiredService<IServiceScopeFactory>();
    using (var scope = factory.CreateScope())
    {
        var service = scope.ServiceProvider.GetRequiredService<ISomeService>();
        service.DoYourMagic();
    }
    
  • Do note that when using ASP.NET Core, you typically don't have to manually create a new scope—every web request will automatically get its own scope. All your classes are automatically requested from a scope and that scope is automatically cleaned up at the end of the web request.