Search code examples
c#xmllinq-to-xml

Replace child name by concatenating its parent name, move up at level of parent, and remove the parent element using Ling to Xml


I have this original Xml:

    <?xml version="1.0" encoding="utf-8"?>
    <Home>
       <Kitchen>
          <Pantry>
             <Ingredients>
                <Name>Tomato</Name>
                <Price>
                   <ID>1</ID>
                   <Name>SALES</Name>
                </Price>
                <Cost>
                   <ID>2</ID>
                   <Name>COGS</Name>
                </Cost>
              </Ingredients>
           </Pantry>
        </Kitchen>
    </Home>

And I want it to look like this, concatenated price/cost-id/name:

    <?xml version="1.0" encoding="utf-8"?>
    <Home>
       <Kitchen>
          <Pantry>
             <Ingredients>
                <Name>Tomato</Name>
                <Price_ID>1</Price_ID>
                <Price_Name>SALES</Price_Name>
                <Cost_ID>2</Cost_ID>
                <Cost_Name>COGS</Cost_Name>
              </Ingredients>
           </Pantry>
        </Kitchen>
    </Home>

Here's my code so far, and I am getting an error in this line xChild.ReplaceWith(new XElement(concatName, xChild.Value.ToString())); -- System.NullReferenceException: 'Object reference not set to an instance of an object.'

    // source programmatically generated
    XElement xml = XElement.Parse(xml.ToString());

    foreach (XElement xParent in xQuery.Descendants("Ingredients").Nodes())
    {
        if (xParent.HasElements)
        {
            string parentName = xParent.Name.ToString();
    
            foreach (XElement xChild in xParent.DescendantsAndSelf(xParent.Name).Nodes())
            {
                string childName = xChild.Name.ToString();
                string concatName = xParent.Name.ToString() + "_" + xChild.Name.ToString();
    
                XElement child = xChild.Element(childName); //looks like this is the culprit
    
                xChild.ReplaceWith(new XElement(concatName, xChild.Value.ToString())); //error here: System.NullReferenceException: 'Object reference not set to an instance of an object.'
                XElement concatElement = xChild.Element(concatName);
                xChild.AddBeforeSelf(concatElement); 
            }
    
            foreach (XElement elParent in xQuery.Descendants("Ingredients"))
            {
                if (elParent.Element(parentName) != null)
                {
                    XElement parentEl = xParent.Element(parentName);
                    parentEl.Remove();
                }
            }
        }
    }

Solution

  • Your primary issue is that your .Nodes() list is changing while you are enumerating it. You need to use .ToList or .ToArray on it.

    It feels like you are over-complicating this anyway. You can just remove each child node, rename it, and add back as a sibling of its parent.

    var ingredients = xQuery.Descendants("Ingredients");
    foreach (XElement xParent in ingredients.Nodes().ToList())
    {
        if (!xParent.HasElements)
        continue;
                    
        string parentName = xParent.Name.ToString();
    
        foreach (XElement xChild in xParent.Nodes().ToArray())
        {
            string concatName = xParent.Name + "_" + xChild.Name;
            xChild.Remove();
            xChild.Name = concatName;
            xParent.AddBeforeSelf(xChild);
        }
        xParent.Remove();
    }
    

    dotnetfiddle