Avoid Lazy Loading in ASP.NET


Sad CatI know I am not going to make everyone happy with this post. I've been hoping to not have to make this post, but Entity Framework Core has finally added support for Lazy Loading, so it's time.

This problem is not new. Entity Framework (not Core) also has this problem. But it's far easier to accidentally do this in that version. Luckily, Entity Framework Core has made it harder to inadvertently turn it on. Let's see what's wrong with Lazy Loading in Web Apps.

What is Lazy Loading?

Just to make sure were talking about the same things, I'll explain how Lazy Loading works.

Let's assume we have a simple data-model:

  public class Person
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual Address Address { get; set; }
  }
  
  public class Address
  {
    public int Id { get; set; }
    public string FullAddress { get; set; }
  }

Note the virtual property that is a relationship to the Address entity. That's part of the magic that makes Lazy Loading work. But we'll get there in a minute (or however fast you read). You can query it by just asking for the entity from the database:

var ctx = new SomeContext();
var person = ctx.Persons.Where(p => p.Id == 1).FirstOrDefault();
return person;

This is a simple query, and it returns a Person with the Id of 1. But it doesn't automatically get the Address associated with the Person. You didn't ask for the Address. In fact, the Address would be NULL whether it existed or not in the data store. Without Lazy Loading, you'd have to Eager Load the data to get the Address:

var ctx = new SomeContext();
var person = ctx.Persons
                .Include(p => p.Address) // Eager Loading
                .Where(p => p.Id == 1).FirstOrDefault();
return person;

Here you would get the entire object graph including the Address.

But in Lazy Loading, it works a bit differently. With Lazy Loading enabled, what is returned from the first query is a Proxy object. It's a temporary type that is derived from your Entity class. It does this by overriding the property (which is why it needs to be virtual) so that when you call it, it can issue a query through the connected context to get the Address.

This is super-helpful so you don't have to know what parts of a data-model you need, so you get them when you ask them. But this power comes with a lot of responsibility.

Why Not Use Lazy Loading in ASP.NET?

You can leverage Lazy Loading in a web app without doing it wrong. Lots of people have successfully used Lazy Loading. I just think it's trouble.

So let me explain why I think Lazy Loading is cool outside a web app. When you're building an app on Entity Framework where you have long-lived objects, pushing the data access for related data to only when you need it makes a ton of sense. It wouldn't be smart to get the Address in our example until you needed it. And Lazy Loading makes that super easy and efficient.

But web apps, by their very nature, live within the scope of a request. Requests should be super short on purpose (often < 1 second). So the benefit of delay loading goes away. You can make a web site functional using Lazy Loading, but I'd prefer the data abstraction (e.g. Repository Pattern, CQRS layer) to use the shape of the data as the contract.

Let's see how easy it is to get it wrong. In my example posted here on GitHub, the controller very innocently queries to show all the Persons in the data store on a view:

  public class HomeController : Controller
  {
    private readonly LazyContext _ctx;

    public HomeController(LazyContext ctx)
    {
      _ctx = ctx;
    }
    public IActionResult Index()
    {
      return View(_ctx.People.ToList());
    }
    
    ...

In the view, you just add the @model and you can show data:

@model IEnumerable<DangersOfLazyLoadingInASPNETCore.Data.Person>
...
    <ul class="list-unstyled">
      @foreach (var p in Model)
      {
        <li>@p.Name</li>
      }
    </ul>

All good, nothing bad here. But then a new requirement comes in to add the Address:

    <ul class="list-unstyled">
      @foreach (var p in Model)
      {
        <li>@p.Name : @p.Address.FullAddress</li>
      }
    </ul>

Should be a simple fix. Unknowingly you just added a round-trip to the database without noticing. I only have three records in my example, but already it's doing too much (from the log):

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (119ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [p].[Id], [p].[AddressId], [p].[Name]
      FROM [People] AS [p]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (193ms) [Parameters=[@__get_Item_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SELECT [e].[Id], [e].[FullAddress]
      FROM [Address] AS [e]
      WHERE [e].[Id] = @__get_Item_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (12ms) [Parameters=[@__get_Item_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SELECT [e].[Id], [e].[FullAddress]
      FROM [Address] AS [e]
      WHERE [e].[Id] = @__get_Item_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (16ms) [Parameters=[@__get_Item_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SELECT [e].[Id], [e].[FullAddress]
      FROM [Address] AS [e]
      WHERE [e].[Id] = @__get_Item_0

Sure, it should be a fast query, but it's adding load to the database server and slowing down the page. The more time you spend on the server, the worse it is. The fix here would be to simply add the eager loading. But that wouldn't be an issue if you didn't have Lazy Loading enabled at all. You'd simply get an error in development, way before you had to dig into a performance problem later on.

You can get the code that shows this work (and see how EFCore 2.1 enables lazy loading) here:

Dangers of Lazy Loading on GitHub

The dangers I talk about are true for Entity Framework 4-6 as well as Entity Framework Core 2.1, but luckily, EFCore 2.1 doesn't make Lazy Loading the default (unlike the older version). In fact, there are a couple of hoops you need to jump through to enable it so you won't be lazy loading by accident. I like this approach.

I'm ready for the flames, come get me in the comments!


Bootstrap 4 is Here!

After a long development cycle, Bootstrap has been completely re-written to improve performance and be more consistent. Learn Bootstrap 4 now with my Wilder Minds course:

Enroll Today


Shawn
Shawn Wildermuth
Author, Teacher, and Coach




My Courses

Wilder Minds Training
Vue.js by Example (Now Available)
Bootstrap 4 by Example
Intro to Font Awesome 5 (Free Course)
Pluralsight
Less: Getting Started (Coupon Available)
Building a Web App with ASP.NET Core, MVC6, EF Core, Bootstrap and Angular (updated for 2.1)
Using Visual Studio Code for ASP.NET Core Projects
Implementing ASP.NET Web API
Web API Design

Application Name WilderBlog Environment Name Production
Application Ver v4.0.30319 Runtime Framework x86
App Path D:\home\site\wwwroot\ Runtime Version .NET Core 4.6.26919.02
Operating System Microsoft Windows 10.0.14393 Runtime Arch X86