Scotty H
Scotty H

Reputation: 6694

Entity Framework 6 Load List on Eager Load

Entity Framework is loading everything in the Polygon but my list of LineSegments. What am I missing?

Polygon:

public class Polygon
{
    List<LineSegment> _lineSegments = new List<LineSegment>();
    public List<LineSegment> LineSegments
    {
        get { return _lineSegments; }
        protected set { _lineSegments = value; }
    }

    public Point BasePoint { get; protected set; }

    public Vector NormalVector { get; protected set; }

    public int? DatabaseId { get; set; }

    // ...
}

LineSegment class (Polygon has a list of these)

public class LineSegment
{
    public virtual Distance Magnitude
    {
        get { return _magnitude; }
        set { _magnitude = value; }
    }
    private Distance _magnitude;

    public virtual Point BasePoint
    {
        get { return _basePoint; }
        set { this._basePoint = value; }
    }
    private Point _basePoint;

    public virtual Direction Direction
    {
        get { return _direction; }
        set { _direction = value; }
    }
    private Direction _direction;

    public int? DatabaseId { get; set; }

    // ...
}

And here is the relationship setup in the model:

modelBuilder.Entity<Polygon>()
        .HasMany(polygon => polygon.LineSegments);

So there is a table for both Polygons and LineSegments, and they insert properly where the LineSegment has a reference to a Polygon. But when I try to retrieve them using eager loading, it doesn't load the list. I have the properties of LineSegment listed in the includes, but it's not working. I think I need to amend the relationship setup in the model, but I'm not sure how. How can I correct this so that I load the list of LineSegments eagerly when I load a Polygon? Here's the querying:

    private static List<Expression<Func<Polygon, object>>> polygonRegionNaviationProperties = new List<Expression<Func<Polygon, object>>>()
    {
        (polygon => polygon.BasePoint),
        (polygon => polygon.BasePoint.X),
        (polygon => polygon.BasePoint.Y),
        (polygon => polygon.BasePoint.Z),
        (polygon => polygon.NormalVector),
        (polygon => polygon.NormalVector.Direction),
        (polygon => polygon.NormalVector.Direction.Phi),
        (polygon => polygon.NormalVector.Direction.Theta),
        (polygon => polygon.NormalVector.Magnitude),
        (polygon => polygon.NormalVector.BasePoint.X),
        (polygon => polygon.NormalVector.BasePoint.Y),
        (polygon => polygon.NormalVector.BasePoint.Z),
        (polygon => polygon.LineSegments),
        (polygon => polygon.LineSegments.Select(lineSegment => lineSegment.Direction)),
        (polygon => polygon.LineSegments.Select(lineSegment => lineSegment.Direction.Phi)),
        (polygon => polygon.LineSegments.Select(lineSegment => lineSegment.Direction.Theta)),
        (polygon => polygon.LineSegments.Select(lineSegment => lineSegment.Magnitude)),
        (polygon => polygon.LineSegments.Select(lineSegment => lineSegment.BasePoint.X)),
        (polygon => polygon.LineSegments.Select(lineSegment => lineSegment.BasePoint.Y)),
        (polygon => polygon.LineSegments.Select(lineSegment => lineSegment.BasePoint.Z))
    };

    public Polygon GetPolygon(int? databaseId)
    {
        if(databaseId != null)
        {
            Polygon retrievedPolygon = Query((polygon => polygon.DatabaseId == databaseId), polygonRegionNaviationProperties);
            return retrievedPolygon;
        }
        else
        {
            return null;
        }
    }

    public override Polygon Query(Expression<Func<Polygon, bool>> match, List<Expression<Func<Polygon, object>>> includes = null)
    {
        using (var databaseContext = new ClearspanDatabaseContext())
        {
            databaseContext.Database.Log = Console.Write;

            if (includes != null)
            {
                var dataSet = databaseContext.Set<Polygon>(); // Get the relevant DataSet
                Polygon retrievedObject = includes.Aggregate( // Eagerly load the passed navigation properties
                        dataSet.AsQueryable(),
                        (current, include) => current.Include(include)
                    ).SingleOrDefault(match);
                databaseContext.Entry(retrievedObject).Collection(polygon => polygon.LineSegments).Load();
                return retrievedObject;
            }
            else
            {
                Polygon retrievedObject = databaseContext.Set<Polygon>().SingleOrDefault(match);
                databaseContext.Entry(retrievedObject).Collection(polygon => polygon.LineSegments).Load();
                return retrievedObject;
            }
        }
    }

