【问题标题】:Get All Children to One List - Recursive C#将所有孩子都放在一个列表中 - 递归 C#
【发布时间】:2013-10-08 02:03:55
【问题描述】:

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 更清洁、更容易,从 cmets Linq 和 Entity 来看,这似乎不是最好的选择。感谢大家的帮助!

【问题讨论】:

  • 实体框架不包含与递归查询有关的任何内容。
  • 是的,我正在寻找扩展此功能,请参阅我的编辑。
  • 我假设您想要一个实体框架解决方案,而不是一个由实体框架延迟加载支持的 Linq To Object 解决方案......我查看了实体框架 6 源代码并希望实际添加功能...... .但是微软将相关类设置为internal。 BAS$%^DS
  • 最终在 SQL 中使用递归并引用了一个 SP。感谢您的帮助!

标签: c# linq entity-framework recursion


【解决方案1】:

你可以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();

【讨论】:

  • 这个会遍历多个层级。假设一个位置是否有孩子,而这些孩子有孩子?
  • +1 这比 Syzmon 的答案要好,并且可能是您可以在没有任何数据库结构的情况下使用 EF 开箱即用的最佳答案。但是,您仍将进行 O(levels) 数据库调用。
  • 将其写为递归 SQL 并将其放入存储过程中会更好吗?我真的只是在寻找 ID,并不真正关心拥有整个 Entity 对象。
  • @Will:如果您只需要孩子的身份证,请查看我编辑的答案。
  • 这不会递归得到所有的子孙
【解决方案2】:

这样就可以了:

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

【讨论】:

  • 如果你要投反对票,至少有礼貌地说出原因。
  • 这只会从当前集合下面取回孩子。如果您希望最终结果中包含这些子项,则需要合并源集合
  • @Slicksim 不是这样,再试一次。
  • 投反对票,因为它是一种低效的解决方案。调用Any,然后调用Concat,然后调用SelectManyRecursive 到延迟的枚举会导致对枚举的多次评估,以及随后对selector lambda 的多次调用。不是为树中的每个元素调用一次 lambda,而是每个元素调用约 3 次。
【解决方案3】:

我的模型中没有 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)

【讨论】:

  • 完美!我的设置与您的模型完全相同(ID (PK)、父 ID 和子 ID)。此方法非常适合获取父 ID 的完整层次结构。我知道有一种方法可以通过递归或其他方式来实现,但这是一个纯 Linq 示例,它也适用于 EF!
【解决方案4】:

试试这个扩展方法:

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

【讨论】:

  • 代码的写法,不需要R泛型参数。
  • 由于多次调用 recursion lambda 以及对结果可枚举的多次评估而被否决。
【解决方案5】:

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

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 上的其他人发布了一个答案,我从中复制了一些代码。

【讨论】:

  • 代码的写法,不需要R泛型参数。
  • 我不明白您应该如何调用扩展方法 child.Flatten() 因为 child 不是 IEnumerable 而只是 T
  • @CarterăVeaceslav 在示例中,Children 将始终是一个列表;它可能是空的。如果您的代码工作方式不同,那么您可以检查类型以查看它是否为 IEnumerable,即 return typeof(IEnumerable).IsAssignableFrom(type); Ref。 stackoverflow.com/questions/28701867/…
【解决方案6】:

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

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

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

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

对于代码优先,考虑类似于

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

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

要递归取回数据,您需要了解递归 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);
}

扁平化列表在变量查询中

【讨论】:

  • 这就是我最终要做的。感谢您的帮助。
【解决方案7】:

创建列表以递归方式添加所有子项 公共静态列表列表=新列表();

递归函数

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

【讨论】:

    【解决方案8】:

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

    我确实想念 @EricLippert 在 Code Review 上给出的答案。

    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。

    【讨论】:

      【解决方案9】:

      假设 Locations 在您的 DB 上下文中是 DbSet&lt;Location&gt;,这将解决您的问题“我正在寻找一些简单的方法......将所有 'Location' 及其子项放到一个包含 Location 的列表中.ID 的”。好像我遗漏了什么,所以请澄清一下。

      dbContext.Locations.ToList()
      // IDs only would be dbContext.Locations.Select( l => l.ID ).ToList()
      

      【讨论】:

        【解决方案10】:

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

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

        【讨论】:

        • 不要试图编辑掉代码。如果这不是您想保留的答案,请删除答案。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-01-27
        • 2022-06-15
        • 2022-01-27
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多