2

I'm using EF Core together with Postgres (probably doesn't matter) inside an .NET Core 3.1 console application.

The program is using a shared project (among other components of the solution) with all business logic implemented using a simple CQRS type pattern with Mediator.

At one place I'm retrieving large objects from the database (10 - 100MB) in size. This is not very frequent so by itself is not an issue. It takes a fraction of a second on modern hardware. The problem is that for some reason those objects get cached in the datacontext between command executions, like the datacontext doesn't get disposed.

I don't understand why, because I registered the DbContext inside the DI container (the standard built in one) as transient. How I understand it it should create a new instance every time it's requested and the garbage collector should take care of the rest.

The registration code is something like this:

static IServiceProvider ConfigureServiceProvider()
{
    IServiceCollection services = new ServiceCollection();
    
    DbContextOptions<MyAppDbContext> dbContextOptions = new DbContextOptionsBuilder<MyAppDbContext>()
       .UseNpgsql(Configuration.GetConnectionString("MyApp.Db"))
       .Options;
    services.AddSingleton(dbContextOptions);
    services.AddDbContext<MyAppDbContext>(options => options.UseNpgsql(Configuration.GetConnectionString("MyApp.Db"), options => options.EnableRetryOnFailure()), ServiceLifetime.Transient);
    services.AddTransient<IMyAppDbContext>(s => s.GetService<MyAppDbContext>());

    // (...)
}

Then the command is using it in this way:

public class RecalculateSomething : IRequest
{
    public Guid SomeId { get; set; }

    public class Handler : IRequestHandler<RecalculateSomething>
    {
        private IMyAppDbContext context;
        private readonly IMediator mediator;

        public Handler(IMyAppDbContext context, IMediator mediator)
        {
            this.context = context ?? throw new ArgumentNullException(nameof(context));
            this.mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
        }
        
        public async Task<Unit> Handle(RecalculateSomething request, CancellationToken ct)
        {       
            // (...)
        }
    }
}

Does anyone know what the problem is? Is it something I'm doing wrong configuring the DI container? Or something else, like a reference I'm holding onto somewhere (couldn't find it). What would be the best way to approach debugging an issue like this?

BTW I have "fixed it" by just forcing it to create a new DbContext each time from DbContextOptions, but that's more of a workaround. Would like to know what the core issue is.

kowgli
  • 101
  • 6
  • 1
    Does the `dbContextOptions` has a `Dispose()?` If so, you should implement the IDisposable also on your `Handler`, so it disposes the context. – Jeroen van Langen Feb 20 '21 at 17:56

1 Answers1

3

You almost never want to register a DbContext. It is better to register a factory, which you use to create a new DbContext on every request. While that seems inefficient, that pattern has been optimized over the years, and allows you to deterministically reclaim resources on each request.

From the documents

The lifetime of a DbContext begins when the instance is created and ends when the instance is disposed. A DbContext instance is designed to be used for a single unit-of-work. This means that the lifetime of a DbContext instance is usually very short.

The document goes on to explain that you can inject it, which you are doing, but the problem is this lacks deterministic reclamation of resources. It's better to keep this in your power using a factory, which is explained later in the same document

Some application types (e.g. ASP.NET Core Blazor) use dependency injection but do not create a service scope that aligns with the desired DbContext lifetime. Even where such an alignment does exist, the application may need to perform multiple units-of-work within this scope. For example, multiple units-of-work within a single HTTP request.

In these cases, AddDbContextFactory can be used to register a factory for creation of DbContext instances.

I'm willing to bet that it's the combination of this non-determinism, the large size of the queries, and the frequency of the queries that is causing some memory pressure in your application.

You can see if adding the factory (AddDbContextFactory ) and using it to create a context helps; again, refer to the same section referenced above in the document for the actual code.

If you're wondering why a DbContext that is transient is non-deterministic, you'd probably need to dive into the .NET Core codebase to see when transient resources are reclaimed. That's probably not right after you exit your handler, but somewhere after rolling back up the (potentially deep) call stack.

(If that's the case think of it this way -- many calls trying to complete as many are unwinding their stack; this transient situation is pressure that can build over some period).

Your DI is not wrong per se, but it does not seem optimal for what you're trying to accomplish and the context in which you're code is operating.

Kit
  • 20,354
  • 4
  • 60
  • 103
  • That seems like a lot of complexity when you could just new up a DbContext yourself. – Robert Harvey Feb 20 '21 at 18:27
  • 2
    @RobertHarvey The complexity of a factory per request vs. a `new` per request are roughly equivalent and the results are the same: resources you can reclaim within your own power. The factory gives a few advantages if there's more to DI. Though the `new` is certainly more obvious. DI may give you some testing advantages as well. Tradeoffs! – Kit Feb 20 '21 at 18:31