Best Practices in Using the DbContext in EF Core

October 19th, 2022

Entity Framework Core is an open-source, popular, lightweight, and flexible cross-platform ORM. In Entity Framework Core (also called EF Core), a Db context is an object that coordinates queries, updates, and deletes against your database. It takes care of performing CRUD operations against your database.

The DbContext, the central object in Entity Framework Core, is a gateway to your database. It provides an abstraction layer between the domain model and EF Core. This abstraction helps you keep your models independent from EF Core and lets you easily switch persistence providers if needed.

This article talks about Db Context concepts and the best practices you can adhere to for improving the performance and scalability of your applications built using ASP.NET 6. We’ll use a PostgreSQL database using Devart for PostgreSQL to store and retrieve data.

Getting Started

First off, we need to have a database against which the queries will be executed. For the sake of simplicity, instead of creating our own database, we will use the Northwind database. If you don’t have the Northwind database available, you can get the script(s) from here: https://github.com/Microsoft/sql-server-samples/tree/master/samples/databases/northwind-pubs.

Next, create a new .NET Core Console Application project and include the Devart.Data.Postgresql NuGet Package onto it. It can be installed either from the NuGet Package Manager tool within Visual Studio or from the NuGet Package Manager console by using the following command:

PM> Install-Package Devart.Data.PostgreSql

Next, create a custom DbContext class named MyDbContext in a file having the same name with a “.cs” extension and replace the default generated code with the code listing given below:

public class MyDbContext : DbContext
{
    public MyDbContext()
    {
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    }
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Order> Orders { get; set; }
}

We’ll use this class to perform CRUD operations in the subsequent sections of this article.

What is DbContext? Why do we need it?

The concept of a DbContext has been around since the first version of the Entity Framework was released. It is one of the fundamental building blocks in the Entity Framework and Entity Framework Core that are used for creating, querying, and updating data from a database.

A DbContext represents a collection of entity sets or tables in your database. It allows us to write strongly-typed code against our database without having to deal with low-level SQL statements or having to worry about manually mapping classes to database tables.

The DbContext is not just another object. It’s one of the core objects in Entity Framework Core, and it has a lot of responsibilities to manage. You’ll use it for things like:

  • Accessing the database
  • Managing connections to the database
  • Managing transactions with the database (including setting up new ones, rolling back existing ones, or even canceling them)
  • Change tracking
  • Reading and persisting data.
  • Querying for entity types using LINQ syntax

There Should be Only one DbContext Instance per Request/Unit of Work/Transaction

The DbContext is a singleton class that represents the gateway to all data access, and therefore should not be instantiated more than once. If you need multiple database connections or have multiple active contexts operating in parallel, then use the DbContextFactory class instead.

Avoid Disposing DbContext Instances

It would help if you did not dispose of DbContext objects in most cases. Although the DbContext implements IDisposable, it should not be manually disposed of or wrapped in a using statement. DbContext controls its lifespan; after your data access request is complete, DbContext will automatically close the database connection for you.

Don’t use Dependency Injection for your DbContext Instances

It would help if you did not use Dependency Injection for your DbContext instance in domain model objects. This rule aims to keep the code clean and DRY and make it easier to test. Hence, inject the actual instance of the class that has a dependency if you want to use dependency injection. So, instead of injecting an instance of DbContext, you should inject an instance of type IUnitOfWork or another class that contains other dependencies that the specified object type can use.

Injecting a DBContext directly into your business logic classes will make the methods of your business logic classes hard to unit test as they would require a connection string and possibly some configuration settings etc., to work. So, avoid code such as this in your controllers or business logic classes:

public class DemoController : ControllerBase
{
    private MyCustomDbContext _dbContext;
    public DemoController(MyCustomDbContext customDbContext)
    {
       _dbContext = customDbContext;
    }
//Other methods
}

Keep Your Domain Objects Ignorant of the Persistence Mechanism

Persistence ignorance may be defined as the ability to persist and retrieve standard .NET objects without knowing the intricacies related to how the data is stored and retrieved in the data store. Your domain model objects should not be aware of the persistence mechanism, i.e., how the data is persisted in the underlying data store.

In other words, if your entities are persistence ignorant, they shouldn’t bother about how they’re persisted, created, retrieved, updated, or deleted. This would help you to focus more on modeling your business domain.

You can refer to the Customer or Order class given later in this article. Both of these classes are POCO (an acronym for Plain Old CLR Objects) classes.

The persistence mechanism logic should be encapsulated inside the DbContext. A persistent ignorant class is one that doesn’t have any knowledge of how the data is persisted in a data store. Only the data access layer in your application should have access to the DbContext. Additionally, only the DbContext should be allowed to access the database directly. That said, you should use persistence ignore with caution. Here’s what the Microsoft documentation says:

“Even when it is important to follow the Persistence Ignorance principle for your Domain model, you should not ignore persistence concerns. It is still very important to understand the physical data model and how it maps to your entity object model. Otherwise, you can create impossible designs”.

