Lazy Vs Eager Loading With Entity Framework Core

Rasheed K Mozaffar - May 23 '23 - - Dev Community

...

Hello There! πŸ‘‹πŸ»

In this post, we'll look through the two techniques while doing data access, we'll also compare the 2 methods and their respective pros and cons, and see how we can use both of them

Here's a brief outline of today's agenda

1: Explaining lazy loading and eager loading
2: How lazy loading works + testing
3: How eager loading works + testing

Explaining lazy loading and eager loading

When I first heard lazy loading, I somehow thought that it's a slow way of doing something, but yeah, turns out lazy isn't always bad.
Lazy loading is a method of retrieving related data when it's demanded, while eager loading fetches all related data as part of the initial query using joins in the database when needed.

The demo

Say you have a data model like a book and the book reasonably should have an author and the author should have a list of publishings, a collection of books, right? So you'll have an author property inside the book class, and a books property in the author class, now if we use a demo app that I created, that's a basic web api with two models, Book and Author, which you can view their code here:




public class Book
{
    public int Id { get; set; }
    public int AuthorId { get; set; }
    public string Title { get; set; }
    public int NumberOfPages { get; set; }
    public Author Author { get; set; }
}

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Book> Books {get; set;}
}




Enter fullscreen mode Exit fullscreen mode

I'm keeping things simple here so we can concentrate on the main concept, now I created a basic GET endpoint that's ultimate purpose is to create some mock data and then return it to the caller, take a look at it:

The GET all books endpoint

Now if I run the api, and use a tool like Postman to make an HTTP request to that GET endpoint, it'll return 2 objects like the following:




[
    {
        "id": 1,
        "authorId": 1,
        "title": "Some Random Book",
        "numberOfPages": 200,
        "author": {
            "id": 1,
            "name": "Random Author 1"
        }
    },
    {
        "id": 2,
        "authorId": 2,
        "title": "Another Random Book",
        "numberOfPages": 300,
        "author": {
            "id": 2,
            "name": "Random Author 2"
        }
    }
]



Enter fullscreen mode Exit fullscreen mode

Intuitively, that's all good, but now if we create a database with two tables, one for books and one for authors, the outcome will change.
I've set up a data context class and registered it in the DI container so we can inject it in the controller, this's the content of it:

The content of the DB Context class

I've seeded the same data we had in the lists before to the database, and I modified the Books Controller to this:




public class BooksController : ControllerBase
{
    private readonly AppDbContext _context;

    public BooksController(AppDbContext context)
    {
        _context = context;
    }

    [HttpGet]
    public async Task<IActionResult> GetAllBooks()
    {
        var books = await _context.Books.ToListAsync();

        return Ok(books);
    }
}



Enter fullscreen mode Exit fullscreen mode

If I now run the api and make the call again, I'll get this:




{
     "id": 1,
     "authorId": 1,
     "title": "Some Random Book",
     "numberOfPages": 200,
     "author": null
},
//Second book removed for brevity



Enter fullscreen mode Exit fullscreen mode

Why is the author NULL?
That's the problem we have, Lazy and Eager Loading are the solutions. So, let's have a look at each one separately.

πŸ¦₯ How lazy loading works + Testing

Lazy loading in EF Core doesn't come enabled by default, and you have to set it up manually, fortunately, it's plain simple. Let's look at how it works first and then see the basic method of implementation.

Let's answer how it works
When you have an entity that has navigational properties, in our case, the book class has an Author property which is a navigational property, however, when we query for all the books, their authors aren't loaded, and thus they're null, by setting up lazy loading, every related piece of data to an entity is loaded when it's accessed, therefore the performance will be improved as the needed data will only be loaded when necessary and not as part of the initial query like the case with Eager loading which we'll soon dissect.

How do we set Lazy loading up?
As I mentioned earlier, it's plain simple, and it only takes a couple of lines to add.

Set up steps

1: Add this package to the project
Microsoft.EntityFrameworkCore.Proxies

You can do that using Nuget Package Manager if you're on the fully fledged Visual Studio or use the terminal if Vs Code is your editor of choice, this is the command though:

