Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework lazy loaded collection sometimes null

I have 2 models, one of which has a child collection of the other:

[Table("ParentTable")]
public class Parent
{
    [Key, Column("Parent")]
    public string Id { get; set; }

    [Column("ParentName")]
    public string Name { get; set; }

    public virtual ICollection<Widget> Widgets { get; set; }
}

[Table("WidgetTable")]
public class Widget
{
    public string Year { get; set; }

    [Column("Parent")]
    public string ParentId { get; set; }

    public string Comments { get; set; }

    [Key, Column("ID_Widget")]
    public int Id { get; set; }

    [ForeignKey("ParentId"), JsonIgnore]
    public virtual Parent Parent { get; set; }
}

This code works for > 99% of widgets:

var parent = _dbContext.Parents.FirstOrDefault(p => p.Id == parentId);

Usually, parent.Widgets is a collection with more than one item. In a couple of instances, however, parent.Widgets is null (not a collection with no items).

I have used Query Analyzer to trace both the query for the parent and the query for widgets belonging to that parent. Both return exactly the rows I expect; however, the model for one or two parent IDs results in a null value for the Widgets collection. What could cause a lazy-loaded collection to be null in some instances but not others?

like image 587
Andy Avatar asked Mar 08 '23 11:03

Andy


1 Answers

This situation commonly comes up when a dbContext lifetime is left open across an Add, saveChanges, and then retrieval.

For example:

var context = new MyDbContext(); // holding Parents.
var testParent = new Parent{Id = "Parent1", Name = "Parent 1"};
context.Parents.Add(testParent);

At this point if you were to do:

var result = context.Parents.FirstOrDefault(x=> x.ParentId == "Parent1"); 

you wouldn't get a parent. Selection comes from committed state.. So...

context.SaveChanges();
var result = context.Parents.FirstOrDefault(x=> x.ParentId == "Parent1"); 

This will return you a reference to the parent you had inserted since the context knows about this entity and has a reference to the object you created. It doesn't go to data state. Since your definition for Widgets was just defined with a get/set auto-property the Widgets collection in this case will be #null.

if you do this:

context.Dispose();
context = new MyDbContext();
var result = context.Parents.FirstOrDefault(x=> x.ParentId == "Parent1"); 

In this case the parent is not known by the new context so it goes to data state. EF will return you a proxy list for lazy loading the Widgets, which there are none so you get back an empty list, not #null.

When dealing with collection classes in EF it's best to avoid auto-properties or initialize them in your constructor to avoid this behaviour; you'll typically want to assign Widgets after creating a Parent. Initializing a default member is better because you don't want to encourage ever using a setter on the collection property.

For example:

private readonly List<Widget> _widgets = new List<Widget>();
public virtual ICollection<Widget> Widgets
{
  get { return _widgets; }
  protected set { throw new InvalidOperationException("Do not set the Widget collection. Use Clear() and Add()"); }
}

Avoid performing a Set operation on a collection property as this will screw up in entity reference scenarios. For instance, if you wanted to sort your Widget collection by year and did something like:

parent.Widgets = parent.Widgets.OrderBy(x=> x.Year).ToList();

Seems innocent enough, but when the Widgets reference was an EF proxy, you've just blown it away. EF now cannot perform change tracking on the collection.

Initialize your collection and you should avoid surprises with #null collection references. Also I would look at the lifetime of your dbContext. It's good to keep one initialized over the lifetime of a request or particular operation, but avoid keeping them alive longer than necessary. Context change tracking and such consume resources and you can find seemingly intermittent odd behaviour like this when they cross operations.

like image 98
Steve Py Avatar answered Mar 20 '23 18:03

Steve Py