Using EF Core, references can be eager loaded, explicitly loaded, and lazy loaded. With eager loading you load references by specifying what references should be included when defining the query. Specifying the query, you use the Include method to define what references should be included. This is best when knowing in advance the needed references. With explicit loading you explicitly invoke the Load method when references to single items or collections should be loaded. With lazy loading, you do not need to explicitly invoke the Load method. Instead, just accessing the property dynamically invokes a query to the database to retrieve the data for the needed references.
With EF Core, lazy loading is available since version 2.1. This article gives information on lazy loading which is very different to lazy loading with Entity Framework 6.
Overview
I usually prefer eager loading or explicit loading to lazy loading. With lazy loading, reading the code you can easily miss where queries to the database are done. More queries than needed could be done to retrieve data from the database. Using eager loading you can reduce the number of queries, and access relations with a single SQL query. Using explicit loading, usually the same number of queries like with lazy loading are done – it’s just shown explicitly where the database access is used.
Using lazy loading, every time a property is accessed and the data is not yet retrieved, a query to the database is done.
Because of the disadvantages of lazy loading, comparing the implementations of lazy loading between Entity Framework and EF Core, it’s now implemented in a different way. You need to explicitly turn it on.
If the issues don’t apply to your scenario, go ahead and use lazy loading. There’s just a little more work needed defining the model types, and doing some configuration.
Preparations
To use lazy loading, an additional package needs to be added. Lazy loading is not available if you just use the package Microsoft.EntityFrameworkCore. For using lazy loading, the package Microsoft.EntityFrameworkCore.Proxies needs to be added to the project references. As mentioned previously, because of the lazy loading disadvantages, it’s not included by default with your EF Core projects.
Checking the dependencies, Microsoft.EntityFrameworkCore.Proxies is dependent on the package Castle.Core. Castle.Core is part of the Castle Project. The Castle Project consists of several interesting projects. One of these is DynamicProxy. EF Core makes use of this package for dynamic-loading proxies.
With the sample code I’m using Visual Studio 2019 and C# 8. In case you don’t have this versions yet, you can change the code easily to use C# 7 – just remove some features on nullable reference types.
Configure Proxy
Proxies are used to make lazy loading possible. After adding the reference to the NuGet package, proxies can be configured using the method UseLazyLoadingProxies
. This is an extension method to extend the DbContextOptionsBuilder
. This method turns on proxy creation and uses proxy services implemented in the referenced NuGet package.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private ServiceProvider GetServiceProvider() => | |
new ServiceCollection() | |
.AddLogging(config => | |
{ | |
config.AddConsole() | |
.AddDebug() | |
.AddFilter(level => level > LogLevel.Debug); | |
}) | |
.AddTransient<BooksService>() | |
.AddDbContext<BooksContext>(options => | |
{ | |
options.UseLazyLoadingProxies() | |
.UseSqlServer(Configuration.GetConnectionString(BooksConnection)); | |
}) | |
.BuildServiceProvider(); |
With the sample code, the lazy loading proxies are configured using the DI container. In case you are not using the DI Container with your EF Core application, you can use the method
UseLazyLoadingProxies
in theOnConfiguring
method of the EF Core context.
Models and Context
The sample application defines the model classes Book
, Chapter
, and User
.
The Book
class contains a list of chapters with a one-to-many relation. The Book
type also references multiple User
types with different roles. A user can be the author, the reviewer, or the project editor. To make the class ready for lazy loading, the relations need to be specified with the virtual modifier. Behind the scenes, the created proxy class derives from the Book
class and overrides these properties. The proxy then loads the data needed on first access of the property.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Book | |
{ | |
public Book(int bookId, string title) => (BookId, Title) = (bookId, title); | |
public int BookId { get; set; } | |
public string Title { get; set; } | |
public virtual ICollection<Chapter> Chapters { get; } = new List<Chapter>(); | |
public int? AuthorId { get; set; } | |
public int? ReviewerId { get; set; } | |
public int? EditorId { get; set; } | |
public virtual User? Author { get; set; } | |
public virtual User? Reviewer { get; set; } | |
public virtual User? Editor { get; set; } | |
} |
With the EF Core context class BooksContext
, the fluent API is used to specify the relations. The book has a list of chapters, and a chapter belongs to one book – this is specified with HasMany
and WithOne
. One user is associated with multiple books with the relations WrittenBooks
, ReviewedBooks
, and EditedBooks
.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class BooksContext : DbContext | |
{ | |
public BooksContext(DbContextOptions<BooksContext> options) | |
: base(options) { } | |
public DbSet<Book> Books { get; private set; } | |
public DbSet<Chapter> Chapters { get; private set; } | |
public DbSet<User> Users { get; private set; } | |
protected override void OnModelCreating(ModelBuilder modelBuilder) | |
{ | |
modelBuilder.Entity<Book>() | |
.HasMany(b => b.Chapters) | |
.WithOne(c => c.Book) | |
.OnDelete(DeleteBehavior.Cascade); | |
modelBuilder.Entity<Book>() | |
.HasOne(b => b.Author) | |
.WithMany(a => a.WrittenBooks) | |
.HasForeignKey(a => a.AuthorId) | |
.OnDelete(DeleteBehavior.Restrict); | |
modelBuilder.Entity<Book>() | |
.HasOne(b => b.Reviewer) | |
.WithMany(r => r.ReviewedBooks) | |
.HasForeignKey(b => b.ReviewerId) | |
.OnDelete(DeleteBehavior.Restrict); | |
modelBuilder.Entity<Book>() | |
.HasOne(b => b.Editor) | |
.WithMany(e => e.EditedBooks) | |
.HasForeignKey(b => b.EditorId) | |
.OnDelete(DeleteBehavior.Restrict); | |
modelBuilder.Entity<Chapter>() | |
.HasOne(c => c.Book) | |
.WithMany(b => b.Chapters) | |
.HasForeignKey(c => c.BookId); | |
modelBuilder.Entity<User>() | |
.HasMany(a => a.WrittenBooks) | |
.WithOne(nameof(Book.Author)) | |
.OnDelete(DeleteBehavior.Restrict); | |
modelBuilder.Entity<User>() | |
.HasMany(r => r.ReviewedBooks) | |
.WithOne(nameof(Book.Reviewer)) | |
.OnDelete(DeleteBehavior.Restrict); | |
modelBuilder.Entity<User>() | |
.HasMany(e => e.EditedBooks) | |
.WithOne(nameof(Book.Editor)) | |
.OnDelete(DeleteBehavior.Restrict); | |
SeedData(modelBuilder); | |
} | |
} |
The sample code uses the fluent API to specify the relation. Relations can also be specified using annotations. The book Professional C# 7 and .NET Core 2.0 covers all variants.
Lazy Loading
To access books, LINQ queries can be done like the one shown passing a where clause. After iterating the books, the references to chapters, authors, reviewers, and editors are done just by accessing the properties of the Book
type:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void GetBooksWithLazyLoading() | |
{ | |
var books = _booksContext.Books.Where(b => b.Publisher.StartsWith("Wrox")); | |
foreach (var book in books) | |
{ | |
Console.WriteLine(book.Title); | |
foreach (var chapter in book.Chapters) | |
{ | |
Console.WriteLine($"{chapter.Number}. {chapter.Title}"); | |
} | |
Console.WriteLine($"author: {book.Author?.Name}"); | |
Console.WriteLine($"reviewer: {book.Reviewer?.Name}"); | |
Console.WriteLine($"editor: {book.Editor?.Name}"); | |
} | |
} |
Accessing the books, from the LINQ statement a SQL query is generated to access different columns from the Books
table:
SELECT [b].[BookId], [b].[AuthorId], [b].[EditorId], [b].[Publisher], [b].[ReviewerId], [b].[Title] FROM [Books] AS [b] WHERE [b].[Publisher] LIKE N'Wrox' + N'%' AND (LEFT([b].[Publisher], LEN(N'Wrox')) = N'Wrox')
With the first query, other tables than the Books
table are not accessed. However, accessing the Chapters property, this SQL query is done:
SELECT [e].[ChapterId], [e].[BookId], [e].[Number], [e].[Title] FROM [Chapters] AS [e] WHERE [e].[BookId] = @__get_Item_0
Later on in the code, accessing the Author, Reviewer, and Editor relations, more queries are done.
SELECT [e].[UserId], [e].[Name] FROM [Users] AS [e] WHERE [e].[UserId] = @__get_Item_0
When the data was not loaded previously, every time a property is accessed that maps to a related table, another query to the database is done. Behind the scenes, the query doesn’t return the defined Book
types, but instead the class Castle.Proxies.BookProxy
is returned. This class derives from the base class Book
and overrides virtual properties.
Explicit Loading
With lazy loading, just properties need to be accessed from the C# code to get the data from the database as needed. This is easy to do, but you might miss better performance by reducing the number of queries. Using explicit loading you do have the same number of queries, it’s just easier to detect from the source code. Using the Collection
method from the EntityEntry
type, 1:n relations can be retrieved from the program invoking the Load
method. Here, the same SQL statement is generated accessing the book chapters as with lazy loading. Accessing a 1:1 relation, the Reference
method is used – again with the Load
method.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void GetBooksWithExplicitLoading() | |
{ | |
var books = _booksContext.Books.Where(b => b.Publisher.StartsWith("Wrox")); | |
foreach (var book in books) | |
{ | |
Console.WriteLine(book.Title); | |
EntityEntry<Book> entry = _booksContext.Entry(book); | |
entry.Collection(b => b.Chapters).Load(); | |
foreach (var chapter in book.Chapters) | |
{ | |
Console.WriteLine($"{chapter.Number}. {chapter.Title}"); | |
} | |
entry.Reference(b => b.Author).Load(); | |
Console.WriteLine($"author: {book.Author?.Name}"); | |
entry.Reference(b => b.Reviewer).Load(); | |
Console.WriteLine($"reviewer: {book.Reviewer?.Name}"); | |
entry.Reference(b => b.Editor).Load(); | |
Console.WriteLine($"editor: {book.Editor?.Name}"); | |
} | |
} |
You can also use the IsLoaded
property to see if the related data is already loaded. The implementation of the Load
method itself checks if the related data is already loaded to not query the database another time if the data is already in memory.
With explicit loading the source code gets more complex when accessing the objects from the EF Core database context. Related data needs to be explicit loaded using the
Load
method from theCollectionEntry
or theReferenceEntry
returned from theCollection
andReference
methods. The advantage using explicit loading is that you see it from the C# source code that additional SQL queries are done. Also, the model type doesn’t need special treatment. Here, virtual properties are no longer needed.
Eager Loading
In case you already know in advance the needed loaded relations, eager loading can be used. This should be the preferred way to get the data from the database. You can get deep relations with just one query. Defining the LINQ query, you now add calls to the Include
method and specify the relations that should be included. The Include
method is an extension to the IQueryable
type defined in the Microsoft.EntityFrameworkCore
namespace.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void GetBooksWithEagerLoading() | |
{ | |
var books = _booksContext.Books | |
.Where(b => b.Publisher.StartsWith("Wrox")) | |
.Include(b => b.Chapters) | |
.Include(b => b.Author) | |
.Include(b => b.Reviewer) | |
.Include(b => b.Editor); | |
foreach (var book in books) | |
{ | |
Console.WriteLine(book.Title); | |
foreach (var chapter in book.Chapters) | |
{ | |
Console.WriteLine($"{chapter.Number}. {chapter.Title}"); | |
} | |
Console.WriteLine($"author: {book.Author?.Name}"); | |
Console.WriteLine($"reviewer: {book.Reviewer?.Name}"); | |
Console.WriteLine($"editor: {book.Editor?.Name}"); | |
} | |
} |
In case you have deeper relations, and need to access relation by relation, e.g. by accessing another relation from the
Chapter
type, you can use theThenInclude
method.
The SQL statement now becomes more complex. Not just the Books
table is queried, but with my current SQL Server provider two SELECT statements are done accessing the Books
, Chapters
, and Users
table using LEFT JOIN
and INNER JOIN
. Now just one time information from the database is retrieved instead when accessing every single book:
SELECT [b].[BookId], [b].[AuthorId], [b].[EditorId], [b].[Publisher], [b].[ReviewerId], [b].[Title], [b.Editor].[UserId], [b.Editor].[Name], [b.Reviewer].[UserId], [b.Reviewer].[Name], [b.Author].[UserId], [b.Author].[Name] FROM [Books] AS [b] LEFT JOIN [Users] AS [b.Editor] ON [b].[EditorId] = [b.Editor].[UserId] LEFT JOIN [Users] AS [b.Reviewer] ON [b].[ReviewerId] = [b.Reviewer].[UserId] LEFT JOIN [Users] AS [b.Author] ON [b].[AuthorId] = [b.Author].[UserId] WHERE [b].[Publisher] LIKE N'Wrox' + N'%' AND (LEFT([b].[Publisher], LEN(N'Wrox')) = N'Wrox') ORDER BY [b].[BookId] SELECT [b.Chapters].[ChapterId], [b.Chapters].[BookId], [b.Chapters].[Number], [b.Chapters].[Title] FROM [Chapters] AS [b.Chapters] INNER JOIN ( SELECT DISTINCT [b0].[BookId] FROM [Books] AS [b0] LEFT JOIN [Users] AS [b.Editor0] ON [b0].[EditorId] = [b.Editor0].[UserId] LEFT JOIN [Users] AS [b.Reviewer0] ON [b0].[ReviewerId] = [b.Reviewer0].[UserId] LEFT JOIN [Users] AS [b.Author0] ON [b0].[AuthorId] = [b.Author0].[UserId] WHERE [b0].[Publisher] LIKE N'Wrox' + N'%' AND (LEFT([b0].[Publisher], LEN(N'Wrox')) = N'Wrox') ) AS [t] ON [b.Chapters].[BookId] = [t].[BookId] ORDER BY [t].[BookId]
Instead of accessing the database with every property accessing a relation, the data is loaded early with less SQL statements sent to the database. Similar to explicit loading, the model doesn’t need special treatment. This scenario can also be used to return the model type and have all the associated data as needed when you can’t access the context instance to get additional data.
Summary
Using models to load related data is easy as long as the context is available. However, you need to pay attention not to create too many queries accessing the data from the database. If you know the relations needed in advance, you can use eager loading.
With lazy loading, proxy classes are created that derive from the model type. The proxy class overrides virtual properties to retrieve data needed dynamically.
The sample code is based on an EF Core sample from the book Professional C# 7 and .NET Core 2.0. In the book and in the book’s source code repository you can find samples with eager and explicit loading.
If you’ve read this far, consider buying me a coffee which helps me staying up longer and writing more articles.
Interesting Links for this article:
Mapping to Getter-only Properties with EF Core
C# 8 & No More NullReferenceExceptions – What about legacy code?
Code Samples for Professional C# 7 and .NET Core 2.0
Complete Sample Code for this article
More information on EF Core and writing data-driven applications is in my book Professional C# 7 and .NET Core 2.0, and in my Data Programming workshops.
Enjoy learning and programming!
Christian
If you define your navigation property virtual, Entity Framework will at runtime create a new class (dynamic proxy) derived from your class and uses it instead of your original class. This new dynamically created class contains logic to load the navigation property when accessed for the first time. This is referred to as “lazy loading”. It enables Entity Framework to avoid loading an entire tree of dependent objects which are not needed from the database.
https://dipoletechi.com/blog…/virtual-in-entity-framework/
Thanks
LikeLike
This is the behavior with Entity Framework. With Entity Framework Core you need to do more – just read the article.
LikeLike
If you define your navigation property virtual, Entity Framework will at runtime create a new class (dynamic proxy) derived from your class and uses it instead of your original class. This new dynamically created class contains logic to load the navigation property when accessed for the first time. This is referred to as “lazy loading”. It enables Entity Framework to avoid loading an entire tree of dependent objects which are not needed from the database.
https://dipoletechi.com/blog-post/virtual-in-entity-framework/
LikeLike
What would you change in your class if the Author were required
LikeLike