Search code examples
c#entity-framework-6nullreferenceexceptionnavigation-properties

Entity Framework 6.1: navigation properties not loading


This is my first time using Entity Framework 6.1 (code first). I keep running into a problem where my navigation properties are null when I don't expect them to be. I've enabled lazy loading.

My entity looks like this:

public class Ask
{
    public Ask()
    {
        this.quantity = -1;
        this.price = -1;
    }

    public int id { get; set; }
    public int quantity { get; set; }
    public float price { get; set; }

    public int sellerId { get; set; }
    public virtual User seller { get; set; }
    public int itemId { get; set; }
    public virtual Item item { get; set; }
}

It has the following mapper:

class AskMapper : EntityTypeConfiguration<Ask>
{
    public AskMapper()
    {
        this.ToTable("Asks");

        this.HasKey(a => a.id);
        this.Property(a => a.id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
        this.Property(a => a.id).IsRequired();

        this.Property(a => a.quantity).IsRequired();

        this.Property(a => a.price).IsRequired();

        this.Property(a => a.sellerId).IsRequired();
        this.HasRequired(a => a.seller).WithMany(u => u.asks).HasForeignKey(a => a.sellerId).WillCascadeOnDelete(true);

        this.Property(a => a.itemId).IsRequired();
        this.HasRequired(a => a.item).WithMany(i => i.asks).HasForeignKey(a => a.itemId).WillCascadeOnDelete(true);
    }
}

Specifically, the problem is that I have an Ask object with a correctly set itemId (which does correspond to an Item in the database), but the navigation property item is null, and as a result I end up getting a NullReferenceException. The exception is thrown in the code below, when I try to access a.item.name:

List<Ask> asks = repo.GetAsksBySeller(userId).ToList();
List<ReducedAsk> reducedAsks = new List<ReducedAsk>();

foreach (Ask a in asks)
{
    ReducedAsk r = new ReducedAsk() { id = a.id, sellerName = a.seller.username, itemId = a.itemId, itemName = a.item.name, price = a.price, quantity = a.quantity };
    reducedAsks.Add(r);
}

Confusingly, the seller navigation property is working fine there, and I can't find anything I've done differently in the 'User' entity, nor in its mapper.

I have a test which recreates this, but it passes without any problems:

public void canGetAsk()
{
    int quantity = 2;
    int price = 10;

    //add a seller
    User seller = new User() { username = "ted" };
    Assert.IsNotNull(seller);
    int sellerId = repo.InsertUser(seller);
    Assert.AreNotEqual(-1, sellerId);

    //add an item
    Item item = new Item() { name = "fanta" };
    Assert.IsNotNull(item);
    int itemId = repo.InsertItem(item);
    Assert.AreNotEqual(-1, itemId);

    bool success = repo.AddInventory(sellerId, itemId, quantity);
    Assert.AreNotEqual(-1, success);

    //add an ask
    int askId = repo.InsertAsk(new Ask() { sellerId = sellerId, itemId = itemId, quantity = quantity, price = price });
    Assert.AreNotEqual(-1, askId);

    //retrieve the ask
    Ask ask = repo.GetAsk(askId);
    Assert.IsNotNull(ask);

    //check the ask info
    Assert.AreEqual(quantity, ask.quantity);
    Assert.AreEqual(price, ask.price);
    Assert.AreEqual(sellerId, ask.sellerId);
    Assert.AreEqual(sellerId, ask.seller.id);
    Assert.AreEqual(itemId, ask.itemId);
    Assert.AreEqual(itemId, ask.item.id);
    Assert.AreEqual("fanta", ask.item.name);
}

Any help would be extremely appreciated; this has been driving me crazy for days.


EDIT:

The database is SQL Server 2014.

At the moment, I have one shared context, instantiated the level above this (my repository layer for the db). Should I be instantiating a new context for each method? Or instantiating one at the lowest possible level (i.e. for every db access)? For example:

public IQueryable<Ask> GetAsksBySeller(int sellerId)
{
    using (MarketContext _ctx = new MarketContext())
    {
        return _ctx.Asks.Where(s => s.seller.id == sellerId).AsQueryable();
    }
}

Some of my methods invoke others in the repo layer. Would it better for each method to take a context, which it can then pass to any methods it calls?

public IQueryable<Transaction> GetTransactionsByUser(MarketContext _ctx, int userId)
{
    IQueryable<Transaction> buyTransactions = GetTransactionsByBuyer(_ctx, userId);
    IQueryable<Transaction> sellTransactions = GetTransactionsBySeller(_ctx, userId);

    return buyTransactions.Concat(sellTransactions);
}

Then I could just instantiate a new context whenever I call anything from the repo layer: repo.GetTransactionsByUser(new MarketContext(), userId);

Again, thanks for the help. I'm new to this, and don't know which approach would be best.


Solution

  • Try to add Include call in your repository call:

    public IQueryable<Ask> GetAsksBySeller(int sellerId)
    {
        using (MarketContext _ctx = new MarketContext())
        {
            return _ctx.Asks
                   .Include("seller")
                   .Include("item")
                   .Where(s => s.seller.id == sellerId).AsQueryable();
        }
    }
    

    Also, there is an extension method Include which accepts lambda expression as parameter and provides you type checks on compile time

    http://msdn.microsoft.com/en-us/data/jj574232.aspx