Lazy Loading in Entity Framework Core

Here’s all you need to do to enable lazy loading of navigation properties in Entity Framework Core – doing this allows you to call one of the navigation properties after the original query, and EF will figure out what SQL it needs to run to pull that data.

If you know you’re going to need the data, it’s generally better to pull it upfront, using Include or including the data in the LINQ select clause. But sometimes the option is nice, so I recommend allowing it, and deciding at dev time whether or not to use it.

Install-Package Microsoft.EntityFrameworkCore.Proxies
 -- or --
dotnet add package Microsoft.EntityFrameworkCore.Proxies

In the OnConfiguring method, just tack on UseLazyLoadingProxies, like:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseLazyLoadingProxies().UseSqlServer(_connectionString);

Make sure your navigation properties are all marked virtual (thankfully, you’ll get a friendly runtime error right away if you forget this step):

[Table("Customers")]
public class Customer {
    [Key]
    public int CustomerID { get; set; }
    public string Name { get; set; } = "";

    [InverseProperty(nameof(Customer))]
    public virtual List<Car>? Cars { get; set; }
}

[Table("Cars")]
public class Car {
    [Key]
    public int CarID { get; set; }
    public string Make { get; set; } = "";
    public int CustomerID { get; set; }

    [ForeignKey(nameof(CustomerID))]
    public virtual Customer? Customer { get; set; }
}

// Use lazy loading: easiest, but requires two database calls
async static Task Main() {
    using var context = new MyContext();

    /*
        SELECT TOP(2) [c].[CustomerID], [c].[Name]
        FROM [Customers] AS [c]
        WHERE [c].[CustomerID] = 4
    */
    var cust = await context.Customers.SingleAsync(x => x.CustomerID == 4).ConfigureAwait(false);
    Console.WriteLine(cust.Name);

    /*
        exec sp_executesql N'SELECT [c].[CarID], [c].[CustomerID], [c].[Make]
        FROM [Cars] AS [c]
        WHERE [c].[CustomerID] = @__p_0',N'@__p_0 int',@__p_0=4
    */
    foreach (var car in cust.Cars!) {
        Console.WriteLine($"{car.CarID}: {car.Make}");
    }
}

// Get the data upfront using Include - single database call, but more complex query
async static Task Main() {
    using var context = new MyContext();

    /*
        SELECT [t].[CustomerID], [t].[Name], [c0].[CarID], [c0].[CustomerID], [c0].[Make]
        FROM (
            SELECT TOP(2) [c].[CustomerID], [c].[Name]
            FROM [Customers] AS [c]
            WHERE [c].[CustomerID] = 4
        ) AS [t]
        LEFT JOIN [Cars] AS [c0] ON [t].[CustomerID] = [c0].[CustomerID]
        ORDER BY [t].[CustomerID], [c0].[CarID]
    */
    var cust = await context.Customers.Include(c => c.Cars)
        .SingleAsync(x => x.CustomerID == 4).ConfigureAwait(false);
    Console.WriteLine(cust.Name);

    foreach (var car in cust.Cars!) {
        Console.WriteLine($"{car.CarID}: {car.Make}");
    }
}