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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With