2

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?

jokr
  • 73
  • 1
  • 9
  • That's not how that works. You shouldn't depend on a Destructor to be called. Free your managed and unmanged resources in the [Dispose](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose). – Fildor Apr 21 '20 at 18:33
  • Hi Fildor, I appreciate your attempt to help. I think you did not read the question well enough. The problem is/was that the object was not being destroyed after is was created using GetService. – jokr Apr 23 '20 at 12:27
  • Actually no. I did read it well enough. The accepted answer reiterates in greater detail what I wrote. You just had another issue that contributed to your observed behavior, which is also addressed in the answer but not my comment. I knew there would be another factor, thus I didn't write an answer, but a comment. – Fildor Apr 23 '20 at 12:33

1 Answers1

5

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.
Steven
  • 166,672
  • 24
  • 332
  • 435
  • Hi Steven, I marked you elaborate response as the answer. The part in the code block did resolve it. Some of your remarks at the top do not match with my observations. some feedback: Non-optimized (debug ... : replacing the creation of context by a 'new' implementation, as mentioned in my question, results in the destructor/finalizer being called. When the Dispose Pattern is ... : could be. However with scoped implementation in place (the solution provided by you), the context finalizer is being called. I just implemented it with a Console.WriteLine for testing purposes. – jokr Apr 23 '20 at 12:24
  • Hi @jokr, if you're seeing your finalizer being hit after you called `Dispose()`, you are probably using Entity Framework Core, as it [doesn't suppress finalization](https://github.com/dotnet/efcore/blob/master/src/EFCore/DbContext.cs#L734). Entity Framework 6, on the other hand, [does suppress finalization](https://github.com/dotnet/ef6/blob/master/src/EntityFramework/DbContext.cs#L583). – Steven Apr 23 '20 at 14:00