在Entity Framework 6中投影自引用多级实体

5

在Entity Framework 6中投射自我引用的多层实体。

假设我有一个 Category 实体,如下所示:

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>();
    }
}

我希望能够将整个CategoryDbSet的层级结构映射到下面的 POCO 类中(包括所有可能的子类别和父类别):

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>();            
    }
}

请记住,一个类别可能有无限级的子类别,具体如下所示:
Category (Level 0)
    SubCategory1 (Level 1)
    SubCategory2
        SubCategory2SubCategory1 (Level 2)
        SubCategory2SubCategory2
            SubCategory2SubCategory2SubCategory1 (Level 3)
            ... (Level N)
    SubCategory3

试图创建一个递归方法来处理每个类别的子类别和父级类别以创建层级时,出现了"stackoverflow exception"错误。由于"ParentCategory"与"SubCategories"之间的关系,在第一个类别("Category")和第一个子类别("SubCategory1")之间卡住了。
有什么最好、最优雅的方法可以完成这样的投影(而不是消除父级)吗?还是有其他方法吗?
任何帮助都将不胜感激。
谢谢,

你的意思是要生成 IQueryable<CategoryView> 还是 IEnumerable<CategoryView>?此外,CategoryViewParentCategorySubCategories 不应该指向帖子中的 Category 对象,而应该指向 CategoryView 对象。 - Ivan Stoev
IvanStoev,jorgonor,你们两个都是对的,这是复制/粘贴错误。POCO类应该持有CategoryView而不是Category。IvanStoev希望在投影结果的末尾拥有IEnumerable<CategoryView>。谢谢。 另外,jorgonor,使用字典方法处理子类别的父类别时也会导致stackoverflow错误。 - Onur Buyukcaglar
就像我之前所说的,每个函数调用都必须使用相同的字典。我猜你正在调用一个递归函数来创建序列,每个函数调用都必须共享这个实例。这可以防止创建具有相同ID的新实例。除非你有一个非常大的数据集,否则你不应该看到任何StackOverflowException。 - jorgonor
2个回答

4

我不能说这是最好或最优雅的方法,但这是一种相当标准和高效的非递归方式来构建这样的结构。

首先使用简单的投影加载所有类别,没有父/子对象链接

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();

接着创建一个快速查找数据结构,通过Id查找CategoryView

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

然后使用之前准备好的数据结构将子类别链接到它们的父类别:
foreach (var category in allCategories.Where(c => c.ParentCategoryId != null))
{
    category.ParentCategory = categoryById[category.ParentCategoryId.Value];
    category.ParentCategory.SubCategories.Add(category);
}

此时,树形链接已经准备好了。根据您的需求,如果需要真正的树形表示,则返回root类别,否则返回allCategories

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

顺便说一下,实际上可以避免使用allCategories列表,因为categoryById.Values可以起到相同的作用。


1
谢谢 @Ivan,这个像魔法一样好用! - Onur Buyukcaglar

2
可能不够优雅,但是一个适合的解决方案是在你的代码中有一个共享的IDictionary<int,CategoryView>。当你要将实体类别映射为CategoryView时,首先检查是否已经创建了这个对象,并设置存储在字典中的引用,而不是创建一个CategoryView实例。创建新实例时,请将其存储在字典中。这是利用实体类的主键避免代码中无限递归问题的一种方法。
此外,请注意,在CategoryView对象中,您不应引用Category实例,而应更新它以引用类似于CategoryView的实例。
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>();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接