Why does EF Core One-to-Many relationship collection return null?

Viewed 3435

This may seem like a duplicate question EF Core One-to-Many relationship list returns null, but the answer to that question didn't help me. My situation:

public class Section
{
    public int Id { get; set; }
    // ...
    public IEnumerable<Topic> Topics { get; set; }
}

public class Topic
{
    public int Id { get; set; }
    // ...
    public int SectionId { get; set; }
    public Section Section { get; set; }
}

But I have not implemented the OnModelCreating method in DbContext because in that case, errors occurs with users identity. There are topics in the database with the specified SectionId. But no matter how I try to get the section, I get null in the Topics property. For example:

var section = _dbContext.Sections.Include(s => s.Topics).FirstOrDefault(s => s.Id == id);

What is the reason for this problem? Have I declared something wrong? Or maybe there is a problem in creating a topic?

UPDATE

I tried to override the OnModelCreating method this way:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Section>()
    .HasMany(s => s.Topics)
    .WithOne(t => t.Section);

    base.OnModelCreating(modelBuilder);
}

And this way:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Section>()
    .HasMany(s => s.Topics)
    .WithOne(t => t.Section)
    .HasForeignKey(prop => prop.SectionId);

    base.OnModelCreating(modelBuilder);
}

I also added the virtual attribute to the dependencies again. It did not help. Added a test migration (thought there might be something wrong with the database structure), but the migration was empty.

SOLUTION

As a result, I solved the problem with a crutch:

var section = _dbContext.Sections.Include(s => s.Topics).FirstOrDefault(s => s.Id == id);

if (section == null)
{
    return Error();
}

section.Topics = _dbContext.Topics.Where(t => t.SectionId == section.Id).Include(t => t.Author).ToList();

foreach(var topic in section.Topics)
{
    topic.Author = _dbContext.Users.FirstOrDefault(u => u.Id == topic.AuthorId);
    topic.Posts = _dbContext.Posts.Where(t => t.TopicId == topic.Id).ToList();
}

As you can see, I had to explicitly get data from the dbContext and assign them to the appropriate properties. Include method calls can be deleted because they do not perform the desired action.

2 Answers

Following the guide on this link https://www.learnentityframeworkcore.com/lazy-loading

  1. Install the Microsoft.EntityFrameworkCore.Abstractions package into the project containing your model classes:
    [Package Manager Console]
    install-package Microsoft.EntityFrameworkCore.Abstractions

    [Dotnet CLI]
    add package Microsoft.EntityFrameworkCore.Abstractions
  1. Alter the principal entity to include
  • a using directive for Microsoft.EntityFrameworkCore.Infrastructure
  • a field for the ILazyLoader instance
  • an empty constructor, and one that takes an ILazyLoader as a parameter (which can be private, if you prefer)
  • a field for the collection navigation property
  • a getter in the public property that uses the ILazyLoader.Load method
using Microsoft.EntityFrameworkCore.Infrastructure;

public class Author
{
    private readonly ILazyLoader _lazyLoader;

    public Author()
    {
    }

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

    private List<Book> _books;

    public int AuthorId { get; set; }

    public List<Book> Books
    {
        get => _lazyLoader.Load(this, ref _books);
        set => _books = value;
    }
}

The solution in the answer will not work in case of many-to-many relationship.

Several suggestions:

  1. Try to make your Section define Topics as ICollection rather than IEnumerable and virtual so that they can be lazy loaded if necessary
public class Section
{
    public int Id { get; set; }
    // ...
    public virtual ICollection<Topic> Topics { get; set; }
}
  1. In your solution sample, you should be able to leverage EF Core's ThenInclude method to get the Sections, Topics, and Authors in one fell swoop:
var section = _dbContext.Sections
    .Include(s => s.Topics)
    .ThenInclude(t => t.Authors)
    .FirstOrDefault(s => s.Id == id);

However to get the collection of authors and posts in the same child collection, you may want to consider a custom projection into a new type. EF Core 3.1 should piece all of this into a single query. Earlier versions of Core would break it apart into multiple database queries depending on the version and exact syntax you used. Something like:

var items = 
    from section in _dbContext.Sections
    from topic in section.Topics
    select new {
       section.Name,
       topic.Description,
       Authors = topic.Authors.ToList(),
       Posts = topic.Posts.ToList()
    };
Related