UPDATE

Here is a link to a thinned out project illustrating my problem.

  1. Clone it down.
  2. Initialize the submodules (git submodule init and git submodule update in the root of the project) (this may happen automatically if you use SourceTree)
  3. Run the PostgreSQL script, noting that you can't run it all at once since it's creating a database. Follow the comments at the top.
  4. Run the unit tests, and note that they all should pass except InsertAndRetrievePolygon. That's because the List isn't retrieved, which you can see by hovering over the variables in the debugger.

Hopefully this is helpful to understanding, diagnosing, and solving this problem. You'll note that in my question I simplified the hierarchy that actually exists in the GeometryClassLibrary. Thanks, Scott H

Upvotes: 4

Views: 217

Answers (3)

Gert Arnold
Gert Arnold

Reputation: 109099

This is the code of Polygon.LineSegments:

public virtual List<LineSegment> LineSegments
{
    get { return this._Edges.Select(e => (LineSegment)e).ToList(); }
    set { _Edges = value.ToList<IEdge>(); }
}

When EF populates a child collection it won't assign a complete collection to it, but initialize it if necessary and then add items to it. Now here's the pitfall: to which collection does EF add items? Not _Edges, but a transient collection that is build by ToList() each time LineSegments is addressed. _Edges itself remains empty.

Unfortunately, you can't solve this by Include()-ing Edges in stead of LineSegments, because this is the code of Edges:

protected List<IEdge> _Edges = new List<IEdge>();
public virtual List<IEdge> Edges
{
    get { return _Edges; }
    set { _Edges = value; }
}

And EF doesn't support interfaces. Frankly, I don't know if there is a decent work-around. This indirection in LineSegments is likely to cause more issues with EF. For instance, I don't think relationship fixup (auto-population of navigation properties from entities loaded into the context) will run either.

I understand these classes are from an external library. I see all these XyzEntityFramework partial classes there (e.g. LineSegmentEntityFramework) that seem to add properties to facilitate working with EF. Maybe they are willing to look at your issue and find a solution for it.

Upvotes: 3

AesopWaits
AesopWaits

Reputation: 21

I'd take a look at the first example about one-to-many relationships in Code First here. http://www.entityframeworktutorial.net/code-first/configure-one-to-many-relationship-in-code-first.aspx

I think Fabio Luz was on the right track. The LineSegments collection should be virtual. You can also try overriding the default constructor and instantiate the property. In addition, there should probably be a virtual property for the Polygon in the LineSegments class to complete the referential relation.

public class Polygon 
{
    public Polygon()
    {
        LineSegments = new List<LineSegment>();
    }

    public virtual List<LineSegment> LineSegments
    {
        get;
        set;
    }


    // ...
}

public class LineSegment
{
   public virtual Polygon Polygon { get; set; }
}

Upvotes: 0

Fabio
Fabio

Reputation: 11990

Try to change your class as follow:

public class Polygon
{
    public virtual List<LineSegment> LineSegments
    {
        get;
        set;
    }


    // ...
}

In your query, include only navigation properties. Strings, primitive types and enumeration types shouldn't be included. Like this:

var dataSet = databaseContext.Set<Polygon>();
Polygon retrievedObject = dataSet.Include(i => i.LineSegments).SingleOrDefault(match);

return retrievedObject;

Does that work for you?

Upvotes: 1

Related Questions