Search code examples
c#entity-frameworklinq

LINQ query in controller showing child as null in Detail view but not in Index view for self reference relationship


I have a self-referencing relationship for the Contacts model, e.g. a contact can have a relationship with another contact. In the Controller GET: Details method I have this code:

var contact = await _context.Contacts
                            .Include(r => r.RelationshipsAsParent)
                            .ThenInclude(t => t.RelationshipType)
                            .FirstOrDefaultAsync(m => m.Id == id);

Get Details LINQ

I can get the ChildId, but there should be the FullName of the Contact under the child.

The GET index query shows it as not null, so I can access the FullName there.

Index method:

return _context.Contacts != null 
           ? View(await _context.Contacts
                                .Include(r => r.RelationshipsAsParent)
                                .ThenInclude(t => t.RelationshipType)
                                .ToListAsync()) 
           : Problem("Entity set 'ApplicationDbContext.Contacts' is null.");

Get Index LINQ

I can't figure out why the Child is null for the Details method, but not for the Index method.

Thanks in advance for any help :)

Models

public class Contact
{
    public int Id { get; set; }

    public string? FirstName { get; set; }
    public string? LastName { get; set; }

    [NotMapped]
    public string? FullName { get { return $"{FirstName} {LastName}"; } }

    // Navigation properties for relationships where this contact is the parent
    public virtual List<Relationship>? RelationshipsAsParent { get; set; }

    // Navigation properties for relationships where this contact is the child
    public virtual List<Relationship>? RelationshipsAsChild { get; set; }
}

public class Relationship
{
    public int Id { get; set; }

    public int? ParentId { get; set; }
    public virtual Contact? Parent { get; set; }

    // Foreign key and navigation property for the child contact
    public int? ChildId { get; set; }
    public virtual Contact? Child { get; set; }

    // Navigation property for the relationship type (assuming there's a RelationshipType entity)
    public int? RelationshipTypeId { get; set; }
    public virtual RelationshipType? RelationshipType { get; set; }
}

public class RelationshipType
{
    public int Id { get; set; }
    public string? Type { get; set; }

    public virtual List<Relationship>? Relationships { get; set; } = new List<Relationship>();
}

Fluent API

modelBuilder.Entity<Relationship>(b =>
        {
            modelBuilder.Entity<Relationship>()
            .HasOne(r => r.Parent)
            .WithMany(c => c.RelationshipsAsParent)
            .HasForeignKey(r => r.ParentId)
            .OnDelete(DeleteBehavior.Restrict);

            modelBuilder.Entity<Relationship>()
            .HasOne(r => r.Child)
            .WithMany(c => c.RelationshipsAsChild)
            .HasForeignKey(r => r.ChildId)
            .OnDelete(DeleteBehavior.Restrict);
        });

Detail view

<h3>Relationships</h3>
    <ul>
        @if (@Model.RelationshipsAsParent.Count > 0)
        {
            @for (int j = 0; j < Model.RelationshipsAsParent.Count; j++)
            {
                <li>@Model.RelationshipsAsParent[j].ChildId: @Model.RelationshipsAsParent[j].RelationshipType.Type</li>
            }
        }
        else
        {
            <li>No Relationships</li>
        }
    </ul>

Index view

<div class="row ms-5">
  <ul>
  @if (item.RelationshipsAsParent != null)
    {
    @for (int j = 0; j < item.RelationshipsAsParent.Count; j++)
    {
    <li>@item.RelationshipsAsParent[j].Child.FullName: @item.RelationshipsAsParent[j].RelationshipType.Type</li>
    }
    }
    else
   {
    <li>No Relationships</li>
   }
  </ul>
</div>

