Updated on
EF Core’s lazy loading proxies generate a subclass of each entity class at runtime and override its navigation properties. A property can only be overridden if it is virtual, so the keyword is what makes the proxy possible.
That is its whole job here. Without proxies, virtual changes nothing about how EF Core maps, tracks, or loads our entities, and lazy loading never needs it on a scalar property.
The rest of the article follows from that: what the proxy does with a virtual navigation property, what EF Core throws when the modifier is missing, and how to lazy load when we would rather not mark anything virtual at all.
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.
We will use two instances of DbContext, one with default parameters and one configured for lazy loading, to observe the differences, plus a third one without proxies, DataContextInjected, near the end. Thus, we will generate three 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:
using System.ComponentModel.DataAnnotations;
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; } = null!;
}
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");
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.
Why Do Navigation Properties Have to Be Virtual for EF Core Lazy Loading Proxies?
Lazy loading delays fetching related rows until the code actually touches the navigation property. With proxies, EF Core generates a second class at runtime that inherits from ours, and the query hands back an instance of that subclass in place of our own type.
A subclass can only change what the base class permits it to change. Marking a navigation property virtual is that permission, so the generated proxy replaces the getter with one that queries the database on first access. A non-virtual property cannot be overridden, and the proxy has nothing to hook into.
The keyword itself is plain C#: it is the modifier that makes the proxy’s override legal, and EF Core adds nothing to it.
Two consequences follow. The entity class must be unsealed and, as a rule, public, because a class nothing can inherit from cannot be proxied either. And only navigation properties need the modifier: a scalar property like Title arrives with the row, so there is nothing left to load later.
Lazy loading may lead to the execution of multiple additional database queries, negatively impacting performance. When we know we will need the related data, we can load it up front instead, and EF Core even lets us choose between one query and splitting one query into several.
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 with proxies available, it is necessary to add the Microsoft.EntityFrameworkCore.Proxies NuGet package and enable it via the UseLazyLoadingProxies() method when configuring the DbContext:
using Microsoft.EntityFrameworkCore;
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:
using System.ComponentModel.DataAnnotations;
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; } = null!;
}
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:
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:
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 generating 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 virtual navigation property, the proxy subclass executes a load from the database. This load is transparent to the developer code.
In other words, to enable lazy loading with proxies, 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.
The figure puts the whole chain in one place. Notice that our code only ever reads authorLazy.Books, and the generated subclass is what turns that read into a query:

EF Core is applying the Proxy design pattern here, and our article on that pattern shows how a proxy stands in for the real object when we write one by hand.
Note that it is possible to disable lazy loading on demand for a single context instance, with the DbContext’s ChangeTracker.LazyLoadingEnabled property set to false:
Despite the BooksLazy table containing multiple records for the author, we get no related data in this case.
The LazyLoader entry visible on the proxy is the same service we inject by hand below.
What Happens if a Navigation Property Is Not Virtual?
The application fails when EF Core builds its model, not when the code reads the property. EF Core validates every entity type once, the first time the context is used, and a non-virtual navigation fails that validation immediately. The exception names the property, so it is a short fix.
The message also separates the two kinds of proxy. UseChangeTrackingProxies is the stricter option and wants every property virtual, including the scalar ones. UseLazyLoadingProxies, the one enabled here, asks for the navigations alone.
A failure at startup is the better case. Without UseLazyLoadingProxies(), the same non-virtual navigation compiles, runs, and hands back an empty collection, exactly as our first example did. There is no exception and no warning, only missing data that looks like an empty table.
So when a solution uses proxies, it pays to give the proxy configuration its own context class, as our sample does, instead of switching proxies on and off in a shared one.
This is what EF Core 10 throws when we remove virtual from AuthorLazy.Books and run the application:
System.InvalidOperationException: Property 'AuthorLazy.Books' is not virtual. 'UseChangeTrackingProxies' requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. 'UseLazyLoadingProxies' requires only the navigation properties be virtual. at Microsoft.EntityFrameworkCore.Proxies.Internal.ProxyBindingRewriter.ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext`1 context)
How Do We Lazy Load Without Virtual Navigation Properties?
Proxies are the convenient option, and EF Core has two others. It can inject an ILazyLoader service into the entity itself, and an entity that loads its own navigations needs no virtual anywhere and is never replaced by a subclass.
The entity keeps a private constructor taking ILazyLoader, a backing field for the navigation, and a getter that calls LazyLoader.Load(this, ref _books). EF Core uses that constructor when it materialises the entity. Our own public constructor still works for objects we create ourselves.
The cost is a reference to Microsoft.EntityFrameworkCore.Abstractions in the entity assembly. Where even that is unwanted, the same constructor can take an Action<object, string> delegate instead, and the parameter has to be named lazyLoader.
Explicit loading is the third option and the plainest. Leave the class untouched and ask for the related data by hand with context.Entry(author).Collection(a => a.Books).Load(), which makes every round trip visible in the code.
Here is the whole entity, with no virtual anywhere in it:
using Microsoft.EntityFrameworkCore.Infrastructure;
public class AuthorInjected
{
private ICollection<BookInjected>? _books;
public AuthorInjected() { }
private AuthorInjected(ILazyLoader lazyLoader) => LazyLoader = lazyLoader;
private ILazyLoader? LazyLoader { get; set; }
public int AuthorInjectedId { get; set; }
public string? FullName { get; set; }
public ICollection<BookInjected>? Books
{
get => LazyLoader.Load(this, ref _books);
set => _books = value;
}
}
Queried from a fresh DataContextInjected, it reports its own type, VirtualKeywordInEFCore.Models.AuthorInjected, and still loads its books on first access. Microsoft’s lazy loading documentation shows the delegate version of the same pattern as well.
For explicit loading, the class stays as it is and the call site asks for the books:
using var context = new DataContextWithoutLazyLoading(); var author = context.Authors.First(a => a.FullName == "Lucy FOLEY"); context.Entry(author).Collection(a => a.Books).Load();
After Load(), author.Books.Count is 4, where the same entity read without it held 0.
The table sets the options side by side, with what each one asks of the entity class:
| Approach | Navigation must be virtual? | What the entity class carries | Use when |
|---|---|---|---|
| Lazy loading proxies | Yes, every lazily loaded navigation | Nothing but the virtual modifier; the class must be unsealed and public, or internal with InternalsVisibleTo for Castle's proxy assembly | The entity classes are ours to change and we want the loading to be invisible |
ILazyLoader injection | No | A private constructor taking ILazyLoader, a backing field, and LazyLoader.Load(this, ref _field) in the getter | We want lazy loading without proxies, and a reference to an EF Core package in the entity assembly is acceptable |
ILazyLoader as a delegate | No | The same, with Action<object, string> in place of ILazyLoader; the parameter must be named lazyLoader | The entity classes must not reference EF Core at all |
| Explicit loading | No | Nothing | We want every round trip to the database visible at the call site |
Eager loading with Include() | No | Nothing | The related data is always needed, so one query beats several |
When the entity classes are ours to change, proxies remain the shortest route. The other rows are for the projects where they are not.
Is the Virtual Keyword in C# the Same as in EF Core?
It is the same keyword doing the same job. In C#, virtual on a member means a derived class is allowed to override it, and the call is then resolved by the object’s runtime type rather than by the type of the variable holding it.
EF Core gives the keyword no extra meaning. It generates the derived class, and the keyword does what it always does, for a subclass we never see in our own source code.
Microsoft’s C# language reference says the same holds for properties: virtual properties “behave like virtual methods, except for the differences in declaration and invocation syntax”.
So whether a property should be virtual is a C# question first. Outside EF Core’s lazy loading and change-tracking proxies, mark a member virtual when a subclass has a reason to change it, and leave it alone otherwise.
For the language rules themselves, how overriding resolves at runtime and when it is worth paying for, our article on virtual methods in C# is the place to start.
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. Apart from change-tracking proxies, that’s the only role of the virtual modifier in Entity Framework. If we’re not using proxies, its usage on related entities is unnecessary.
Tested with .NET 10.0.10 and EF Core 10.0.12.



