Projecting self referencing multi level Entities I

2020-03-24 09:06发布

问题:

Projecting self referencing multi level entities in Entity Framework 6.

Let's say that I have a Category entity as follows:

public class Category
{
    public int CategoryId { get; set; }
    public int? ParentCategoryId { get; set; }        
    public string Name { get; set; }
    public string Description { get; set; }        

    public virtual Category ParentCategory { get; set; }

    public virtual ICollection<Category> SubCategories { get; set; }
    public virtual ICollection<Product> Products { get; set; }

    public Category()
    {            
        SubCategories = new HashSet<Category>();
        Products = new HashSet<Product>();
    }
}

And I would like to map the whole Category DbSet with all the hierarchy to a following POCO class (while including all possible levels of sub and parent categories):

public class CategoryView
{
    public int Id { get; set; }
    public int? ParentCategoryId { get; set; }        
    public string Name { get; set; }
    public string Description { get; set; }        

    public CategoryView ParentCategory { get; set; }

    public List<CategoryView> SubCategories { get; set; }

    public int ProductCount { get; set; }

    public Category()
    {            
        SubCategories = new HashSet<CategoryView>();            
    }
}

Please bear in mind that a single category may have unlimited levels of subcategories as follows:

Category (Level 0)
    SubCategory1 (Level 1)
    SubCategory2
        SubCategory2SubCategory1 (Level 2)
        SubCategory2SubCategory2
            SubCategory2SubCategory2SubCategory1 (Level 3)
            ... (Level N)
    SubCategory3

When tried to create hierarchy with recursive a method which tries to process every single categories sub and parent categories, got stackoverflow exception, since it get stuck between the first category (Category) and the first subcategory (SubCategory1) due to relation between ParentCategory and SubCategories.

What is the best and elegant way of doing such projection (without eliminating parents)? (Or is there any?)

Any help would be appreciated.

Thank you,

回答1:

I can't say if it's the best or elegant way, but it's pretty standard and efficient non recursive way of building such structure.

Start with loading all categories without parent / child object links using a simple projection:

var allCategories = db.Categories
    .Select(c => new CategoryView
    {
        Id = c.CategoryId,
        ParentCategoryId = c.ParentCategoryId,
        Name = c.Name,
        Description = c.Description,
        ProductCount = c.Products.Count()
    })
    .ToList();

then create a fast lookup data structure for finding CategoryView by Id:

var categoryById = allCategories.ToDictionary(c => c.Id);

then link the subcategories to their parents using the previously prepared data structures:

foreach (var category in allCategories.Where(c => c.ParentCategoryId != null))
{
    category.ParentCategory = categoryById[category.ParentCategoryId.Value];
    category.ParentCategory.SubCategories.Add(category);
}

At this point, the tree links are ready. Depending of your needs. either return the allCategories or the root categories if you need a real tree representation:

return allCategories.Where(c => c.ParentCategoryId == null);

P.S. Actually the allCategories list can be avoided, since categoryById.Values could serve the same purpose.



回答2:

It might not be elegant, but a suitable solution is to have in your code a shared IDictionary<int, CategoryView>. When you are going to map an entity Category into a CategoryView check first if you have already created this object and set the reference stored in the dictionary instead of creating a CategoryView instance. When creating a new instance, store it in the dictionary. This is a way to take advantage of the primary key of your entity to avoid the infinite recursion issue in your code.

Also, notice that in your CategoryView object you shouldn't be referencing Category instances. Update it to reference CategoryView instances like this.

public class CategoryView
{

    public int Id { get; set; }

    public int? ParentCategoryId { get; set; }        

    // other properties ...

    public CategoryView ParentCategory { get; set; }

    public List<CategoryView> SubCategories { get; set; }

    public int ProductCount { get; set; }

    public CategoryView()
    {            
        SubCategories = new List<CategoryView>();
    }
}