Solution

  • This is likely because the "child" was loaded as a byproduct of the Index's ToList call. When you load an individual record with a DbContext instance and do not have lazy loading enabled then the context will fetch only what you eager load with Include.

    For example, if I have a set of records, A, B, and C, where A is a parent of B and C is a child of B, and I load that list of records:

    var records = _context.Records.ToList();
    

    When I look at Record B, I will see that it's parent and child are populated because when the context loads all of the applicable entities, and populates the references.

    In a detail view for record B, if I execute:

    var record = _context.Records
        .Include(x => x.Parent)
        .Single(x => x.Id == "B");
    

    This would fetch record B, and it's parent (A), but the child "C" will not be returned, we didn't request it. The behavior can be inconsistent because you may find sometimes the child does get loaded, while other times it doesn't. It depends if the DbContext happens to be tracking the reference or not. If we had some code that resulted in "C" getting loaded then it would be populated. For example:

    var temp = _context.Records.Single(x => x.Id == "C");
    
    // Some other logic....
    
    var record = _context.Records
        .Include(x => x.Parent)
        .Single(x => x.Id == "B");
    

    Here we don't reference the temp record C, and we don't eager load it when fetching "B" but because the DbContext is tracking the instance, when it populates our "B" record, it will fill in the child reference pointing at "C".

    If you are loading data and expect a reference to be available you should eager load it.So when reading a "B" and you want its parent and its children, include them in the query or better, use projection to fetch the data you want into a ViewModel or ViewModel graph. (set of related view models) Projection helps minimize the data being pulled and you don't need to worry at all about what gets eager loaded. (You do not use Include when using Select or ProjectTo (Automapper)).

    Edit: Regarding projection: Say you want to display a contact, and its parents and children..

    public class ContactViewModel { int Id { get; set; } string Name { get; set; }

    public ICollection<ContactViewModel> Parents { get; set; } = new List<ContactViewModel>();
    public ICollection<ContactViewModel> Children { get; set; } = new List<ContactViewModel>();
    

    }

    This looks similar to an entity, but it is a model that will serve a view or a consumer. (If not for a view, could call it ContactDTO for instance) These projections only care about what the consumer needs and can transform/flatten etc. down from the entities that populate them. Then to read the data:

    var contactVMs = _context.Contacts
        .Where(x => x.Id == contactId)
        .Select(x => new ContactViewModel
        {
           Id = x.Id,
           Name = x.FirstName + " " + x.LastName, // Note we cannot use NotMapped columns here.
           Parents = x.RelationshipsAsChild
               .Select(r => new ContactViewModel
               {
                   Id = r.Parent.Id,
                   Name = r.Parent.FirstName + " " + r.Parent.LastName
               }).ToList();
           Children = x.RelationshipsAsParent
               .Select(r => new ContactViewModel
               {
                   Id = r.Child.Id,
                   Name = r.Child.FirstName + " " + r.Child.LastName
               }).ToList();
       }).Single();        
    

    Note that in this case we are only populating one level. For the contact VM collection for Parents and Children we do not populate the parents or children of each of those. When dealing with self-referncing structures in entities code like serializers and such can get tripped up by circular references. Projection can help avoid those scenarios. Mapping libraries can be set up with rules on populating ViewModels/DTOs, though in those cases you might want to use a separate view model for the contact and related contact. You may prefer that in any case as opposed to leaving Parents/Children empty for relations:

    public class ContactViewModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    public class ContactWithRelationsViewModel : ContactViewModel
    {
        public ICollection<ContactViewModel> Parents { get; set; } = new List<ContactViewModel>();
        public ICollection<ContactViewModel> Children { get; set; } = new List<ContactViewModel>();
    }
    

    One other detail if this is still running into issues populating or consuming the entities. The use of nullable references might cause some issues. Consider avoiding the nullable references especially on the lists and initializing them insted:

    public virtual ICollection<Relationship> RelationshipsAsParent { get; protected set; } = new List<Relationship>();
    
    // Navigation properties for relationships where this contact is the child
    public virtual ICollection<Relationship> RelationshipsAsChild { get; protected set; } = new List<Relationship>();
    

    When it comes to entities you don't want code new-ing a collection reference, or overwriting it. Protect the setter to discourage overwriting the reference, and initialize it to a new List/HashSet so it is ready to receive values. For singular references, null-able is fine for optional associations. For required ones where the FK is not null-able, there are a few options. For instance is a relationship type optional? if not:

    public int RelationshipTypeId { get; set; }
    
    public required RelationshipType? RelationshipType { get; set; } 
    // or
    public RelationshipType? RelationshipType { get; set; } = null!; // forgiveness
    
    // or
    
    public required RelationshipType RelationshipType { get; set; } 
    
    
    public Relationship(RelationshipType relationshipType) 
    {
         ArgumentException.ThrowIfNull(relationshipType, nameof(relationshipType));
         RelationshipType = relationshipType;
    }
    
    #pragma disable CS8618 -- Constructor used by EF. If you don't use public constructor initialization for new instances, the below would be the default public constructor
    protected Relationship() {}
    #pragma restore CS8618
    

    ... some options to choose from rather than making everything null-able to keep the compiler happy.