简体   繁体   中英

How do I load nested entities in entity framework when they cross reference each other?

This one is a bit complicated so I've created a sample project (instructions in the readme)

https://github.com/dominicshaw/EntityFrameworkNestingQuirk

I have the following code:

private async Task<(bool Found, Appraisal Appraisal)> Get(int id)
{
    var staff = await _context.Staff.FindAsync(3);

    _logger.LogInformation("Got Staff Object   : Staff {id} has manager set to {managerId} - running query for appraisal", staff.Id, staff.ManagerId);

    var appraisal =
        await _context.Appraisals
            .Include(ap => ap.Staff).ThenInclude(s => s.Manager)
            .Include(ap => ap.Staff).ThenInclude(s => s.SecondaryManager)
            .Where(ap => ap.Id == id)
            .SingleOrDefaultAsync();

    _logger.LogInformation("Appraisal Query Run: Staff {id} has manager set to {managerId} - run completed", staff.Id, staff.ManagerId);

    if (appraisal != null)
    {
        _logger.LogInformation("Appraisal->Staff->2ndManager->Manager={id} (We should NOT have this)", appraisal.Staff.SecondaryManager?.ManagerId);
        return (true, appraisal);
    }

    return (false, null);
}

The data is never written to and staff id 3 has two managers, both set at the point of the first logged line.

I then grab the appraisal for this staff member from the database - the query ef generates looks good and brings back both managers, however in running that query, I lose the manager id (which should be 1) on the staff object (3).

When the second log is hit, the managerid is null.

I note that the EF model has loaded the secondary manager of the manager even though I didn't ask it to (unless my ef query syntax is wrong?). The manager (1) is manager of both 2 and 3, so this is correct, but it should have been loaded into Appraisal->Staff->Manager, not Appraisal->Staff->SecondaryManager->Manager

Any ideas how I can resolve this?

The issue is that EF Core thinks that the Manager and SecondaryManager self-referencing relationships are one-to-one relationships, which further means it thinks it can only assign a given entity to a single navigational property.

The following OnModelCreating() configuration solved the issue for me locally. Only the commented lines are changed compared to the configuration you provided:

builder.Entity<Staff>().HasIndex(e => e.ManagerId).IsUnique(false);
    builder.Entity<Staff>()
        .HasOne(a => a.Manager)
        .WithMany() // Changed
        .HasForeignKey(s => s.ManagerId)
        .IsRequired(false)
        .OnDelete(DeleteBehavior.NoAction);
    
    builder.Entity<Staff>().HasIndex(e => e.SecondaryManagerId).IsUnique(false);
    builder.Entity<Staff>()
        .HasOne(a => a.SecondaryManager)
        .WithMany() // Changed
        .HasForeignKey(s => s.SecondaryManagerId)
        .IsRequired(false)
        .OnDelete(DeleteBehavior.NoAction);

Why is the nested Manager populated?

The reason for this is the 'relationship fix-up' behavior of EF Core, which means if you load an entity that is referenced by a navigation property, it automatically assigns that entity to the given navigation property.

Even if you run two completely separate queries, if there is a relational connection between the separately loaded entities, EF Core will connect them together in memory.

In this example, both the main Manager and the nested Manager references the same Staff entity with Id 1, so it would 'fix up' both in memory. But, it happens to 'fix up' the nested one first, and given the one-to-one constraint I suppose it simply stops there. Perhaps someone with a deeper understanding of the inner workings of EF Core will enlighten us with more details.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM