Lazy loading in Entity Framework Core allows EF Core to retrieve related data whenever it needs it. The EF Core retrieves the related data behind the scene, whenever we access the navigational property. The EF Core does not support Lazy Loading out of the box. Hence to use it we need to enable it. There are two ways you can enable Lazy Loading. One using the Proxies
Package and the one by injecting ILazyLoader
service. In this tutorial, we will learn how to enable and how to use it.
Database:
The Database for this tutorial is taken from the chinook database.Source Code:
The source code of this project available in GitHub. It also contains the script of the database
Table of Contents
Enabling Lazy Loading
Unlike the its predecessor EF Core does not support it out of the box. To Enable it you have two options
- Use the
Microsoft.EntityFrameworkCore.Proxies
Package - Use the
ILazyLoader
Service
Proxies
The Lazy Loading in Entity Framework uses the Proxies classes. It was enabled by default as it was part of the man package. In EF Core the Proxies option is now part of the Microsoft.EntityFrameworkCore.Proxies
package.
There three steps involved in enabling Lazy Loading using Proxies
- Install
Proxies
Package - Enable Lazy Loading using
UseLazyLoadingProxies
- Mark the Property as
Virtual
Install Proxies
Install the Microsoft.EntityFrameworkCore.Proxies
using the PMC Console as shown below. Ensure that you install the correct version as per your EF Core version. The latest version available is 3.1.3
1 2 3 | Install-Package Microsoft.EntityFrameworkCore.Proxies |
Enable Lazy Loading
How to use the UseLazyLoadingProxies
method depends on whether you are using .Net Core Console apps or ASP.NET Core
.NET Core Console Apps
In Console apps, you can enable it in the OnConfiguring
method in the context class.
1 2 3 4 5 6 7 8 9 10 11 12 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder .UseLazyLoadingProxies() ===> Enable it here .UseLoggerFactory(MyLoggerFactory) .UseSqlServer(ConnectionString); } } |
ASP.NET Core Apps
Open the startup.cs
class in ASP.NET Core Apps and locate the ConfigureServices()
method where you have AddDbContext
method.
1 2 3 4 5 6 7 8 9 10 11 12 | public void ConfigureServices(IServiceCollection services) { services.AddDbContext<DbCon>(options => { options .UseLazyLoadingProxies() .UseSqlServer(ConnectionString); }); } |
Mark the Property as Virtual
Finally, you need to add virtual
keyword to any navigation property, which you want to participate in the Lazy Loading.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public partial class Album { public Album() { Track = new HashSet<Track>(); } public int AlbumId { get; set; } public string Title { get; set; } public int ArtistId { get; set; } public virtual Artist Artist { get; set; } ==> Virtual keyword enables lazy loading public virtual ICollection<Track> Track { get; set; } ==> } |
ILazyLoader Service
The second option is to inject the ILazyLoader
service into an entity. ILazyLoader
is part of the Microsoft.EntityFrameworkCore.Abstractions
package. This package is already part of the main package and hence no need to install it separately.
Injecting ILazyLoader Service
There are two ways by which you can inject ILazyLoader
service into a entity
.
- Injecting the
Lazyloader
service (ILazyLoader)
. - Using the
Action<object, string>
delegate to Inject the lazy-loading service
EF Core can inject few selected services into an entity type’s constructor. This does not depend on Dependency injection built into .NET Core / ASP.NET Core. It can inject only DbContext
, ILazyLoader
, a lazy-loading delegate Action<object, string>
& IEntityType
.
ILazyLoader Service
The following example shows how to inject ILazyLoader
in the constructor of the entity
. The constructor can be private
Note that you should also define a Parameter less constructor, otherwise you will not be able to create a new entity. i.e the statement (var album = new Album();
will throw an error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | using Microsoft.EntityFrameworkCore.Infrastructure; using System.Collections.Generic; public partial class Album { private readonly ILazyLoader _lazyLoader; public Album() { _Track = new HashSet<Track>(); } private Album(ILazyLoader lazyLoader) //Injecting Service { _lazyLoader = lazyLoader; } public int AlbumId { get; set; } public string Title { get; set; } public int ArtistId { get; set; } //public virtual Artist Artist { get; set; } //Remove this private Artist _Artist; public Artist Artist //Add a new getter & Setter { get => _lazyLoader.Load(this, ref _Artist); //Gettr calls the _lazyLoader.Load set => _Artist = value; } //public virtual ICollection<Track> Track { get; set; } private ICollection<Track> _Track; public ICollection<Track> Track { get => _lazyLoader.Load(this, ref _Track); set => _Track = value; } } |
lazy-loading delegate
The following example shows how to inject the ILazyLoader.Load
method as a delegate.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | public partial class Album { public Album() { _Track = new HashSet<Track>(); } private Action<object, string> _lazyLoader { get; set; } //Action Deletage private Album(Action<object, string> lazyLoader) //Injecting Delegate { _lazyLoader = lazyLoader; } public int AlbumId { get; set; } public string Title { get; set; } public int ArtistId { get; set; } //public virtual Artist Artist { get; set; } //Remove this private Artist _Artist; public Artist Artist //Add a new getter & Setter { get => _lazyLoader.Load(this, ref _Artist); //Gettr calls the _lazyLoader.Load set => _Artist = value; } //public virtual ICollection<Track> Track { get; set; } private ICollection<Track> _Track; public ICollection<Track> Track { get => _lazyLoader.Load(this, ref _Track); set => _Track = value; } } |
Create a new PocoLoadingExtensions.cs
class. Define the Load
extension method as shown below. The load
method loads related data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public static class PocoLoadingExtensions { public static TRelated Load<TRelated>( this Action<object, string> loader, object entity, ref TRelated navigationField, [CallerMemberName] string navigationName = null) where TRelated : class { loader?.Invoke(entity, navigationName); return navigationField; } } |
Once ILazyLoader
service using one of the methods above. You can create a separate getter
& setter
for each navigation property, where you want to implement Lazy Loading.
In the getter
, use the _lazyLoader.Load(this, ref _Track);
to load the entity, whenever the navigational property is accessed.
Lazy Loading in Action
Once you enabled it, You can try running this query.
Without Lazy Loading the loop foreach (var t in p.Track)
will result in an error as EF core will not load the Tracks
collection. But if it is working, the as soon as you tried to access the track
the EF Core will sends a query to the database to get list of tracks
Similarly as soon as you access the p.Artist.Name
it sends an another query to retentive the Artist
entity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | using (ChinookContext db = new ChinookContext()) { var albums = db.Album.Take(5).ToList(); foreach (var p in albums) { foreach (var t in p.Track) { Console.WriteLine("{0} {1} {2}", p.Title,p.Artist.Name, t.Name); } Console.WriteLine("Press any key to continue"); Console.ReadKey(); } } |
Lazy Loading Pitfalls
Lazy Loading is a good feature to have. But it also creates lot of Performance issues, if you not carefull
For Example consider this query. It displays the Track name and album title of all the Tracks in the database and works perfectly correct.
The first query brings all the Track
in the database at one go. But as you loop through the tracks
it sends a query to the database to get the album
title. It sends as many queries as there are Albums
in the system. This is very inefficient and this kind of performance issue are easier to miss.
1 2 3 4 5 6 7 8 9 10 11 | using (ChinookContext db = new ChinookContext()) { var Tracks = db.Track.ToList(); foreach (var p in Tracks) { Console.WriteLine("{0} {1}", p.Name, p.Album.Title); //Query is sent to DB here } } |
The EF Core will not load the same data again. In the above query, if an album is loaded once, it will not be queried again. Whether it is a good thing or a bad thing depends on the use case. For Example, in the above case, it is a good thing as it eliminates the duplication of the query. But if someone changes the data after you load the data, the EF will not refresh your data and you going to see only the old data.
Now, look at the following query. p.Album.Track.Count()
A Simple operation of requesting a count will send a query to get the album
& then a query to all the Tracks
before returning the count.
1 2 3 4 5 6 7 8 9 10 11 12 13 | using (ChinookContext db = new ChinookContext()) { var Tracks = db.Track.Take(3).ToList(); foreach (var p in Tracks) { Console.WriteLine("{0} {1}", p.Name, p.Album.Track.Count()); Console.WriteLine("Press any key to continue"); Console.ReadKey(); } } |
Serialization
Serialization and Lazy loading do not work well. Especially when you have properties that hold a reference to each other. For example, consider the following model.
When you try to serialize the Track
instance.
- The serializer starts to serialize the
Track
. - It reads the
Album
property. The EF Core will send a query to the database get theAlbum
- Serializer attempt to serialize the
Album
. - The
Album
contains the collectionTracks
. The EF Core Lazy loads theTracks
from the database. - Now Serializer attempts to read the each
Track
in theTracks
collection. - Each of those
Track
hasAlbum
and theAlbum
hasTracks
collection - The Loop goes on.
I followed your example but it doesn’t work, I guess that’s because you failed to add something important. My constructor that gets passed the ILazyLoader parameter is never called, hence my _lazyLoader is always null. Why the heck is literally every EF Core 6 example (regardless of topic) I have looked into on the inet incomplete and doesn’t work?