0

I have a recursive setup of Projects. The entity model is defined like this:

public class Project
{
    public int Id { get; set; }
    public int? ParentId { get; set; }
    public string ProjectType { get; set; } // "project" or "task"
    public string Title { get; set; }

    // The next three values are always 0 for projects of type "project".
    // Only projects of type "task" has these values set:
    public int EstimatedScheduleWeeks { get; set; }
    public int EstimatedScheduleDays { get; set; }
    public int EstimatedScheduleHours { get; set; }

    public List<Project> ChildProjects { get; set; }
}

A Project of type "project" can have unlimited levels of child projects, each of which can also have tasks. A Project of type "task" can not have child items.

In the view model I have the property CalculatedHours, which is supposed to summarize all the EstimatedSchedule-values from all the descendant Projects:

public int CalculatedHours
{
    get
    {
        if (ProjectType == "task")
        {
            return 
                (EstimatedScheduleWeeks * 40) + 
                (EstimatedScheduleDays * 8) + 
                EstimatedScheduleHours;
        }
        if (ProjectType == "project" && ChildProjects != null)
        {
            IEnumerable<ProjectViewModel> childProjects = 
                ItemDescendantsFlat(ChildProjects, Id);
            int weeks = childProjects.Select(w => w.EstimatedScheduleWeeks).Sum();
            int days = childProjects.Select(w => w.EstimatedScheduleDays).Sum();
            int hours = childProjects.Select(w => w.EstimatedScheduleHours).Sum();
            // A week has 40 hours, a day has 8:
            return (weeks * 40) + (days * 8) + hours;
        }
        return 0;
    }
}

private static IEnumerable<ProjectViewModel>
    ItemDescendantsFlat(IEnumerable<ProjectViewModel> src, int parentId)
{
    // https://stackoverflow.com/questions/48453796/linq-recursive-sum
    // NetMage's answer:
    var childItems = src.ToLookup(i => i.ParentId);

    var stackOfChildren = new Stack<IEnumerable<ProjectViewModel>>();
    stackOfChildren.Push(childItems[parentId]);
    do
        foreach (var c in stackOfChildren.Pop())
        {
            yield return c;
            stackOfChildren.Push(childItems[c.Id]);
        }
    while (stackOfChildren.Count > 0);
}

When I inspect childProjects in the CalculatedHours-property, all the Projects and descendants are there. But they are not in a flat list. They are still structured like a recursive tree. Maybe that is why the returned value is only the sum of the EstimatedSchedule-variables from the first level of Projects, with type "task".

I'm pretty sure the ItemDescendantsFlat()-method works. Maybe I'm not using it correctly? How can I summarize the EstimatedSchedule-values from all the descendants?

I'm referencing this question in the code (NetMage's answer).

Stian
  • 1,522
  • 2
  • 22
  • 52

1 Answers1

0

Here I create the test data and retrieve all projects & tasks using the below extension. After that a simple Where filters out all tasks.

var testdata = new Project(new Random(4));
var tasks = testdata.SelectChilds(x => x.children).Where(x => x.type == "task").ToList();

The extension. It iterates through all childs and returns a flat structure based on a func defined by the user.

public static IEnumerable<T> SelectChilds<T>(this T source, Func<T, IEnumerable<T>> func)
{
    yield return source;
    foreach (T element in func(source) ?? Enumerable.Empty<T>())
    {
        var subs = element.SelectChilds(func);
        foreach (T sub in subs)
        {
            yield return sub;
        }
    }
}

The class for the test data. Constructor randomly creates some childs.

public class Project
{
    public Guid id { get; set; } = Guid.NewGuid();
    public string type { get; set; }
    public List<Project> children { get; set; } = new List<Project>();

    public Project(Random rng)
    {
        var count = rng.Next(3);
        type = count == 0 ? "task" : "collection";
        for (var i = 0; i < count; i++)
        {
            children.Add(new Project(rng));
        }
    }
}
NotFound
  • 5,005
  • 2
  • 13
  • 33
  • I am not able to make your solution work. In the view model, where I'm calling the extension, I get "The name SelectChilds" does not exist in the current context. I had to make the extension method static in order to make it compile, and I have placed it in a folder called "Extensions". – Stian Nov 24 '19 at 13:20