Split a large DbContext to Multiple DbContext Instances

Since the DB context represents your database, you may wonder whether the application should only have one DB context. In reality, this practice is not at all acceptable. It has been observed that if you have a large Db Context, EF Core will take more time to get started, i.e., the startup time of EF Core will be significantly more. Therefore, rather than using an extensive database context, break it into many smaller ones. Then, you can have each module or unit have one Db Context.

Disable Lazy Loading and Use Eager Loading for Improved Performance

Entity Framework Core uses any of the following three approaches to load related entities in your application.

  • Eager loading – in this case, related data is loaded at the time when the query is executed using the Include() method.
  • Explicit loading – in this case, related data is loaded explicitly at a later point in time using the Load() method.
  • Lazy loading – this is the default phenomenon used meant for the delayed loading of related entities.

You should turn off lazy loading to improve performance using the LazyLoadingEnabled property as shown in the code snippet given below:

ChangeTracker.LazyLoadingEnabled = false;

Instead, you should use eager loading to load your required entities at once. The following code snippet illustrates how you can use eager loading using your custom DbContext instance.

using (MyDbContext dbContext = new DbContext())
{
   var result = dbContext.Customers.Include("Orders")
                .Where(o => o.ShipCity == "New Jersey")
                .FirstOrDefault();
}

Disable Object Tracking unless it is Required

An ORM can manage changes between in-memory objects and the database. This feature is also known as object tracking. Note that this feature is turned on by default on all entities. As a consequence, you may modify those entities and then persist the changes to the database.

However, you should be aware of the performance cost associated. Unless necessary, you should disable object tracking. The following code sample shows how to use the AsNoTracking method to minimize memory use and increase speed.

var order = dbContext.Orders.Where(o => o.ShipCountry == "India").AsNoTracking().ToList();

You may also deactivate query-tracking behavior at the database context level. This would deactivate change tracking for all entities associated with the database context.

this.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

Here’s the updated source code of our custom Db context class named MyDbContext with change tracking and query tracking disabled.

public class MyDbContext : DbContext
{
   public MyDbContext()
   {
       ChangeTracker.QueryTrackingBehavior =
       QueryTrackingBehavior.NoTracking;
       this.ChangeTracker.LazyLoadingEnabled = false;
   }

   public MyDbContext(DbContextOptions<MyDbContext>
options) : base(options)
   {
       ChangeTracker.QueryTrackingBehavior =
       QueryTrackingBehavior.NoTracking;
       this.ChangeTracker.LazyLoadingEnabled = false;
   }
   protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
   {
   }
   protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
   }

   public DbSet<Customer> Customers { get; set; }
   public DbSet<Order> Orders { get; set; }
}

The source code of the Customer and Order classes is given below:

public class Customer
{
   public int Id { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
}
public class Order
{
   public int Id { get; set; }
   public DateTime OrderDate { get; set; } = DateTime.Now;
}

Use DbContext Pooling

A DbContext is often a lightweight object: creating and disposing of one does not require a database activity, and most applications may do so with little to no performance effect. However, each context instance builds up various internal services and objects required for executing its functions, and the overhead of doing so repeatedly might be detrimental to the application’s performance. Here’s exactly where DbContext pooling helps.

When a newly created instance is requested, it is returned from the pool rather than being created from scratch. With context pooling, context setup costs are only incurred once at the program startup time rather than every time the application runs. When using context pooling, EF Core resets the state of the context instance and places it in the pool when you dispose of an instance.

You can leverage the built-in support for DbContext pooling in EF Core to enhance performance. Using this feature, you can reuse previously generated DbContext instances rather than building them repeatedly. DbContext pooling, in other words, enables you to reuse pre-created instances to achieve a speed benefit. The following piece of code shows how you can use this feature:

services.AddDbContextPool<MyDbContext>(
   options => options. UsePostgreSql(dbConnectionString));

Other Best Practices

If you’re dealing with vast amounts of data, i.e., large datasets, you should not return the entire resultset. Instead, you should implement paging to return one page of data. You should note that the DbContext instance is not thread-safe. Hence, you should not use multiple threads to access a DbContext instance simultaneously.

You can batch queries in EF Core to minimize roundtrips and improve performance. Another way to improve query performance in EF Core is by using compiled queries.

Take advantage of the execution plan of queries to fine-tune performance, understand performance bottlenecks, etc. An execution plan comprises the series of operations performed by a database to fulfill a request. You should check the execution plans of your queries and costs (CPU, elapsed time, etc.). You may then choose the best strategy and modify the query before using the query again.

Summary

From a conceptual perspective, the DbContext is similar to the ObjectContext and represents a hybrid of the unit of work and repository design patterns. You can use it for any database interaction in an application that leverages EF Core in its data access layer. When using DbContext in EF Core, there are a few best practices to follow in order to maximize the efficiency and effectiveness of working with EF Core in your applications.

Comments are closed.