32

C# | .NET 4.5 | 实体框架 5

我在实体框架中有一个类,如下所示:

public class Location
{
   public long ID {get;set;}
   public long ParentID {get;set;}
   public List<Location> Children {get;set;}
}

ID 是位置的标识符,ParentID 将其链接到父位置,而 Children 包含父位置的所有子位置。我正在寻找一种简单的方法,可能是递归的,将所有“位置”及其子项放到一个包含 Location.ID 的列表中。我在递归地概念化这个时遇到了麻烦。任何帮助表示赞赏。

这是我到目前为止所拥有的,它是实体类的扩展,但我相信它可以做得更好/更简单:

public List<Location> GetAllDescendants()
{
    List<Location> returnList = new List<Location>();
    List<Location> result = new List<Location>();
    result.AddRange(GetAllDescendants(this, returnList));
    return result;
}

public List<Location> GetAllDescendants(Location oID, ICollection<Location> list)
{
    list.Add(oID);
    foreach (Location o in oID.Children)
    {
            if (o.ID != oID.ID)
                    GetAllDescendants(o, list);
    }
    return list.ToList();
}

更新

我最终用 SQL 编写了递归,将其放入 SP,然后将其拉入 Entity。对我来说似乎比使用 Linq 更干净、更容易,从评论来看,Linq 和 Entity 似乎不是最好的选择。感谢所有的帮助!

4

10 回答 10

31

你可以做SelectMany

List<Location> result = myLocationList.SelectMany(x => x.Children).ToList();

您可以将 where 条件用于某些选择性结果,例如

List<Location> result = myLocationList.Where(y => y.ParentID == someValue)
                                      .SelectMany(x => x.Children).ToList();

如果您只需要孩子的身份证,您可以这样做

List<long> idResult = myLocationList.SelectMany(x => x.Children)
                                    .SelectMany(x => x.ID).ToList();
于 2013-10-08T02:07:32.197 回答
15

这可以解决问题:

class Extensions
{
    public static IEnumerable<T> SelectManyRecursive<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> selector)
    {
        var result = source.SelectMany(selector);
        if (!result.Any())
        {
            return result;
        }
        return result.Concat(result.SelectManyRecursive(selector));
    }
}

像这样使用它:

List<Location> locations = new List<Location>();
//
// your code here to get locations
//
List<string> IDs = locations.SelectManyRecursive(l => l.Children).Select(l => l.ID).ToList();
于 2017-03-08T20:07:54.347 回答
11

我的模型中没有Children道具,所以Nikhil Agrawal的答案对我不起作用,所以这是我的解决方案。

使用以下型号:

public class Foo
{
    public int Id { get; set; }
    public int? ParentId { get; set; }  
    // other props
}

您可以使用以下方法获取一项的子项:

List<Foo> GetChildren(List<Foo> foos, int id)
{
    return foos
        .Where(x => x.ParentId == id)
        .Union(foos.Where(x => x.ParentId == id)
            .SelectMany(y => GetChildren(foos, y.Id))
        ).ToList();
}

例如。

List<Foo> foos = new List<Foo>();

foos.Add(new Foo { Id = 1 });
foos.Add(new Foo { Id = 2, ParentId = 1 });
foos.Add(new Foo { Id = 3, ParentId = 2 });
foos.Add(new Foo { Id = 4 });

GetChild(foos, 1).Dump(); // will give you 2 and 3 (ids)
于 2018-02-04T10:02:08.577 回答
10

试试这个扩展方法:

public static IEnumerable<T> Flatten<T, R>(this IEnumerable<T> source, Func<T, R> recursion) where R : IEnumerable<T>
{
    return source.SelectMany(x => (recursion(x) != null && recursion(x).Any()) ? recursion(x).Flatten(recursion) : null)
                 .Where(x => x != null);
}

你可以像这样使用它:

locationList.Flatten(x => x.Children).Select(x => x.ID);
于 2014-01-10T20:45:51.327 回答
7

我想贡献我自己的解决方案,该解决方案是根据以下参考资料修改的:

public static IEnumerable<T> Flatten<T, R>(this IEnumerable<T> source, Func<T, R> recursion) where R : IEnumerable<T>
{
    var flattened = source.ToList();

    var children = source.Select(recursion);

    if (children != null)
    {
        foreach (var child in children)
        {
            flattened.AddRange(child.Flatten(recursion));
        }
    }

    return flattened;
}

例子:

