Posted in:

I was recently using EF Core's ILazyLoader for lazy loading without proxies, and ran into a performance issue that took me by surprise. When you call DbSet<T>.Add() to add an entity to the context, EF Core immediately injects the lazy loader into your entity even before you've called SaveChangesAsync(). This means if you navigate to a lazy-loaded navigation property before persisting, EF Core will try to query the database for related entities that don't exist yet.

It's an unnecessary performance overhead and the fix is fortunately very simple: don't add entities to the DbContext until right before you're ready to call SaveChangesAsync().

The Model

To understand how it behaves I created a simple example project using a Blog and Post relationship with ILazyLoader injection:

public class Blog
{
    private ICollection<Post>? _posts;
    private ILazyLoader? _lazyLoader;

    public Blog() {}

    public Blog(ILazyLoader lazyLoader)
    {
        _lazyLoader = lazyLoader;
    }

    public int Id { get; set; }
    public required string Name { get; set; }
    
    public virtual ICollection<Post> Posts
    {
        get => _lazyLoader?.Load(this, ref _posts) ?? _posts ?? [];
        set => _posts = value;
    }
}

public class Post
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public required string Content { get; set; }
    public virtual Blog? Blog { get; set; }
}

Reproducing The Problem

Now let's look at what happens when you add a blog with posts, but navigate into the Posts collection before persisting to the database:

using (var context = new BloggingContext())
{
    await context.Database.EnsureCreatedAsync();

    // Create a new Blog with two Posts
    var blog = new Blog
    {
        Name = "Test Blog",
        Posts =
        [
            new Post { Title = "First Post", Content = "Hello from EF Core 10!" },
            new Post { Title = "Second Post", Content = "Another post for testing." }
        ]
    };

    // This causes EF Core to inject the lazy loader using reflection
    context.Blogs.Add(blog);

    // Accessing blog.Posts triggers the lazy loader to query the database
    // even though this blog hasn't been saved yet!
    Console.WriteLine("Number of posts: " + blog.Posts.Count);

    await context.SaveChangesAsync();
}

When you call context.Blogs.Add(blog), EF Core uses reflection to inject an ILazyLoader instance into the Blog object. From that point on, any access to blog.Posts will trigger the lazy loading mechanism. Since the blog doesn't exist in the database yet (no Id has been assigned), EF Core will execute a query that looks something like:

SELECT [p].[Id], [p].[BlogId], [p].[Content], [p].[Title]
FROM [Posts] AS [p]
WHERE [p].[BlogId] = 0

This is completely pointless - the blog hasn't been persisted, so there can't possibly be any related posts in the database.

The Solution

The fix is straightforward: only add the entity to the context right before you call SaveChangesAsync():

using (var context = new BloggingContext())
{
    await context.Database.EnsureCreatedAsync();

    var blog = new Blog
    {
        Name = "Test Blog",
        Posts =
        [
            new Post { Title = "First Post", Content = "Hello from EF Core 10!" },
            new Post { Title = "Second Post", Content = "Another post for testing." }
        ]
    };

    // Do all your work with the blog object first
    Console.WriteLine("Number of posts: " + blog.Posts.Count);

    // Only add to context when you're ready to save
    context.Blogs.Add(blog);
    await context.SaveChangesAsync();
}

Now when you access blog.Posts, there's no lazy loader injected yet, so it just returns the collection you assigned, with no database query needed.

Summary

If you're using ILazyLoader injection in EF Core, be mindful of when you add entities to the DbContext. The lazy loader gets injected as soon as you call Add(), not when you call SaveChangesAsync(). So if you need to work with navigation properties before persisting, keep the entity disconnected from the context until you're ready to save. This avoids unnecessary database queries.