Search code examples
c#treegroup-byhierarchyflat

Flat Data to Hierarchical Model C#


I have some flat data coming from the database that looks like this:

List<FlatDataGroup> elements = new List<FlatDataGroup>()
        {
            new FlatDataGroup {Text = "", GroupID = 1, ParentGroupID = 0, GroupName = "Admin", UserID = 1, UserName = "John Doe"},
            new FlatDataGroup {Text = "", GroupID = 1, ParentGroupID = 0, GroupName = "Admin", UserID = 2, UserName = "Jane Smith"},
            new FlatDataGroup {Text = "", GroupID = 2, ParentGroupID = 1, GroupName = "Support", UserID = 3, UserName = "Johnny Support"},
            new FlatDataGroup {Text = "", GroupID = 3, ParentGroupID = 2, GroupName = "SubSupport", UserID = 4, UserName = "Sub Johnny Support"},
            new FlatDataGroup {Text = "", GroupID = 4, ParentGroupID = 1, GroupName = "Production", UserID = 5, UserName = "Johnny Production"}
        };

I would like to convert it to this:

List<Group> model = new List<Group>
            {
                new Group()
                {
                    ID = 1,
                    Name = "Admin",
                    ParentGroupID = 0,
                    Type = "Group",
                    Users = new List<User>()
                    {
                        new User()
                        {
                            ID = 1,
                            Name = "John Doe",
                            GroupID = 1,
                            Type = "User",
                        },
                        new User()
                        {
                            ID = 2,
                            Name = "Jane Smith",
                            GroupID = 1,
                            Type = "User",
                        },
                    },
                    Groups = new List<Group>
                    {
                        new Group()
                        {
                            ID = 2,
                            Name = "Support",
                            ParentGroupID = 1,
                            Type = "Group",
                            Users = new List<User>()
                            {
                                new User()
                                {
                                    ID = 3,
                                    Name = "Johnny Support",
                                    GroupID = 2,
                                    Type = "User",
                                }
                            },
                            Groups = new List<Group>()
                            {
                                new Group()
                                {
                                    ID = 3,
                                    Name = "SubSupport",
                                    ParentGroupID = 2,
                                    Type = "Group",
                                    Users = new List<User>()
                                    {
                                        new User()
                                        {
                                            ID = 4,
                                            Name = "Sub Johnny Support",
                                            GroupID = 3,
                                            Type = "User",
                                        }
                                    },
                                    Groups = null
                                }
                            }
                        },
                        new Group()
                        {
                            ID = 4,
                            Name = "Production",
                            ParentGroupID = 1,
                            Type = "Group",
                            Users = new List<User>()
                            {
                                new User()
                                {
                                    ID = 5,
                                    Name = "Johnny Production",
                                    GroupID = 4,
                                    Type = "User",
                                }
                            },
                            Groups = null
                        }
                    }
                }
            };

which will ultimately display like this in a treeview:

+Admin (Group)
    John Doe (User)
    Jane Smith (User)
    +Support (Group)
        Johnny Support (User)
        +SubSupport (Group)
            Sub Johnny Support (User)
    +Production (Group)
        Johnny Production (User)

This is what I've come up with so far to transform the flat data into the model above:

List<Group> model = new List<Group>();

        var parentGrouping = elements.GroupBy(x => x.ParentGroupID);

        foreach (var parentGroup in parentGrouping)
        {
            var grouping = parentGroup.GroupBy(y => y.GroupID);

            foreach (var group in grouping)
            {
                Group groupItem = new Group()
                {
                    ID = group.FirstOrDefault().GroupID,
                    Name = group.FirstOrDefault().GroupName,
                    ParentGroupID = group.FirstOrDefault().ParentGroupID,
                    Type = "Group",
                    Users = new List<User>()
                };

                foreach (var user in group)
                {
                    groupItem.Users.Add(new User()
                        {
                            ID = user.UserID,
                            Name = user.UserName,
                            GroupID = user.GroupID,
                            Type = "User",
                        });
                }

                model.Add(groupItem);
            }
        }

All my groups come out along with their children users but the hierarchy is not preserved. I think I may need to do this recursively but I can't seem to get my head around it. Any help would be greatly appreciated.

Here are the models for the sake of completeness:

public class FlatDataGroup
{
    public string Text { get; set; }
    public int GroupID { get; set; }
    public int ParentGroupID { get; set; }
    public string GroupName { get; set; }
    public int UserID { get; set; }
    public string UserName { get; set; }
}

public class Group
{
    public int ID { get; set; }
    public int ParentGroupID { get; set; }
    public string Name { get; set; }
    public List<Group> Groups { get; set; }
    public List<User> Users { get; set; }
    public string Type { get; set; }
}

public class User
{
    public int ID { get; set; }
    public int GroupID { get; set; }
    public string Name { get; set; }
    public string Type { get; set; }
 }

Solution

  • I'd do this in 3 passes:

    1. Create all Group classes and populate them with data other than child groups, adding them incrementally to a dictionary mapping ID to Group.

    2. Loop through all the groups in the dictionary and add children to their parents' Groups list of children.

    3. Return a filtered list of all groups with no parent group -- these are the root groups. (I also sorted them by ID to remove the random ordering that the dictionary will introduce.)

    Thus:

    public static class FlatDataGroupExtensions
    {
        public const string UserType = "User";
        public const string GroupType = "Group";
    
        public static List<Group> ToGroups(this IEnumerable<FlatDataGroup> elements)
        {
            // Allocate all groups and index by ID.
            var groups = new Dictionary<int, Group>();
            foreach (var element in elements)
            {
                Group group;
                if (!groups.TryGetValue(element.GroupID, out group))
                    groups[element.GroupID] = (group = new Group() { ID = element.GroupID, Name = element.GroupName, ParentGroupID = element.ParentGroupID, Type = GroupType });
                group.Users.Add(new User() { GroupID = element.GroupID, ID = element.UserID, Name = element.UserName, Type = UserType });
            }
            // Attach child groups to their parents.
            foreach (var group in groups.Values)
            {
                Group parent;
                if (groups.TryGetValue(group.ParentGroupID, out parent) && parent != group) // Second check for safety.
                    parent.Groups.Add(group);
            }
            // Return only root groups, sorted by ID.
            return groups.Values.Where(g => !groups.ContainsKey(g.ParentGroupID)).OrderBy(g => g.ID).ToList();
        }
    }
    

    I also modified your Group class a little to automatically allocate the lists:

    public class Group
    {
        List<Group> groups = new List<Group>();
        List<User> users = new List<User>();
    
        public int ID { get; set; }
        public int ParentGroupID { get; set; }
        public string Name { get; set; }
        public string Type { get; set; }
        public List<Group> Groups { get { return groups; } }
        public List<User> Users { get { return users; } }
    
        public override string ToString()
        {
            return string.Format("Group: ID={0}, Name={1}, Parent ID={2}, #Users={3}, #Groups={4}", ID, Name, ParentGroupID, Users.Count, Groups.Count);
        }
    }