var n = new List<FamilyMember>()
{
    new FamilyMember { Name = "Dominic", Children = new List<FamilyMember>() 
        {
            new FamilyMember { Name = "Brittany", Children = new List<FamilyMember>() }
        }
    }
}.Flatten(x => x.Children).Select(x => x.Name);

输出:

  • 多米尼克
  • 布列塔尼

班级:

public class FamilyMember {
    public string Name {get; set;}
    public List<FamilyMember> Children { get; set;}
}

参考。https://stackoverflow.com/a/21054096/1477388

注意:找不到其他参考,但 SO 上的其他人发布了一个答案,我从中复制了一些代码。

于 2015-08-04T18:33:28.020 回答
4

实体框架目前不支持递归,因此您可以

  • 像您所做的那样依赖延迟加载子集合(注意 N+1 问题)
  • 查询任意深度的对象(这将是一个丑陋的查询,尽管您可以使用 System.Linq.Expressions 生成它)

唯一真正的选择是避免使用 LINQ 来表达查询,而是使用标准 SQL。

无论您是否首先使用代码,实体框架都能很好地支持这种情况。

对于代码优先,请考虑以下内容

var results = this.db.Database.SqlQuery<ResultType>(rawSqlQuery)

对于模型优先,考虑使用定义查询,我认为这是一个不错的选择,因为它允许进一步组合或存储过程。

要递归取回数据,您需要了解递归 CTE,假设您使用的是 SQL Server,并且它是 2005+ 版本

编辑:

这是对任意深度的递归查询的代码。我把它放在一起只是为了好玩,我怀疑它会非常有效!

var maxDepth = 5;

var query = context.Locations.Where(o => o.ID == 1);
var nextLevelQuery = query;

for (var i = 0; i < maxDepth; i++)
{
    nextLevelQuery = nextLevelQuery.SelectMany(o => o.Children);
    query = query.Concat(nextLevelQuery);
}

扁平化列表在变量查询中

于 2013-10-08T02:43:21.630 回答
2

创建列表以使用递归方式添加所有子项 public static List list = new List();

递归函数

 static  void GetChild(int id) // Pass parent Id
                {

                    using (var ctx =  new CodingPracticeDataSourceEntities())
                    {
                        if (ctx.Trees.Any(x => x.ParentId == id))
                        {
                            var childList = ctx.Trees.Where(x => x.ParentId == id).ToList();
                            list.AddRange(childList);
                            foreach (var item in childList)
                            {
                                GetChild(item.Id);
                            }

                        }

                    }
                }

样品模型

 public partial class Tree
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Nullable<int> ParentId { get; set; }
    }
于 2018-08-03T08:02:25.410 回答
1

正如@electricalbah 指出的那样,@NikhilAgrawal 接受的答案不会递归地得到所有子孙。

我确实错过了代码审查中@EricLippert 给出的答案。

https://codereview.stackexchange.com/a/5661/96658

static IEnumerable<T> DepthFirstTreeTraversal<T>(T root, Func<T, IEnumerable<T>> children)      
{
    var stack = new Stack<T>();
    stack.Push(root);
    while(stack.Count != 0)
    {
        var current = stack.Pop();
        // If you don't care about maintaining child order then remove the Reverse.
        foreach(var child in children(current).Reverse())
            stack.Push(child);
        yield return current;
    }
}

像这样调用:

static List<Location> AllChildren(Location start)
{
    return DepthFirstTreeTraversal(start, c=>c.Children).ToList();
}

我在下面做了一个例子SelectMany。如您所见,Immediate Window如果您使用该解决方案,您甚至不会获得父 ID。

在此处输入图像描述

于 2021-05-28T15:41:13.533 回答
0

假设LocationsDbSet<Location>您的数据库上下文中,这将解决您的问题“我正在寻找一些简单的方法......将所有 'Location' 及其子级获取到一个包含 Location.ID 的列表”。好像我错过了一些东西,所以请澄清如果是这样。

dbContext.Locations.ToList()
// IDs only would be dbContext.Locations.Select( l => l.ID ).ToList()
于 2013-10-08T02:21:41.797 回答
0

这是我扁平化孩子的方法。

private Comment FlattenChildComments(Comment comment, ref Comment tempComment)
    {
        if (comment.ChildComments != null && comment.ChildComments.Any())
        { 
            foreach (var childComment in comment.ChildComments)
            {
                tempComment.ChildComments.Add(childComment);
                FlattenChildComments(childComment, ref tempComment);
            }
        }
        comment.ChildComments = tempComment.ChildComments;
        return comment;
    }
于 2020-09-18T03:50:50.907 回答