Loading Modes in Entity Framework Core

How, and When, to Use Lazy Loading, Explicit Loading and Eager Loading

Nathan
The Tech Collective
7 min readJun 5, 2024

--

When querying data with Entity Framework Core, it is important to load data efficiently. Database-driven applications can make a huge volume of calls to the database. If the database calls are coded poorly, an app’s response time will be impacted significantly.

In simple terms, a query goes to the database and then the database has to perform this query. Then the result is returned to the app. The more queries we have, the more time it takes to get the information back. Each journey to the database server uses up resources.

When working with related data, choosing the right loading strategy will have a noticeable impact on the speed of your app.

The three main approaches to loading data are Lazy Loading, Explicit Loading and Eager Loading. Let’s look at how, and when, we should use each strategy.

Photo by Mike van den Bos on Unsplash

What is the N+1 problem?

As mentioned earlier, we want to limit the amount of trips we take to the database. When working with Entity Framework Core, it is easy to make ‘hidden’ requests to the database without realising.

Hidden requests occur when the initial query to the database brings back a list of records followed by subsequent queries on those records. The alternative would be to get all the data we need in a single, larger query.

Lazy Loading and the N+1 problem

Also known as “deferred execution”, Lazy Loading reduces the volume of data retrieved from the database. When we load an entity using Lazy Loading, we ignore any related entities. Data from the child elements is only collected when we request them instead of them being provided upfront.

Lazy Loading is ideal when you are confident you don’t need any related data.

Imagine a Brewery entity that has a one-to-many relationship with a Beer entity.

// Parent

public class Brewery
{
public Guid Id { get; set; }
public string Name { get; set; }
public string City { get; set; }
public string State { get; set; }
public string WebsiteUrl { get; set; }
// Navigation property
public ICollection<Beer> Beers { get; set; }
}

// Child

public class Beer
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Style { get; set; }

// Foreign Key
public Guid BreweryId { get; set; }
}

Our app needs to get all the Brewery entries in the database. We also need to get the Beer entries associated with the Brewery.

In the code below, we request all the breweries by accessing the DbContext: _context.Breweries().ToList(). This is our first database call.

To get the beers associated with a given Brewery, we use a For loop to go through every entry in the Beers table:

public class BreweryRepository : IBreweryRepository
{
private readonly DataContext _context;

public BreweryRepository(DataContext context)
{
_context = context;
}

public IEnumerable<Brewery> GetAllBreweriesWithTheirBeers()
{
List<Brewery> breweries = context.Breweries.ToList();

/*
In each loop we cross-reference
Beer.BreweryId with the Id of our indexed Brewery.

We then return all the matching Beer values as a list.
*/
for(var i = 0; i < totalBreweries; i++)
{
breweries[i].Beers = _context.Beers.Where(
b => b.BreweryId == breweries[i].Id)
.ToList();
}

return _breweries;
}
}

The good news is that we have got the data we need. We have our list of Brewery entries, each with their list of Beer values. However, we could be making hundreds of calls to our database in the process. Imagine if our database has 2000+ breweries in the Breweries table with each brewery having 50+ beers. The query volume compounds greatly.

Lazy Loading and Proxies

There is a way to exercise Lazy Loading and still have access to related data. The Microsoft.EntityFrameworkCore.Proxies package generates ‘proxies’ of your child entities at runtime. The proxy entity lets you view related data without explicitly loading it from the database.

To introduce proxies into your app you need to install the below package:

dotnet add package Microsoft.EntityFrameworkCore.Proxies

We then need to invoke the UseLazyLoadingProxies() when registering our DbContext:

// The below code is for a .NET Minimal API

builder.Services.AddDbContext<DataContext>(
options => options
.UseLazyLoadingProxies() // <--- here
.UseSqlite(
builder.Configuration.GetConnectionString("DefaultConnection")));

Then add the virtual keyword to all navigation fields. This allows the proxy to override the values of the entity it is generating:

public class Brewery
{
public Guid Id { get; set; }
...

public virtual ICollection<Beer> Beers { get; set; }
}


public class Beer
{
public Guid Id { get; set; }
...

public virtual Brewery Brewery { get; set; }
}

Now, when we execute an operation like in the below example, we will receive a list of Brewery values and each Beers field will be accessible:



public class BreweryRepository : IBreweryRepository
{
private readonly DataContext _context;

public BreweryRepository(DataContext context)
{
_context = context;
}

public IEnumerable<Brewery> GetAllBreweriesWithTheirBeers() {}

public IEnumerable<Brewery> GetAllBreweries()
{
IEnumerable<Brewery> breweries = _context.Breweries.ToListAsync();

return breweries;
}

...

}

