简体   繁体   中英

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 (ie 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.

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

As for the context lifespan, your repositories should share one context per request if this is a web application. Else it's a bit more arbitrary, but it should be something like a context per use case or service call.

So the pattern would be: create a context, pass it to the repositories involved in the call, do the task, and dispose the context. The context can be seen as your unit of work, so no matter how many repositories are involved, in the end one SaveChanges() should normally be enough to commit all changes.

I can't tell if this will solve the lazy loading issue, because from what I see I can't explain why it doesn't occur.

But although if I were in your shoes I'd like to get to the bottom of it, lazy loading is something that should not be relied on too much. Take a look at your (abridged) code:

foreach (Ask a in asks)
{
    ReducedAsk r = new ReducedAsk()
                   { 
                       sellerName = a.seller.username,
                       itemName = a.item.name
                   };

If lazy loading would work as expected, this would execute two queries against the database for each iteration of the loop. Of course, that's highly inefficient. That's why using Include (as in Anton's answer) is better anyhow, not only to circumvent your issue.

A further optimization is to do the projection (ie the new { ) in the query itself:

var reducedAsks = repo.GetAsksBySeller(userId)
                      .Select(a => new ReducedAsk() { ... })
                      .ToList();

(Assuming – and requiring – that repo.GetAsksBySeller returns IQueryable ).

Now only the data necessary to create ReducedAsk will be fetched from the database and it prevents materialization of entities that you're not using anyway and relatively expensive processes as change tracking and relationship fixup.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM