Flat Data to Hierarchical Model C#

2k views Asked by At

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

There are 1 answers

0
dbc On BEST ANSWER

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