Eager Loading

Although an expensive operation, Eager Loading will return your main record, with its related data, in a single query.

A benefit of Eager Loading is that the code is clearer and cleaner. In our earlier example, we relied on a For loop and compared Id values. Entity Framework Core gives us two extension methods to achieve the same result in fewer lines: Include() and ThenInclude():

public class BreweryRepository : IBreweryRepository
{
private readonly DataContext _context;

public BreweryRepository(DataContext context)
{
_context = context;
}

public IEnumerable<Brewery> GetAllBreweries() {}

public IEnumerable<Brewery> GetAllBreweriesWithTheirBeers()
{
IEnumerable<Brewery> breweries = _context.Breweries
.Include(b => b.Beers)
.ToList();

return breweries;
}


...

}

If Brewery had another one-to-many relationship with an entity, we could even chain Include() calls:

public IEnumerable<Brewery> GetAllBreweriesWithTheirBeersAndSpirits()
{
IEnumerable<Brewery> breweries = _context.Breweries
.Include(b => b.Beers)
.Include(b => b.Spirits)
.ToList();

return breweries;
}

ThenInclude() works in the same way but helps to load multiple levels of related entities. For instance, if the Beer entity had a one-to-many relationship with a new entity called Ingredient we can also bring back that data in a single call:

public class BreweryRepository : IBreweryRepository
{
private readonly DataContext _context;

public BreweryRepository(DataContext context)
{
_context = context;
}

public IEnumerable<Brewery> GetAllBreweries() {}

public IEnumerable<Brewery> GetAllBreweriesWithTheirBeers() {}

public IEnumerable<Brewery> GetAllBreweriesWithTheirBeersAndSpirits() {}

public IEnumerable<Brewery> GetAllBreweriesWithBeersAndBeerIngredients()
{
IEnumerable<Brewery> breweries = _context.Breweries
.Include(b => b.Beers)
.ThenInclude(b => b.Ingredients)
.ToList();

return breweries;
}
}

Explicit Loading

With Eager Loading, we create complex queries that return a large amount of related data. Although it has a narrow use case, Explicit Loading retrieves related data for specific records. We have to load the navigation property explicitly after we have loaded the parent records.

Why would you use Explicit Loading? Depending on the use case, relying on Eager or Lazy Loading could be irresponsible when working with large data sets. The app may only require related data in a certain scenario. The scenario could be down to unique business logic or even user authorisation.

We can use our Brewery app as a reference point to describe a use case where Eager Loading will be useful.

There is a requirement to return all breweries in alphabetical order. The first five breweries need additional data as they will have a more pronounced profile at the top of the page. Therefore, we need to get related data, but only for the first five breweries:

public class BreweryRepository : IBreweryRepository
{
private readonly DataContext _context;

public BreweryRepository(DataContext context)
{
_context = context;
}

...

public IEnumerable<Brewery> GetAllBreweriesWithExplicitLoading()
{
// Step 1: Retrieve all breweries and sort them alphabetically
var breweries = context.Breweries
.OrderBy(b => b.Name)
.ToList();

// Step 2: Take the first 5 breweries from the sorted list
var top5Breweries = breweries.Take(5).ToList();

// Step 3: Explicitly load the related Beer entities for these 5 breweries
foreach (var brewery in top5Breweries)
{
context.Entry(brewery)
.Collection(b => b.Beers)
.Load();
}

return breweries;
}
}

In this code, we use Lazy Loading to retrieve the Brewery entries in our database. At this stage, we have no data about Beers. We then order and filter the Brewery data. We end with a For loop that retrospectively loads the related data into the breweries collection. The difference is that only the first five entries will have the related data. Due to Beers being a collection, we use the Collection() method to access the navigation property. If the navigation property was a one-to-one relationship, we use Reference().

When working with Entity Framework Core, it is easy to write inefficient database queries. As described earlier, hidden queries can be made which can aggregate and result in a slow-performing app. As your app expands and more data is introduced, this will become even more of a problem.

Simply copying code from existing methods, or relying on intensive strategies like For loops, will put stress on your database server. If you have to query the database, you need to reflect on why. What data do you need? What is the purpose? Don’t project data that your request doesn’t require.

Lazy Loading, Explicit Loading and Eager Loading are all valid for different reasons. Thinking deeper about which strategy you use will have a significant impact on the efficiency of your app.

🙌 🙌

--

--