问题
I am trying to implement a simple self referencing relationship with EF 6.1.2 Code First.
public class Branch
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public int? ParentId { get; set; }
[ForeignKey("ParentId")]
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:
- 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. - 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);
}
and
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!
回答1:
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
selector(node)
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
I would like to have the parent node on top of the collection, so I would change the
Flatten
method intopublic static IEnumerable<T> Flatten<T>(this T node, Func<T,IEnumerable<T>> selector) { return new[] { node } .Concat(selector(node).SelectMany(x => Flatten(x, selector))); }
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...
来源:https://stackoverflow.com/questions/27720369/one-to-many-recursive-relationship-with-code-first