Lazy Loading with EF Core Proxies

With the Microsoft.EntityFrameworkCore.Proxies NuGet package you can use to traverse navigation properties. This is often preferable to joins and includes such as when using one-to-many or only exploring a subset of the navigations based on client-side logic or for providers that don't support include yet.

This post will show you how to set up lazy loading with EF Core Proxies and MongoDB.

Background

To talk about lazy loading, we need to understand how EF Core handles types and relationships. In EF Core, types are categorized into four main groups:

Primitive Types

Types the EF provider handles directly when deciding how to store, retrieve and query. This includes the expected CLR-provided types like int, decimal, string etc. but also provider-specific types like ObjectId in MongoDB. EF itself just hands these types off to the provider and doesn't get too involved in the details.

Regular Entities

Non-primitive types that map directly to database collections (MongoDB), containers (Cosmos) or tables (SQL) depending on the provider. They are either:

  • Discovered via DbSet<T> on your DbContext
  • Configured with .Entity<T> on ModelBuilder
  • Declared using .HasOne or .HasMany via the ModelBuilder

Owned Entities

These are non-primitive types found as properties within regular entities:

  • Discovered by convention on your entity classes (e.g. Invoice with Lines)
  • Declared using .OwnsOne or .OwnsMany via the ModelBuilder

Complex Types

EF recently added these are an alternative to owned entities for similar use cases. As they still have a number of rough edges and are not yet supported in the MongoDB provider, we won't cover them here. The main differences is that they don't have a synthetic key tracked by EF and you can use the same instance on multiple regular entities.

Include

When you retrieve a regular entity owned entities come along automatically so any properties are pre-loaded. Just what you would expect when mapping from a document database. For example:

  • With mb.Entity<Invoice>().OwnsMany(i => i.Lines), invoice lines are loaded immediately with the invoice.
  • With mb.Entity<Invoice>().HasMany(i => i.Lines), the lines are separate entities and won't be loaded automatically.

Normally, .Include(i => i.Lines) solves this by telling the query to also bring in the lines. This results in a join query which may cause a lot of duplicate data to be returned in some cases such as a one-to-many relationship. This is because the database will return one row for each invoice line, which means the invoice data is repeated for each line. Some providers offer .AsSplitQuery() to avoid duplication by issuing multiple queries which has its own performance considerations.

EF Core Proxies

There is another option, lazy loading, which allows you to related entities to be loaded on demand as they are required, rather than all at once. While this was built-in to the original EF, in EF Core the functionality is in a separate package called Microsoft.EntityFrameworkCore.Proxies.

Using it is very simple and allows you to traverse navigation properties as if they were loaded. The proxy will detect when you access a navigation property and automatically load it for you. This is particularly useful when you have one-to-many relationships or when you only need a subset of related entities.

Getting started

  1. Add the Microsoft.EntityFrameworkCore.Proxies NuGet package to your project
  2. Enable the lazy loading proxies in your context using UseLazyLoadingProxies() in the OnConfiguring method or in the AddDbContext method in your Startup.cs file.
  3. Ensure any navigation properties are virtual and that your entities are not sealed.

How It Works

EF Core Proxies creates dynamic proxy classes that subclass from your classes overriding virtual navigation properties to add lazy-loading behavior:

  1. You load an Invoice
  2. When you access invoice.Lines, the proxy detects it hasn't been loaded yet
  3. The proxy triggers a database query to fetch the related Lines
  4. The Lines are loaded into memory and returned

Usage Example

We'll take an example here where we break the invoice lines out into their own collection of regular entities instead of the expected owned entities.

public class Invoice {
    public Guid Id { get; set; }
    public string Reference { get; set; }
    public DateTime Date { get; set; }
    public virtual List<InvoiceLine> Lines { get; set; }
}

public class InvoiceLine {
    public string Description { get; set; }
    public decimal Cost { get; set; }
}

public class InvoiceDbContext : DbContext {
    public DbSet<Invoice> Invoices { get; set; }
    public DbSet<InvoiceLine> InvoiceLines { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
        optionsBuilder
            .UseMongoDB("mongodb://localhost:27017", "InvoiceDb")
            .UseLazyLoadingProxies();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        modelBuilder.Entity<Invoice>()
            .HasMany(i => i.Lines);
    }
}

using (var context = new InvoiceDbContext()) {
    var invoice = context.Invoices.First(i => i.Reference == "INV-001");
    
    // We can just use the lines property without explicitly loading it
    decimal total = invoice.Lines.Sum(l => l.Cost);
    Console.WriteLine($"Total: {total}");
}

Caveats

  • Performance: Lazy loading can lead to the N+1 query problem, where accessing a collection property results in multiple queries being sent to the database.
  • Overhead: Proxies add a small amount of overhead to the application, as they need to create dynamic proxy classes and manage the lazy loading behavior.
  • Virtual Properties: Navigation properties must be virtual for lazy loading to work. This means you cannot use readonly properties or properties that are not virtual.
  • Sealed Classes: The classes containing navigation properties cannot be sealed, as EF Core needs to create a derived class to implement the lazy loading behavior.
  • Dependency: Proxies require an extra dependency on the Microsoft.EntityFrameworkCore.Proxies package which also includes Castle.Core for dynamic proxy generation.

Conclusion

Using EF Core Proxies for lazy loading is an easy way to work with navigation properties in many scenarios. It allows you to avoid the complexity of joins and includes, especially in cases where you have one-to-many relationships or when you only need a subset of related entities.

This approach works particularly well with MongoDB and allows you to model relationships between documents while maintaining the document-oriented approach that makes MongoDB powerful in the first place.

Hope that helps!

Damien

0 responses to Lazy Loading with EF Core Proxies

  1. Avatar for

    Information is only used to show your comment. See my Privacy Policy.