EntityFramework Code First: Why does Lazy Loading of Many to Many Reference fail?
My EF 4.1 Code First model should be fairly self-explanitory if you look at the code below and am trying to use DataAnnotations whenever I can.
A franchise sells many items. A franchise has many stores that can sell the items. A store may be sold out of an item.
A many to many table called SoldOutItems allows for a store to have many sold out items, and an item to be sold out at many stores.
Problem:
The navigation properties associated with the many to many table won't lazy load for me like a normal navigation property would. It was possible to do when I used EntityFramework in a Database first approach with the same exact database that generated by the model below.I assume I'm just doing something wrong with the [InverseProperty] DataAnnotations, but everything I search for online just states that as long as the navigation property is virtual, it should be 'lazy loadable'.
At the very bottom, I've provided code to reproduce the issue in a console app.
Franchises
[Table("Franchises")]
public class Franchise
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public Int64 Id { get; set; }
[Required, MaxLength(50)]
public string Name { get; set; }
[InverseProperty("Franchise")]
public virtual ICollection<Store> Stores { get; set; }
[InverseProperty("Franchise")]
public virtual ICollection<Item> Items { get; set; }
}
Stores
[Table("Stores")]
public class Store
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public Int64 Id { get; set; }
public Int64 FranchiseId { get; set; }
[Required, MaxLength(50)]
public string Name { get; set; }
[ForeignKey("FranchiseId")]
public virtual Franchise Franchise { get; set; }
public virtual ICollection<Item> UnavailableItems { get; set; }
}
Items
[Table("Items")]
public class Item
{
[Key]
public Int64 Id { get; set; }
public Int64 FranchiseId { get; set; }
[Required, MaxLength(50)]
public string Name { get; set; }
[ForeignKey("FranchiseId")]
public virtual Franchise Franchise { get; set; }
public virtual ICollection<Store> StoresUnavailableAt { get; set; }
}
SoldOutItems
[Table("SoldOutItems")]
public class SoldOutItem
{
[Key, Column(Order = 0)]
public Int64 StoreId { get; set; }
[Key, Column(Order = 1)]
public Int64 ItemId { get; set; }
[InverseProperty("UnavailableItems")]
[ForeignKey("StoreId")]
public virtual Store Store { get; set; }
[InverseProperty("StoresUnavailableAt")]
[ForeignKey("ItemId")]
public virtual Item Item { get; set; }
}
Context
public class RetailContext : DbContext
{
public DbSet<Franchise> Franchises { get; set; }
public DbSet<Store> Stores { get; set; }
public DbSet<Item> Items { get; set; }
public DbSet<SoldOutItem> SoldOutItems { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<SoldOutItem>().HasRequired(s => s.Store).WithMany().WillCascadeOnDelete(false);
base.OnModelCreating(modelBuilder);
}
}
Code to reproduce null exception that gets thrown:
static void Main(string[] args)
{
RetailContext ctx = new RetailContext();
ctx.Configuration.LazyLoadingEnabled = true;
Franchise franchise = ctx.Franchises.Where(f => f.Id == 0).FirstOrDefault();
if (franchise == null)
{
franchise = new Franchise() { Id = 0, Name = "My Franchise" };
ctx.Franchises.Add(franchise);
ctx.SaveChanges();
}
Item item = ctx.Items.Where(i => i.Name == "Item 1").FirstOrDefault();
if (item == null)
{
item = new Item() { Name = "Item 1" };
franchise.Items.Add(item);
}
Store myStore = ctx.Stores.Where(s => s.Id == 0).FirstOrDefault();
if (myStore == null)
{
myStore = new Store() { Id = 1, Name = "My Store" };
myStore.UnavailableItems.Add(item); // Exception: UnavailableItems is null
}
}
Solution
kwon's answer does indeed fix the immediate problem I was having. However, a much bigger problem for me is that it seems I cannot get what I want simply using DataAnnotations. So I ended up removing my Junction Table (SoldOutItems) in favor of using Fluent. This unfortunately creates a cyclical reference problem since I cannot remove the cascading deletes from my many-to-many table now.
New code after removing my 'SoldOutItems' table:
public class RetailContext : DbContext
{
public DbSet<Franchise> Franchises { get; set; }
public DbSet<Store> Stores { get; set; }
public DbSet<Item> Items { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Store>().HasMa开发者_Go百科ny(s => s.UnavailableItems).WithMany(i => i.StoresUnavailableAt).Map(mc => mc.ToTable("SoldOutItems").MapLeftKey("StoreId").MapRightKey("ItemId"));
//Unfortunately I have to remove the cascading delete here since I can't figure out how to do it on the table created from the line above.
modelBuilder.Entity<Franchise>().HasMany(f => f.Stores).WithRequired(s => s.Franchise).WillCascadeOnDelete(false);
base.OnModelCreating(modelBuilder);
}
}
Simple,, you need to initiate both collections as follow :
[Table("Stores")]
public class Store
{
public Store()
{
UnavailableItems = new HashSet<Item>();
}
...
}
[Table("Items")]
public class Item
{
public Item()
{
StoresUnavailableAt = new HashSet<Store>();
}
...
}
精彩评论