Gabe
Gabe

Reputation: 63

EF Core is lazy loading when I try to eager load with .Include()

I'm using EF Core 2.2.6 (database first) and it seems like just having lazy loading enabled keeps me from being able to eager load. Does enabling lazy loading preclude using eager loading in any capacity?

namespace Example.Models
{
    public class Lead
    {
        public int Id { get; set; }
        public LeadOrganization LeadOrganization { get; set; }

        public Lead(ExampleContext.Data.Lead dbLead)
        {
            Id = dbLead.Id;
            LeadOrganization = new LeadOrganization(dbLead.LeadOrganization);
        }

        public static Lead GetLead(int id)
        {
            using (var db = new ExampleContext())
            {
                var dbLead = db.Leads
                    .Include(l => l.LeadOrganization)
                        .ThenInclude(lo => lo.LeadOrganizationAddresses)
                            .ThenInclude(loa => loa.AddressType)
                    .FirstOrDefault(l => l.Id== id);

                return new Lead(dbLead);
            }
        }
    }
}
namespace Example.Models
{
    public class LeadOrganization
    {
        public IEnumerable<LeadOrganizationAddress> Addresses { get; set; }

        public LeadOrganization(ExampleContext.Data.LeadOrganization dbLeadOrganization)
        {
            Addresses = dbLeadOrganization.LeadOrganizationAddresses.Select(loa => new LeadOrganizationAddress(loa));
        }
    }
}
namespace Example.Models
{
    public class LeadOrganizationAddress
    {
        public AddressType AddressType { get; set; }

        public LeadOrganizationAddress(ExampleContext.Data.LeadOrganizationAddress dbLeadOrganizationAddress)
        {
            AddressType = new AddressType(dbLeadOrganizationAddress.AddressType);
        }
    }
}
namespace Example.Models
{
    public class AddressType
    {
        public short Id { get; set; }

        public AddressType(ExampleContext.Data.AddressType dbAddressType)
        {
            Id = dbAddressType.Id;
        }
    }
}

The ExampleContext.Data namespace contains the EF-generated partial classes from the database. Lead, LeadOrganization, LeadOrganizationAddress, and AddressType are classes that are basically 1:1 with the partials in terms of properties, but with static methods added (yes it's weird, but it's what I have to work with).

A Lead has a LeadOrganization, which in turn has at least one LeadOrganizationAddress, which in turn has an AddressType.

When GetLead calls the Lead constructor, the data from the query has not loaded, even though it should be eager loaded. This leads to problems down the line of nested objects. When it eventually gets to the LeadOrganizationAddress constructor, the DbContext has been disposed, and thus can't lazy load the associated AddressType.

Am I misunderstanding the whole point of eager loading? I thought it would retrieve all the data upon the initial query, letting me then pass that to the constructor without issue. I shouldn't need to keep going back to the database and lazy load anything.

Can you simply not eager load if you have lazy loading enabled? Is there some other workaround like forcing it to load any proxied entities?

Upvotes: 3

Views: 4720

Answers (3)

Ivan Stoev
Ivan Stoev

Reputation: 205629

Ok, after investigating the issue, there is a problem with EF Core 2.x lazy loading via proxies implementation. The related tracked issues are

The problem is that the navigation properties are eager loaded, but LazyLoader does not know that when disposed - can't safely access context change tracker and simply is throwing exception. The relevant code can be seen here, in the very first line:

if (_disposed)
{
    Logger.LazyLoadOnDisposedContextWarning(Context, entity, navigationName);
}

As I read it, it's supposed to be fixed in EF Core 3.0 when released with the following "breaking change" - Lazy-loading proxies no longer assume navigation properties are fully loaded. It also partially explains the current problem:

Old behavior

Before EF Core 3.0, once a DbContext was disposed there was no way of knowing if a given navigation property on an entity obtained from that context was fully loaded or not.

Unfortunately this doesn't help you with the current problem. The options I see are:

  1. Wait for EF Core 3.0 release
  2. Don't use lazy loading via proxies
  3. Turn off the lazy loading on disposed context warning - by default it is Throw, change it to Log or Ignore, for instance:

    optionsBuilder.ConfigureWarnings(warnings => warnings
        .Log(CoreEventId.LazyLoadOnDisposedContextWarning)
    );
    

Upvotes: 3

kreadyf
kreadyf

Reputation: 510

I assume you use UseLazyLoadingProxies() but want to disable the lazy loading for specific Includes in your queries. This is not implemented yet:

https://github.com/aspnet/EntityFrameworkCore/issues/10787

The only thing what you can do right now:

1.) Disable the lazy-loading proxy ("default lazy loading for all properties")

2.) Then use (manual implemented) lazy-loading for specific properties, for example in one of your cases:

public class LeadOrganization
{
    private ILazyLoader _lazyLoader { get; set; }

    private IEnumerable<LeadOrganizationAddress> _addresses;

    public LeadOrganization(ILazyLoader lazyLoader)
    {
        _lazyLoader = lazyLoader;
    }

    public IEnumerable<LeadOrganizationAddress> Addresses
    {
        get => _addresses;
        set => _addresses = value;
    }

    public IEnumerable<LeadOrganizationAddress> AddressesLazy
    {
        get
        {
            _lazyLoader?.Load(this, ref _addresses);
        }
        set => this._addresses = value;
    }
}

So for eager-loading use .Include(lo=>lo.Addresses), for lazy-loading use .Include(lo=>lo.AddressesLazy)


Edit 1

IMO lazy loading shouldn't be enabled per default for all properties - this could impact the performance of your whole implementation. So the solution above is one alternative in cases where lazy loading brings you advantages. I also would like to have this option in every include, something like .Include(o=>o.Addresses, LoadingBehaviour.Eager) - maybe this will exist in the future.

Upvotes: 2

Saeb Amini
Saeb Amini

Reputation: 24410

Lazy loading isn't what's preventing the instantiation of your properties, lacking a proper constructor in them is.

EF Core aside, it's very strange that those types, e.g. LeadOrganization need to be passed in an instance of themselves in their constructor. It's kind of a chicken and egg problem – how do you create the first one?

public class LeadOrganization
{
    public IEnumerable<LeadOrganizationAddress> Addresses { get; set; }

    public LeadOrganization(ExampleContext.Data.LeadOrganization dbLeadOrganization)
    {
        Addresses = dbLeadOrganization.LeadOrganizationAddresses.Select(loa => new LeadOrganizationAddress(loa));
    }
}

In any case, EF Core only supports simple constructors with parameters based on convention (basically a 1-1 mapping of parameters to properties), so it doesn't know how to instantiate and hydrate those nested classes, eager or lazy.

I suggest making those constructors parameterless, or if you want EF to hydrate the objects via properties, at least add a parameterless constructor to your classes.

Upvotes: 0

Related Questions