1

I'm working on building an object with nested hierarchy from a flattened list of BOM items (bill of materials) from SQL that contain the BOM level and the parent they belong to. Basically each parent that is an assembly of smaller sub parts needs to have its sub parts added to a collection within that object. I believe we have some parts that may iterate up to 10 levels deep, and I don't like the way this pattern is going.

This data set is from a stored procedure that runs a complicated CTE query, and I'm not interested in trying to make this work with Entity Framework, as we have a lot of databases in the mix, and we need better control over the SQL that we're writing (hence the stored procedure). Ultimately this will end up as JSON that I'll render as a collapsible tree in a browser.

Here's the ugly part that I'm hoping to clean up a bit. The gist of what's going on here before I begin iterating over the various levels is:

  1. Establish the first item (incomplete, but this is the main item).
  2. Execute a stored procedure (with parameter) to return a datatable.
  3. Create a flattened list of the objects I want to nest.
  4. Get the total number of nested levels that exist on the BOM.
  5. Iterate over the various levels in a very ugly way.
public BOMItemModel GetItemBOM(string item)
        {
            try
            {
                var bom = new BOMItemModel { PLPartNumber = item, BOMLevel = 0 };
                var par = new Hashtable();
                par.Add("@Item", item);
                var dt = db.GetDataTable("[part].[getItemBOM]", par);
                var bomList = new List<BOMItemModel>();
                foreach(DataRow r in dt.Rows)
                {
                    bomList.Add(MapBomItem(r));
                }
                var bomLevels = bomList.Max(x => x.BOMLevel);
                for(var i = 1; i < bomLevels; i++)
                {
                    foreach (var b in bomList.Where(x => x.BOMLevel == i).ToList())
                    {
                        if (i == 1)
                        {
                            bom.SubItems.Add(b);
                        }
                        else
                        {
                            if (i == 2)
                            {
                                var lvl2Items = bom.SubItems;
                                var parent1 = lvl2Items.FirstOrDefault(x => x.PLPartNumber == b.ParentPLNumber);
                                if (parent1 != null) parent1.SubItems.Add(b);
                            }
                            if (i == 3)
                            {
                                var lvl2Items = bom.SubItems;
                                foreach(var lvl2 in lvl2Items)
                                {
                                    var lvl3Items = lvl2.SubItems;
                                    var parent2 = lvl3Items.FirstOrDefault(x => x.PLPartNumber == b.ParentPLNumber);
                                    if (parent2 != null) parent2.SubItems.Add(b);
                                }
                            }
                            if (i == 4)
                            {
                                var lvl2Items = bom.SubItems;
                                foreach (var lvl2 in lvl2Items)
                                {
                                    var lvl3Items = lvl2.SubItems;
                                    foreach (var lvl3 in lvl3Items)
                                    {
                                        var lvl4Items = lvl3.SubItems;
                                        var parent3 = lvl4Items.FirstOrDefault(x => x.PLPartNumber == b.ParentPLNumber);
                                        if (parent3 != null) parent3.SubItems.Add(b);
                                    }
                                }
                            }
                            if (i == 5)
                            {
                                var lvl2Items = bom.SubItems;
                                foreach (var lvl2 in lvl2Items)
                                {
                                    var lvl3Items = lvl2.SubItems;
                                    foreach (var lvl3 in lvl3Items)
                                    {
                                        var lvl4Items = lvl3.SubItems;
                                        foreach (var lvl4 in lvl4Items)
                                        {
                                            var lvl5Items = lvl4.SubItems;
                                            var parent4 = lvl5Items.FirstOrDefault(x => x.PLPartNumber == b.ParentPLNumber);
                                            if (parent4 != null) parent4.SubItems.Add(b);
                                        }
                                    }
                                }
                            }
                            if (i == 6)
                            {
                                var lvl2Items = bom.SubItems;
                                foreach (var lvl2 in lvl2Items)
                                {
                                    var lvl3Items = lvl2.SubItems;
                                    foreach (var lvl3 in lvl3Items)
                                    {
                                        var lvl4Items = lvl3.SubItems;
                                        foreach (var lvl4 in lvl4Items)
                                        {
                                            var lvl5Items = lvl4.SubItems;
                                            foreach (var lvl5 in lvl5Items)
                                            {
                                                var lvl6Items = lvl5.SubItems;
                                                var parent5 = lvl6Items.FirstOrDefault(x => x.PLPartNumber == b.ParentPLNumber);
                                                if (parent5 != null) parent5.SubItems.Add(b);
                                            }
                                        }
                                    }
                                }
                            }
                        }

                    }
                }

                return bom;
            }
            catch (Exception ex)
            {
                Log.Error(ex.Message, ex);
            }
            return null;
        }

This is what my model or class looks like:

public class BOMItemModel
{
    public BOMItemModel()
    {
        SubItems = new List<BOMItemModel>();
    }
   public string ParentPLNumber { get; set; }
   public string PartNumber { get; set; }
   public string PartListName { get; set; }
   public string ItemNumber { get; set; }
   public string PLPartNumber { get; set; }
   public string PartType { get; set; }
   public string PLManufacturer { get; set; }
   public string PLDescription { get; set; }
   public decimal Qty { get; set; }
   public int BOMLevel { get; set; }
   public List<BOMItemModel> SubItems { get; set; }
}

The last property on this class is where I'm stuffing sub items of the parent item, and this can nest several levels deep.

  • Select all ordered by `PLPartNumber ASC, BOMLevel ASC`. Then the loop is straight forward, you could even use a `foreach`. You just need to compare the current `PLPartNumber` and `BOMLevel` with the previous and know if you have a new `BOMItemModel` or just a new level. If both are same continue filling the `SubItems`-list. – Tim Schmelter Jan 10 '19 at 15:58
  • I think I understand what you're saying, and that may simplify some of it. My real issue with this code is that I need to reference the level directly within the loop, where I'd prefer to continue moving down throughout the loop. It feels like there's a cleaner way (potentially moving some of this into another method?) to move all the way down to the bottom level without writing "if blocks" for each level. does that make sense? – Middle Class Lowlife Jan 10 '19 at 16:15

1 Answers1

1

Yes, the following would be much better:

for (var i = 1; i <= bomLevels; i++)
                {
                    foreach (var b in bomList.Where(x => x.BOMLevel == i).ToList())
                    {
                        if (i == 1)
                        {
                            bom.SubItems.Add(b);
                        }
                        else
                        {
                            var parent = bomList.FirstOrDefault(x => x.PLPartNumber == b.ParentPLNumber && b.BOMLevel - 1 == x.BOMLevel);
                            if (parent != null) parent.SubItems.Add(b);
                        }


                    }
                }

Also, I would throw an error when parent is not found. For a more generic approach have a look at my other example: Converting table in tree

Aldert
  • 4,209
  • 1
  • 9
  • 23
  • If you are ok with the answer, please flag as answered. – Aldert Jan 12 '19 at 07:58
  • This does appear to do the trick. I haven't fully verified all of the nodes on the tree, but I feel like this makes sense. At first glance, the output looks good. It's been tough to wrap my head around having multiple (and deeply) nested parent / child relationships, and your solution keeps it simple. Thanks for taking the time! – Middle Class Lowlife Jan 14 '19 at 12:17