Published on
Reading Time
6 min read

Solving the N+1 Problem in Entity Framework Core

Table of Contents

Introduction

Loading related data is a fundamental part of building applications with Entity Framework Core. However, careless loading can lead to the infamous N+1 query problem that cripples performance. In this article, we'll dive deep into what causes N+1 queries, how to detect them, and proven techniques to optimize data access. You'll learn how eager loading, explicit batching, and projection queries can eliminate round trips to the database and unlock huge performance gains. Master these essential strategies and skills to diagnose and prevent N+1 queries before they sabotage your app's speed and scalability. Robust data access optimization provides a force multiplier that allows Entity Framework Core and your database to shine. Let's crack open this critical but underexplored facet of high-performance application development.

What is the N+1 Problem?

The N+1 problem describes executing multiple queries to load related entities. For example, consider the following code:

// Load all customers
var customers = context.Customers.ToList();

// Load orders for each customer
foreach (var customer in customers)
{
  var orders = context.Orders.Where(o => o.CustomerId == customer.Id).ToList();
}

This issue separates queries to load the orders for each customer. If there are N customers, it will result in N+1 total queries - 1 for the customers and N more for the orders. As the number of customers grows, this performs terribly and negates the benefits of using Entity Framework Core.

Not only does the N+1 issue affect read performance, it can also hinder writes. As Entity Framework tracks each entity loaded from its own query, updates need to be propagated across multiple change sets. This can manifest as slower saves and disjointed entity states across different change sets. With eager loading, there is just a single unified change set and no duplication of entities.

Diagnosing N+1 problems can also be challenging. The application code looks innocuous enough - just a simple loop to load some related entities. Without examining the generated SQL or profiling the database, it's hard to detect the underlying ugliness. Subtle changes to navigation properties or queries can suddenly introduce nasty N+1 behavior too. Using an automated profiler like MiniProfiler can help bubble expensive repetitive queries to the surface. Keeping a vigilant eye out for N+1 creep is important.

Solutions

Fortunately, Entity Framework Core provides several ways to eager load related data and solve the N+1 problem.

Eager Loading

Eager loading preemptively loads related entities using the Include method:

// Eager load orders with customers
var customers = context.Customers
    .Include(c => c.Orders)
    .ToList();

This issues a join to load all customers and orders in one query. Include supports recursive loading to arbitrarily nested depths.

Eager loading is the most straightforward way to avoid N+1 queries in Entity Framework Core. By proactively including related data, it prevents the context from needing to issue separate queries later.

For maximum performance, try to eagerly load as much of the expected graph of related entities as possible up front. The more navigation properties you include, the less likely separate queries will be needed later.

However, be careful not to over-eager load data that you don't actually need. Eager loading extra data can result in larger result sets and possibly slower initial queries. Pay attention to the graph of data your screens or processes actually require and aim to eagerly load just that subset.

Eager loading multiple levels of related entities is easy too. Simply chain multiple Includes to recursively load a deep graph. For example:

context.Customers
    .Include(c => c.Orders)
        .ThenInclude(o => o.Items)
    .ToList();

Used judiciously, eager loading is your primary weapon against N+1 queries.

Explicit Loading

Explicit loading allows separate queries but avoids N+1 behavior:

// Single query for customers
var customers = context.Customers.ToList();

// Single query for all orders
var allOrders = context.Orders.Where(o => customers.Contains(o.Customer))
    .ToList();

// Assign orders to customers
foreach (var customer in customers)
{
  customer.Orders = allOrders.Where(o => o.CustomerId == customer.Id).ToList();
}

By batch loading all orders in one query, we prevent separate per-customer queries.

With explicit loading, you deliberately issue separate queries - one for the main entities, and one for the related entities.

The key is the second query batches loading the related entities to avoid repeating queries. We filter orders by the list of customers first, then assign the orders to each customer locally.

This prevents Entity Framework from automatically loading orders per customer resulting in N+1 queries. It can be a good option if you only need orders data for a subset of customers.

Explicit loading allows more flexibility than eager loading. You can load related entities conditionally for just part of the object graph. However, it does require manually stitching entities together locally.

Overall, explicit loading is a viable alternative to eager loading to avoid N+1 queries in situations where you want finer grained control over when related data is loaded.

Projection Queries

Projection using anonymous types can eagerly load related data:

var customers = context.Customers
    .Select(x => new
    {
        Customer = x,
        Orders = x.Orders
    })
    .ToList();

This avoids explicit nested looping while still using eager loading.

Projection queries with Entity Framework Core allow shaping or transforming the query results into a different structure. Here we are projecting customers into anonymous types that include both the customer and related orders data.

Behind the scenes, this will execute as a single query joining across the customers and orders tables. It eagerly loads the related orders without the need for explicit loops like eager loading.

Projection can be useful for limiting the amount of data returned from the query. You can avoid pulling back entire entity classes if you only need a subset of properties.

Using projection may involve additional code to translate results into view models or DTOs. However, for read-only scenarios it can optimize data transfer while still solving N+1 queries.

One downside to watch is that projection will not track entities for change tracking. So any updates would need to be done through separate queries. Overall, projection is a great tool that can optimize queries while preloading related data.

Conclusion

Eliminating N+1 queries should be a top priority when building applications using Entity Framework Core. The performance penalties of loading related data through separate per-entity queries compounds exponentially as the dataset grows. Fortunately, Entity Framework Core provides powerful tools in eager loading, explicit batching, and projection to preempt these inefficient queries. By mastering these techniques, developers can optimize data access, reduce database load, and achieve blazing fast performance. Scalable applications require foresight to avoid N+1 pitfalls before they bring your app to a crawl. Use the strategies outlined here and profiling tools to seek out and destroy inefficient queries. Your users will appreciate the speed and smoothness that diligent optimization brings.