Как рекурсивно загрузить все дочерние записи?

У меня есть этот класс проекта:

public class Project
{
    public int Id { get; set; }
    public int? ParentId { get; set; }
    public List<Project> ChildProjects { get; set; }
    // more properties
}

Это моя попытка загрузить всех потомков любого данного проекта:

private async Task<List<Project>> LoadDescendantsOf(Project project)
{
    project.ChildProjects = await db.Projects
        .Where(p => p.ParentId == project.Id)
        .ToListAsync();
    foreach (Project childProject in project.ChildProjects)
    {
        yield return childProject.ChildProjects = 
            await LoadDescendantsOf(childProject);
    }
}

... но это не работает. Сообщение об ошибке, которое я получаю,

Тело «ProjectsController.LoadDescendantsOf(Project)» не может быть блоком итератора, поскольку «Task>» не является типом интерфейса итератора.

Я попытался сделать метод синхронным, но это то же сообщение об ошибке, только без части «Задача».

Как я могу заставить его работать?


person Stian    schedule 13.10.2019    source источник
comment
Я не знаю, можно ли использовать yield в сочетании с задачами, но yield будет работать только тогда, когда тип возвращаемого значения IEnumerable<T>. Итак, вы можете попробовать изменить тип возврата на Task<IEnumerable<Project>>   -  person Jeroen van Langen    schedule 13.10.2019
comment
yield return childProject.ChildProjects = await LoadDescendantsOf(childProject); как-то странно.   -  person Jeroen van Langen    schedule 13.10.2019


Ответы (1)


Вы можете написать простое расширение для этого:

    public static IEnumerable<T> Traverse<T>(this T e, Func<T, IEnumerable<T>> childrenProvider)
    {
        return TraverseMany(new[] { e }, childrenProvider);
    }

    public static IEnumerable<T> TraverseMany<T>(this IEnumerable<T> collection, Func<T, IEnumerable<T>> childrenProvider)
    {
        var stack = new Stack<T>();
        foreach(var c in collection)
        {
            stack.Push(c);
        }
        while (stack.Count > 0)
        {
            var i = stack.Pop();
            yield return i;
            var children = childrenProvider(i);
            if (children != null)
            {
                foreach (var c in children)
                {
                    stack.Push(c);
                }
            }
        }
    }

И использование:

var allProjectIds = p.Traverse(x => x.ChildProjects).Select(x => x.Id).ToList();

Если вы хотите загрузить подпроекты, я бы порекомендовал для этого написать рекурсивную процедуру SQL для курсоров, но это также хорошо подойдет для небольших данных:

var allProjectIds = p
    .Traverse(x => 
    {
       x.ChildProjects = db.Projects
                           .Where(p => p.ParentId == project.Id)
                           .ToList();
       return x.ChildProjects;
    })
    .Select(x => x.Id)
    .ToList();
person eocron    schedule 13.10.2019
comment
Есть ли ошибка в вашем TraverseMany-методе? Я получаю ошибку времени компиляции на e - не существует в текущем контексте. - person Stian; 14.10.2019
comment
Спасибо, что указали на это. Я его не компилировал. Исправлена ​​ошибка компиляции. - person eocron; 14.10.2019
comment
Что такое p в ваших примерах использования? - person Stian; 16.10.2019
comment
Это любой конкретный проект. - person eocron; 16.10.2019
comment
Кажется, я застрял в бесконечном цикле со вторым примером использования. - person Stian; 16.10.2019
comment
Попробуйте отладить и найти решение. Если вы не можете его найти - создайте хорошо отформатированный вопрос, и сообщество постарается вам помочь. Удачи! - person eocron; 16.10.2019