Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework Core Two Foreign Keys - Same Table

I'm having problems with having two foreign key references to the same table. The foreign key id fields are populated but the navigation fields and lists (the Team fields) are not - they are both null.

My classes are:

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

    public virtual ICollection<Fixture> HomeFixtures { get; set; }
    public virtual ICollection<Fixture> AwayFixtures { get; set; }
}

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

    public int HomeTeamId { get; set; }
    public int AwayTeamId { get; set; }

    public Team HomeTeam { get; set; }
    public Team AwayTeam { get; set; }
}

and my dbContext

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public DbSet<Team> Teams { get; set; }
    public DbSet<Fixture> Fixtures { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Fixture>()
            .HasOne(f => f.HomeTeam)
            .WithMany(t => t.HomeFixtures)
            .HasForeignKey(t => t.HomeTeamId)
            .OnDelete(Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior.Restrict);

        modelBuilder.Entity<Fixture>()
            .HasOne(f => f.AwayTeam)
            .WithMany(t => t.AwayFixtures)
            .HasForeignKey(t => t.AwayTeamId)
            .OnDelete(Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior.Restrict);
    }
}

I have tried adding [ForeignKey()] attributes to the HomeTeam and AwayTeam properties but it has no effect. I have also tried changing the OnModelCreating method to work the other way, i.e.

modelBuilder.Entity<Team>()
    .HasMany(t => t.HomeFixtures)
    .WithOne(f => f.HomeTeam)
    .HasForeignKey(f => f.HomeTeamId)
    .OnDelete(Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior.Restrict);

and the same for away fixtures but this produces identical behaviour.

It doesn't seem to matter how I query but the simplest case is

Fixture fixture = await _context.Fixtures.SingleOrDefaultAsync(f => f.Id == id);

The returned fixture object contains team Ids that are valid and in the database but the Team objects are still not populated.

Has anyone any idea what I'm doing wrong? This is a brand new project and a brand new database so there's no legacy code interfering. I'm using Visual Studio 2017rc with Entity Framework Core.

like image 316
Mog0 Avatar asked Oct 30 '22 11:10

Mog0


1 Answers

At present EF Core does not support lazy loading. Tracking issue here

That means by default navigation properties will not be loaded and will remain null. As a work-around you can use eager loading or explicit loading patterns.

Eager Loading

Eager loading is pattern where you request for the referenced data you need eagerly while running the query using Include API. The usage is somewhat different from how it worked in EF6. To include any navigation, you specify the lambda expression (or string name) in include method in your query.

e.g. await _context.Fixtures.Include(f => f.HomeTeam).FirstOrDefaultAsync(f => f.Id == id);

Presently, filtered include is not supported so you can request to load navigation fully or exclude it. So the lambda expression cannot be complex. It must be simple property access. Also to load nested navigation, you can either chain them with property access calls (like a.b.c) or when its after collection navigation (since you cannot chain them) use ThenInclude.

e.g. await _context.Fixtures.Include(f => f.HomeTeam).ThenInclude(t=> t.HomeFixtures).FirstOrDefaultAsync(f => f.Id == id);

It would be good to remember that include represent a path of navigation from the entity type its being called on to populate all naviagations on the path. Often you may need to write repeated calls if you are including multiple navigations at 2nd or further level. That is just for syntax though and query will be optimized and will not do repeated work. Also with string include you can specify whole navigation path without needing to use ThenInclude. Since reference navigation can utilize join to fetch all data needed in single query, & collection navigation can load all related data in single query, eager loading is most performant way.

Explicit Loading

When you have loaded object in the memory and need to load a navigation, while lazy loading would have loaded it while accessing navigation property, in the absence of that you need to call Load method by yourself. These methods are defined on ReferenceEntry or CollectionEntry.

e.g.

Fixture fixture = await _context.Fixtures.SingleOrDefaultAsync(f => f.Id == id);
_context.Entry(fixture).Reference(f => f.HomeTeam).Load();

var team = await _context.Teams.SingleOrDefaultAsync(t => t.Id == id);
_context.Entry(team).Collection(f => f.HomeFixtures).Load();

For reference navigation you would need Reference on EntityEntry to get ReferenceEntry. For collection navigation equivalent method is Collection. Then you just call Load method on it to load the data in the navigation. There is also async version of LoadAsync if you need.

Hope this helps.

like image 184
Smit Avatar answered Nov 15 '22 05:11

Smit