dotnet add package Microsoft.EntityFrameworkCore.Proxies

2: Make the navigational properties virtual
The virtual keyword is used so that an implementation could be overridden if needed, and in the case of Lazy loading, EF Core needs to override the properties so it can use its own implementation thus allowing lazy loading to be possible.

So change the Book & Author classes by sticking the virtual keyword before these 2 properties:




public class Book
{
    //Rest of the properties
    public virtual Author Author {get; set;}
}

public class Author
{
    //Rest of the properties
    public virtual List<Book> Books {get; set;}
}



Enter fullscreen mode Exit fullscreen mode

3: Modify the DB Context to enable lazy loading
To do that, we need to override a method called OnConfiguring, this method is used to configure the database and other options, like setting up the database provider and connection strings. We merely need couple lines of code to enable lazy loading, just add this to the context class:




protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseLazyLoadingProxies();
}



Enter fullscreen mode Exit fullscreen mode

4: Build DTOs to map the the data models to
If we were to run the api now and make a request, we'll get an error that says the following: A possible object cycle reference was detected.
To avoid this, we'll need to create two Data Transfer Objects of DTOs for short, one called BookDto and the other is AuthorDto. Create a new folder named Dtos and add these two classes to it:




public class BookDto
{
    public int Id { get; set; }
    public string Title { get; set; }
    public int NumberOfPages { get; set; }
    public AuthorDto Author { get; set; }
}

public class AuthorDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<BookDto> Books { get; set; }
}



Enter fullscreen mode Exit fullscreen mode

That's all we need for the DTOs, now we just need to do the mapping, for simplicity, we'll do that inside out GET endpoint using a simple foreach loop, like the following:




[HttpGet]
public async Task<IActionResult> GetAllBooks()
{
    var books = await _context.Books.ToListAsync();

    var booksAsDtos = new List<BookDto>();

    foreach (var book in books)
    {
        var bookDto = new BookDto
        {
            Id = book.Id,
            Title = book.Title,
            NumberOfPages = book.NumberOfPages,
            Author = new AuthorDto { Id = book.AuthorId, Name = book.Author.Name }
        };

        booksAsDtos.Add(bookDto);
    }

    return Ok(booksAsDtos);
}



Enter fullscreen mode Exit fullscreen mode

And that's it, we're done.

Now if I run the api and make a call to that same GET endpoint, here's what I'll get as a response:




[
    {
        "id": 1,
        "title": "Some Random Book",
        "numberOfPages": 200,
        "author": {
            "id": 1,
            "name": "Random Author 1",
            "books": null
        }
    },
    {
        "id": 2,
        "title": "Another Random Book",
        "numberOfPages": 300,
        "author": {
            "id": 2,
            "name": "Random Author 2",
            "books": null
        }
    }
]



Enter fullscreen mode Exit fullscreen mode

Voila, it's all working now. However, you may ask why is the books property null? Well, put bluntly, because we haven't added anything to it, we simply brought the books from the database, but we hadn't added them to the authors list of publishings initially. That's fixable through mapping the books of an author and selecting them as DTOs, but you get the point. So, let's look at eager loading right now.

⚑️ How eager loading works + Testing

In lazy loading, we learned that the related data is loaded when asked for or needed, on the other end, eager loading grabs all related data in one go as part of the initial query, thus reducing the number of total queries needed to achieve the same result. However, that could be costly regrading performance as sometimes it ends up fetching a lot of data that often times is superfluous and unnecessary.

Eager loading unlike lazy loading, doesn't need setting up, it's simply achieved using the Include method, back in our example, we'll use the Books and Authors we've seeded to our database.

I just removed the setup for lazy loading, and changed our query inside the GET endpoint from

var books = await _context.Books.ToListAsync();
To this:

var books = await _context.Books.Include(b => b.Author).ToListAsync();

And the rest of the endpoint implementation remains the same, now you can run the api and send a get request, you'll get the same response as the one we got with lazy loading.

βœ… That's about it for this post

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .