Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EF Core Eager Loading nested collections

I'm trying to load a related modal in Entity Framework Core but for some reason there's a nested collection being loaded when I haven't asked for it in my Include() call.

Here's my two models -

Driver.cs

public partial class Driver : IBaseEntity
{
    public short DriverId { get; set; }
    public string Surname { get; set; }
    public string Initials { get; set; }
    public byte DriverStatusTypeId { get; set; }

    public DriverStatusType DriverStatusType { get; set; }
}

DriverStatusType.cs

public partial class DriverStatusType
{
    public DriverStatusType()
    {
        Drivers = new HashSet<Driver>();
    }

    public byte DriverStatusTypeId { get; set; }
    public string DriverStatusTypeName { get; set; }
    public string Description { get; set; }

    public ICollection<Driver> Drivers { get; set; }
}

DriversService.cs

public class DriverService : IDriverService
{
    public DriverService(MyContext context)
    {
        Context = context;
    }

    public MyContext Context { get; }

    public async Task<IEnumerable<Driver>> GetAllDrivers()
    {
        var drivers = await Context
            .Drivers
            .Include(d => d.DriverStatusType)
            .toListAsync();

        return drivers;
    }

    public async Task<Driver> GetDriverById(int id)
    {
        var driver = await Context
            .Drivers
            .Include(d => d.DriverStatusType)
            .Where(d => d.DriverId == id)
            .FirstOrDefaultAsync();

        return driver;
    }
}

Now when I call the GetDriverById(int id) method from my controller I get back what I'm expecting -

{
    "driverId": 1,
    "surname": "Stark",
    "initials": "T",
    "driverStatusTypeId": 2,
    "driverStatusType": {
        "driverStatusTypeId": 2,
        "driverStatusTypeName": "Available",
        "description": "This driver is available",
        "drivers": []
    }
}

However the GetAllDrivers() method is returning the nested drivers collection which means the data I'm getting back is huge -

[
    {
        "driverId": 1,
        "surname": "Stark",
        "initials": "T",
        "displayText": "Tony Stark",
        "driverStatusTypeId": 2,
        "driverStatusType": {
            "driverStatusTypeId": 2,
            "driverStatusTypeName": "Available",
            "description": "This driver is available",
            "drivers": [
                {
                    "driverId": 2,
                    "surname": "Rogers",
                    "initials": "S",
                    "driverStatusTypeId": 2
                },
                {
                    "driverId": 3,
                    "surname": "Romanoff",
                    "initials": "N",
                    "driverStatusTypeId": 2
                },
                {
                    "driverId": 4,
                    "surname": "Banner",
                    "initials": "B",
                    "driverStatusTypeId": 2
                },
                ...

I thought the idea of eager loading was to only include the related models you specify in the include statement but it seems this is not the case. Could someone explain what's happening here?

like image 559
Chris Edgington Avatar asked Nov 26 '18 09:11

Chris Edgington


1 Answers

I thought the idea of eager loading was to only include the related models you specify in the include statement but it seems this is not the case. Could someone explain what's happening here?

You are right, that's not the case. The idea of eager loading is to ensure the related data you specify is loaded. It doesn't mean/guarantee that related data won't be included.

It's partially explained in the Loading Related Data section of the EF Core documentation:

Tip

Entity Framework Core will automatically fix-up navigation properties to any other entities that were previously loaded into the context instance. So even if you don't explicitly include the data for a navigation property, the property may still be populated if some or all of the related entities were previously loaded.

The navigation property fix-up means that anytime entity instance is materialized, all related navigation properties are updated to reflect it, for instance Driver is added to Driver.DriverStatusType.Drivers and vice versa.

Note that when using tracking queries, this might happen after the non including query is materialized (ToList()) because change tracker keeps track of object references and automatically updates them when you perform other tracking queries.

Another effect of that fix-up process is that when you include one of the ends of the relationship, the inverse navigation property of the other end is automatically populated.

So even if the first case the Drivers property should be populated and contain single item. And this is what actually happening in my clean tests, don't know why you are getting difference - may be the serializer is hiding it?

Anyway, all that means that you can't really control the content of the navigation properties. The only way you can control exactly what are you returning is to use special DTO/ViewModel etc. classes and projection (Select).

like image 70
Ivan Stoev Avatar answered Oct 27 '22 04:10

Ivan Stoev