One to many recursive relationship with Code First

ef-code-first entity-framework entity-framework-6


I am trying to implement a simple self referencing relationship with EF 6.1.2 Code First.

public class Branch 
    public int Id { get; set; }

    public string Name { get; set; }

    public int? ParentId { get; set; }

    public virtual Branch Parent { get; set; }

    public ICollection<Branch> Children { get; set; } // direct successors

In my application I have exactly one root branch. And except for this single root branch, every branch has exactly one parent (the parentId of the root branch is NULL). Other than that, every branch can have [0..n] subbranches.

I have two issues:

  1. Do I need to specify any extra FluentApi code in OnModelCreating(DbModelBuilder modelBuilder) in order to make EF understand this one-to-many self-referencing relationship? I tried this: modelBuilder.Entity<Branch>().HasOptional<Branch>(b => b.Parent).WithMany(b => b.Children).HasForeignKey(b => b.ParentId); But I am not sure if I need this at all.
  2. For a given branch I want to retrieve all children (all the way down the hierarchy). This is what I came up with so far:


 public IEnumerable<Branch> GetBranches(Branch anyBranch)
     return anyBranch.Flatten(b => b.Children);


 public static IEnumerable<T> Flatten<T>(this T node, Func<T, IEnumerable<T>> selector)
     return selector(node).SelectMany(x => Flatten(x, selector))
                            .Concat(new[] { node });

The second snippet is not from me. I found it somewhere else on StackOverflow. To be honest, I hardly understand how it is supposed to work.

When I run my application and call GetBranches() (I tried this with several different branches), I receive an exception inside the Flatten() method. The error message says: "Value cannot be null. Parameter name: source". Unfortunately this does not give me any clue what is going wrong here.

I hope anybody can help me out here? Thanks so much!

12/31/2014 11:54:17 AM

Accepted Answer

Cause of the exception

The exception is caused by a Select or SelectMany on a null collection, in your case the result of

b => b.Children

For each branch in the hierarchy the Children collection is accessed when they reach the part


The selector is the lambda expression b => b.Children, which is the same as a method

IEnumerable<Branch> anonymousMethod(Branch b)
    return b.Children;

So what actually happens is b.Children.SelectMany(...), or null.SelectMany(...), which raises the exception you see.

Preventing it

But why are these Children collections null?

This is because lazy loading does not happen. To enable lazy loading the collection must be virtual:

public virtual ICollection<Branch> Children { get; set; }

When EF fetches a Branch object from the database it creates a proxy object, an object derived from Branch, that overrides virtual properties by code that is capable of lazy loading. Now when b.Children is addressed, EF will execute a query that populates the collection. If there are no children, the collection will be empty, not null.

Flattening explained

So what happens in the Flatten method is that first the children of the branch are fetched (selector(node)), subsequently on each of these children (SelectMany) the Flatten method is called again (now just as a method Flatten(x, selector), not an extension method).

In the Flatten method each node is added to the collection of its children (.Concat(new[] { node }), so in the end, all nodes in the hierarchy are returned (because Flatten returns the node that enters it).

Some remarks

  1. I would like to have the parent node on top of the collection, so I would change the Flatten method into

    public static IEnumerable<T> Flatten<T>(this T node, Func<T,IEnumerable<T>> selector)
        return new[] { node }
            .Concat(selector(node).SelectMany(x => Flatten(x, selector)));
  2. Fetching a hierarchy by lazy loading is quite inefficient. In fact, LINQ is not the most suitable tool for querying hierarchies. Doing this efficiently would require a view in the database that uses a CTE (common table expression). But that's a different story...

12/31/2014 2:37:28 PM

Related Questions


Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow