Section 5 of 6

EF Core Performance

🎯 What You'll Learn

  • N+1 query problem
  • Eager loading
  • Select projections
  • AsNoTracking
  • Compiled queries

N+1 Query Problem

The N+1 problem occurs when you load a collection, then load related data for each item.

N+1 Problem (BAD!) C#
// ❌ Bad - 1 query for products + N queries for categories
var products = await _context.Products.ToListAsync(); // 1 query
foreach (var product in products)
{
    var category = product.Category.Name; // N queries!
}

Eager Loading with Include

Eager Loading (GOOD!) C#
// ✅ Good - 1 query with JOIN
var products = await _context.Products
    .Include(p => p.Category)
    .ToListAsync();

Multiple Includes

Multiple Includes C#
var products = await _context.Products
    .Include(p => p.Category)
    .Include(p => p.Supplier)
    .ToListAsync();

ThenInclude for Nested

Nested Include C#
var orders = await _context.Orders
    .Include(o => o.OrderItems)
        .ThenInclude(i => i.Product)
    .ToListAsync();

Select Projections

Only select the data you need.

Projection C#
// ❌ Bad - loads entire entity
var products = await _context.Products.ToListAsync();

// ✅ Good - only loads needed columns
var products = await _context.Products
    .Select(p => new 
    {
        p.Id,
        p.Name,
        p.Price
    })
    .ToListAsync();

AsNoTracking

Disable change tracking for read-only queries.

AsNoTracking C#
// ✅ Good for read-only
var products = await _context.Products
    .AsNoTracking()
    .ToListAsync();

Compiled Queries

Pre-compile frequently used queries.

Compiled Query C#
private static readonly Func<InvenTrackDbContext, int, Task<Product>> _getProductById =
    EF.CompileAsyncQuery((InvenTrackDbContext context, int id) =>
        context.Products.FirstOrDefault(p => p.Id == id));

public async Task<Product> GetProductAsync(int id)
{
    return await _getProductById(_context, id);
}

InvenTrack Example

Optimized ProductsController.cs C#
public class ProductsController : Controller
{
    // ✅ Good - AsNoTracking for read-only
    [HttpGet]
    public async Task<IActionResult> Index()
    {
        var products = await _context.Products
            .AsNoTracking()
            .Include(p => p.Category)
            .ToListAsync();
        
        return View(products);
    }

    // ✅ Good - Projection for list view
    [HttpGet("api/products")]
    public async Task<IActionResult> GetProducts()
    {
        var products = await _context.Products
            .Select(p => new
            {
                p.Id,
                p.Name,
                p.Price,
                CategoryName = p.Category.Name
            })
            .ToListAsync();
        
        return Ok(products);
    }

    // ✅ Good - Include for details
    [HttpGet("{id}")]
    public async Task<IActionResult> Details(int id)
    {
        var product = await _context.Products
            .Include(p => p.Category)
            .Include(p => p.Supplier)
            .FirstOrDefaultAsync(p => p.Id == id);
        
        if (product == null)
            return NotFound();
        
        return View(product);
    }
}

Key Takeaways

  • N+1 problem: Avoid lazy loading in loops
  • Include(): Eager load related data
  • ThenInclude(): Load nested relationships
  • Select(): Project only needed columns
  • AsNoTracking(): Disable tracking for read-only
  • Compiled queries: Pre-compile frequent queries