Many .NET applications use Entity Framework Core (EF Core), a popular ORM framework. It offers three distinct approaches to loading related data – eager loading, lazy loading, and explicit loading. In this article, we will focus on the mechanism of lazy loading and why we use the virtual keyword for navigation class properties in EF Core.

To download the source code for this article, you can visit our GitHub repository.

For more details on eager loading and lazy loading in EF Core, check out our articles on Lazy Loading and Eager Loading in Entity Framework Core and ASP.NET Core Web API with EF Core DB-First Approach.

EF Core Application Setup

To illustrate the topic, we’ll use a console application and a small local database using SQLite and Entity Framework Core. The application will reference the Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.Sqlite NuGet packages.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

We will use two instances of DbContext, one with default parameters and one configured for lazy loading, to observe the differences. Thus, we will generate two instances of databases. We’ll also define a DataSeeder class to populate the databases.

EF Core Default Behavior Does Not Require Virtual Navigation Properties

Without specific settings, EF Core’s default behavior doesn’t use lazy loading. Therefore, if we request related entities of a parent entity, we won’t retrieve them.

In our application, let’s define two model entities, Author and Book:

public class Author
{
    [Key]
    public int AuthorId { get; set; }
    public string? FullName { get; set; }
    public ICollection<Book> Books { get; set; } = new List<Book>();
}

public class Book
{
    [Key]
    public int BookId { get; set; }
    public string? Title { get; set; }
    public int AuthorId { get; set; }
    public Author Author { get; set; }
}

Let’s see now how EF Core runs by default when we request data with related properties: 

Console.WriteLine("No Lazy Loading:");

using var contextWithoutLazyLoading = new DataContextWithoutLazyLoading();
DataSeeder.SeedWithoutLazy(contextWithoutLazyLoading);
Author author;
author = contextWithoutLazyLoading.Authors.AsNoTracking().First(a => a.FullName == "Lucy FOLEY");
if (author is not null)
{
    Console.WriteLine($"Author Name: {author.FullName}");
    Console.WriteLine($"{author.FullName}'s Books number: {author.Books.Count}");

    foreach (var book in author.Books)
    {
        Console.WriteLine($"Book Title: {book.Title}");
    }
}

Here, we’ve defined and then assigned a value to the author variable on two separate lines, which will help us see a change in class type later on. For now though, we can see that although we have four books for the author in the database, the value returned by author.Books.Count is 0, and EF Core does not retrieve the related data.

EF Core Lazy Loading With Virtual Navigation and Proxies

Lazy loading is a mechanism that allows us to delay loading related data from the database until we specifically request it, requesting associated entities only when needed rather than all at once. This is particularly useful when dealing with one-to-many relationships where related entities won’t be used immediately.

It’s important to note that lazy loading may lead to the execution of multiple additional database queries, negatively impacting performance.

Enable Lazy Loading in EF Core

EF Core 2.1 introduced lazy loading, but EF Core does not include it by default due to the disadvantage mentioned above.

To make lazy loading available, it is necessary to add the Microsoft.EntityFrameworkCore.Proxies NuGet package and enable it via the UseLazyLoadingProxies() method when configuring the DbContext:

public class DataContextLazyLoading : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseSqlite("DataSource=LibraryLazy.db")
               .UseLazyLoadingProxies();

        base.OnConfiguring(options);
    }

    public DbSet<AuthorLazy> AuthorsLazy { get; set; }
    public DbSet<BookLazy> BooksLazy { get; set; }
}

Here, we override the OnConfiguring() method, and within it we call UseLazyLoadingProxies() to ensure that lazy loading is enabled for the EF Core DbContext. 

It also requires all our navigation properties to be virtual:

 public class AuthorLazy
{
    [Key]
    public int AuthorId { get; set; }
    public string? FullName { get; set; }
    public virtual ICollection<BookLazy> Books { get; set; } = new List<BookLazy>();
}

 public class BookLazy
{
    [Key]
    public int BookId { get; set; }
    public string? Title { get; set; }
    public int AuthorId { get; set; }
    public virtual AuthorLazy Author { get; set; }
}

If we forget the virtual modifier on an entity’s navigation property, the program will throw an InvalidOperationException exception.

Proxy Classes and the Virtual Keyword in Navigation Properties

It’s time to run our application using lazy loading. Let’s use the Debug mode and progress step by step:

EF Core Lazy Loading - Before requesting data - Model Entity Type

Here, we’ve defined and assigned a value to the authorLazy variable on two separate lines, which allows us to make some interesting observations. Unsurprisingly, we notice that the authorLazy variable is of type Models.AuthorLazy when it is defined.

But when we step to the next line and request data with lazy loading, we observe that authorLazy is now of type Castle.Proxies.AuthorLazyProxy. As we continue to execute the next statements, we note similarly that the bookLazy variable is of type Castle.Proxies.BookLazyProxy:

EF Core Lazy Loading - After requesting data - Proxy Entity Type

Where do Castle.Proxies.AuthorLazyProxy and Castle.Proxies.BookLazyProxy come from?

How POCO Proxies and Virtual Navigation Properties Work

A proxy is an automatically generated type that inherits from the domain object type (i.e. from our entity POCO class). Query execution through EF Core creates dynamic proxy entities. Generation of these POCO entities occurs at runtime. This enables additional features like lazy loading. This is facilitated by the Microsoft.EntityFrameworkCore.Proxies package, which is dependent on the Castle.Core package. EF Core makes use of the latter package for dynamically loading POCO proxies.

And what is the use of the virtual navigation properties in EF Core?

A dynamic proxy class overrides the virtual properties of the original entity to perform loading automatically when accessing these properties. Therefore, when accessing a property mapped to the database, the proxy subclass executes a load from the database. This load is transparent to the developer code.

In other words, to enable lazy loading, we must add virtual modifiers to the relations. Behind the scenes, when there is a lazy-loaded relationship property between two entities, EF Core creates a proxy class by deriving from the entity class and overriding its virtual properties. Proxy entities provide a convenient method to enhance POCO entities at runtime with additional features such as lazy loading within an ORM framework.

Note that it is possible to disable lazy loading on demand for a single request, with the DbContext’s ChangeTracker.LazyLoadingEnabled property set to false:

Lazy Loading disabled on demand with ChangeTracker.LazyLoadingEnbled set to false

Despite the BooksLazy table containing multiple records for the author, we get no related data in this case.

Conclusion

In the EF Core world, the virtual keyword is necessary for entity class properties when using lazy loading with proxies. Proxies can override these properties to trigger a database load on first access. That’s the only role of the virtual modifier in Entity Framework. If we’re not using lazy loading with proxies, its usage on related entities is unnecessary.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!