DbContext Register and Lifetime

Note: this article is published on 07/28/2024.

Because this article is derived from two serieses of articles, so I will list two base serieses below, if you want to view the base serieses you can eazy to navigate to the locations.

A - Introduction

The content of this article

  • A - Introduction
  • B - Use AddDbContext Extension Method to Register DbContext
  • C - DbContext is Regustered as Scoped Lifetime by Default
    • Verify by Visual Studio Intellisense
    • Verify by AddDbContext Extension Method
    • Verify by Code
  • D - Why Scoped for DbContext
    • Why not Singleton or Transient

B - Use AddDbContext Extension Method to Register DbContext

To regiester DbContext into .Net Core Container, we do not use the methods, such as

  • Service.AddTransient
  • Service.AddSingleton
  • Service.AddScoped

instead, we use AddDbContext Extension Method from EntityFrameworkServiceCollectionExtensions:

this method is more pwerful 

public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddDbContext<TContext> 
(this Microsoft.Extensions.DependencyInjection.IServiceCollection serviceCollection, 
Microsoft.Extensions.DependencyInjection.ServiceLifetime contextLifetime, 
Microsoft.Extensions.DependencyInjection.ServiceLifetime optionsLifetime = 
Microsoft.Extensions.DependencyInjection.ServiceLifetime.Scoped) where TContext : 
Microsoft.EntityFrameworkCore.DbContext;

than other three methods, such as AddSingleton:

public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddSingleton 
(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, 
Type serviceType, 
object implementationInstance);

C - DbContext is Regustered as Scoped Lifetime

The AddDbContext extension method registers DbContext types with a scoped lifetime by default.[ref]

This is safe from concurrent access issues in most ASP.NET Core applications because there is only one thread executing each client request at a given time, and because each request gets a separate dependency injection scope (and therefore a separate DbContext instance). 

  • We can verify the fact by the intelisense of AddDbContext Method from Visual Studio:

  • We can also verify the fact by exaimining the EntityFramworkServiceCollectionExtensions class at [ref]

  • Verify by Code

We use the code combined from these two articles:

i.e., from article top, regester three methods with different lifetimes, such as Singleton, Scoped, and Trensient, while from the second article, regester an entity framework DbContext by AddDbContext method:

The lifetime for the entity framework DbContext is scoped by default, as we discussed previously.

Now, we add two more entity framework DbContexts, as ...Context0 and ...Context2 with explicit named lifetime as Singleton and Transient:

Then we got the results:

Request 1

Request 2

The results:

The DbContext by default is alwarys scoped as light brown color; while Singleton is light green, and Transient is dark brown.

D - Why Scoped for DbContext

The following is mainly from Microsoft [ref].

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. In many web applications, each HTTP request corresponds to a single unit-of-work. This makes tying the context lifetime to that of the request a good default for web applications.

To quote Martin Fowler from the link above, "A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you're done, it figures out everything that needs to be done to alter the database as a result of your work."

A typical unit-of-work when using Entity Framework Core (EF Core) involves:

  • Creation of a DbContext instance
  • Tracking of entity instances by the context. Entities become tracked by
  • Changes are made to the tracked entities as needed to implement the business rule
  • SaveChanges or SaveChangesAsync is called. EF Core detects the changes made and writes them to the database.
  • The DbContext instance is disposed

 Important

  • It is very important to dispose the DbContext after use. This ensures both that any unmanaged resources are freed, and that any events or other hooks are unregistered so as to prevent memory leaks in case the instance remains referenced.
  • DbContext is not thread-safe. Do not share contexts between threads. Make sure to await all async calls before continuing to use the context instance.

Why not Singleton or Transient

This argument is mainly from [ref].

Having a single DbContext for the whole application is a Bad Idea. The only situation where this makes sense is when you have a single-threaded application and a database that is solely used by that single application instance. The DbContext is not thread-safe and since the DbContext caches data, it gets stale pretty soon. This will get you in all sorts of trouble when multiple users/applications work on that database simultaneously which is very common of course. (for more information about why a single DbContext -or even on context per thread- is bad, read this answer).

Registering a DbContext as transient could work, but typically you want to have a single instance of such a unit of work within a certain scope. In a web application, it can be practical to define such a scope on the boundaries of a web request; thus a Per Web Request lifestyle. This allows you to let a whole set of objects operate within the same context. In other words, they operate within the same business transaction.

If you have no goal of having a set of operations operate inside the same context, in that case the transient lifestyle is fine, but there are a few things to watch:

  • Since every object gets its own instance, every class that changes the state of the system, needs to call _context.SaveChanges() (otherwise changes would get lost). This can complicate your code, and adds a second responsibility to the code (the responsibility of controlling the context), and is a violation of the Single Responsibility Principle.
  • You need to make sure that entities [loaded and saved by a DbContext] never leave the scope of such a class, because they can't be used in the context instance of another class. This can complicate your code enormously, because when you need those entities, you need to load them again by id, which could also cause performance problems.
  • Since DbContext implements IDisposable, you probably still want to Dispose all created instances. If you want to do this, you basically have two options. You need to dispose them in the same method right after calling context.SaveChanges(), but in that case the business logic takes ownership of an object it gets passed on from the outside. The second option is to Dispose all created instances on the boundary of the Http Request, but in that case you still need some sort of scoping to let the container know when those instances need to be Disposed.

 

References:


Similar Articles