Entity Framework is quite convenient with all it's automation, but unfortunately, like most things in life, it has yet to master every tricky situation out there, this being one of them. (Although to be fair, the problem pretty much lies within storing hierarchical data in a relational database).
I tend to solve similar situations by "cheating" a bit, at least whenever possible/suitable, by introducing some kind of additional property/column to group them, and then simply load them all, and do the relational mapping by hand, which is usually quite simple.
Loading additional data in one database call is often to prefer before making multiple calls. (You might still have to sneak around any lurking db-admin though).
Assuming you're planning for a situation with potentially N amount in breadth and M amount in depth (if not, the other answers should suffice), it's a quick and dirty solution which in worst case at least gets the job done.
To stick with EF, the idea is essentially to first decouple the relationships that EF might have mapped and use simple value types as reference: (It's not really a necessity, but something I tend to prefer)
class Item
{
public string Id { get; set; }
public virtual IEnumerable<Category> Categories { get; set; }
}
class Category
{
public string Id { get; set; }
// We drop the parent reference property and add a simple ParentId property instead,
// hopefully saving us some future headache.
//
public string ParentId { get; set; }
//public virtual Category Parent { get; set; } // Goodbye dear friend, you have served us well.
// Depending on how you're configuring, we might have to "loose" some EF-mapped relationships,
// [NotMapped] is merely an example of that here, it's not neccessarily required.
[NotMapped]
public virtual IEnumerable<Category> Subcategories { get; set; }
// As an example, I've just added the item id as our category scope/discriminator,
// allowing us to limit our full query at least somewhat.
//
public string ItemId { get; set; }
}
Now we're ready to do what EF does best. Load and map data! We will load a plan list of the category-entities all by themselves, without any direct relationships to anything else, and then map them ourselves.
To make it maintainable, let's create a neat little static class and add some useful extensions methods that will assist us, starting with the initial DbContext-load.
public static class CategoryExtensions
{
/// <summary>
/// Extension method to find and load all <see cref="Category"/> per <see cref="Category.ItemId"/>
/// </summary>
public static List<Category> FindCategoriesForItemId(this DbContext dbContext, string itemId)
=> dbContext.Set<Category>()
.Where(c => c.ItemId == itemId)
.ToList();
}
Once we're able to easily load categories, it would be useful to be able to map the children and possibly flatten them/any subcategory if necessary, so we throw two more methods in there, one to map child categories to all the categories we've found, and one to flatten a hierarchically structure we might have in the future (or just for fun).
/// <summary>
/// Flattens the IEnumerable by selecting and concatenating all children recursively
/// </summary>
/// <param name="predicate">Predicate to select the child collection to flatten</param>
/// <returns>Flat list of all items in the hierarchically constructed source</returns>
public static IEnumerable<TSource> Flatten<TSource>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TSource>> predicate)
=> source.Concat(source.SelectMany(s => predicate(s).Flatten(predicate)));
/// <summary>
/// "Overload" for above but to use with a single root category or sub category...
/// </summary>
public static IEnumerable<TSource> Flatten<TSource>(this TSource source, Func<TSource, IEnumerable<TSource>> predicate)
=> predicate(source).Flatten(predicate);
/// <summary>
/// For each entry in the <paramref name="flatSources"/>,
/// finds all other entries in the <paramref name="flatSources"/> which has
/// a <paramref name="parentRefPropSelector"/> value matching initial entries
/// <paramref name="identityPropSelector"/>
/// </summary>
/// <param name="flatSources">Flat collection of entities that can have children</param>
/// <param name="identityPropSelector">Selector Func to select the identity property of an entry</param>
/// <param name="parentRefPropSelector">Selector Func to select the parent reference property of an entry</param>
/// <param name="addChildren">Action that is called once any children are found and added to a parent entry</param>
public static IEnumerable<TSource> MapChildren<TSource, TKey>(
this IEnumerable<TSource> flatSources,
Func<TSource, TKey> identityPropSelector,
Func<TSource, TKey> parentRefPropSelector,
Action<TSource, IEnumerable<TSource>> addChildren)
=> flatSources.GroupJoin( // Join all entityes...
flatSources, // ... with themselves.
parent => identityPropSelector(parent), // On identity property for one...
child => parentRefPropSelector(child), // ... And parent ref property for another.
(parent, children) => // Which gives us a list with each parent, and the children to it...
{
addChildren(parent, children); // ... Which we use to call the addChildren action, leaving adding up to the caller
return parent;
});
That's it. It's not perfect, but, in my opinion, it's a decent enough starter solution that still takes advantage of EF and doesn't complicate it too much. Only worry is if the amount of categories loaded becomes too large, but at that point, it will be well worth spending some actual time on a more "proper" solution. (I haven't actually tested that MapChildren-extension, and there's a lot of room for improvements to it, but I hope it helps to illustrate the idea.)
To eventually actually use it, it ends up looking something like this:
/// <summary>
/// Loads and structures all categories related to <see cref="itemId"/>
/// and returns first <see cref="Category"/> where <see cref="Category.ParentId"/>
/// is null.
/// </summary>
public Category GetMeRootCategorylore(string itemId)
{
using (var dbContext = new DbContext())
{
var mappedAndArmedCategories
= dbContext // Use our db context...
.FindCategoriesForItemId(itemId) // To find categories..
.MapChildren( // And then immediately map them, which comes close to what we're used with when using EF.
parent => parent.Id, // Set the identity property to map children against
child => child.ParentId, // Set the parent references to map with
(parent, children) => parent.Subcategories = children); // This is called when children have been found and should be mapped to the parent.
// Oh noes, what if I need a flattened category list later for whatever reason! (Or to do some real lazy loading when looking a single one up!)
// ... Aha! I almost forgot about our nifty extension method to flatten hierarchical structures!
//
var flattenedList = mappedAndArmedCategories.Flatten(c => c.Subcategories);
// Maybe we'll pick up a root category at some point
var rootCategory = mappedAndArmedCategories.FirstOrDefault(c => c.ParentId == null);
// And perhaps even flatten it's children from the single category node:
var subFlattenedList = rootCategory?.Flatten(c => c.Subcategories);
// But now we've had enough fun for today, so we return our new category friend.
return rootCategory;
}
}
Finally, here's a quite informative and helpful question about hierarchical data in relational databases if you'd like to dig in deeper or get some other ideas: What are the options for storing hierarchical data in a relational database?