koryakinp
koryakinp

Reputation: 4125

Convert flat XML to hierarchical structure using LINQ

I have an XML document in a flat format:

<root>
   <node-one>
      <parent-id />
      <node-id>1</node-id>
      <value>foo</value>
   </node-one>
   <node-two>
      <parent-id>1</parent-id>
      <node-id>2</node-id>
      <value>bar</value>
   </node-two>
   <node-three>
      <parent-id>1</parent-id>
      <node-id>3</node-id>
      <value>baz</value>
   </node-three>
   <node-four>
      <parent-id>3</parent-id>
      <node-id>4</node-id>
      <value>qux</value>
   </node-four>
</root>

I want to convert it to hierarchical tree-like structure like this:

<root>
   <node-one>
      <parent-id />
      <node-id>1</node-id>
      <value>foo</value>
      <node-two>
         <parent-id>1</parent-id>
         <node-id>2</node-id>
         <value>bar</value>
      </node-two>
      <node-three>
         <parent-id>1</parent-id>
         <node-id>3</node-id>
         <value>baz</value>
         <node-four>
            <parent-id>3</parent-id>
            <node-id>4</node-id>
            <value>qux</value>
         </node-four>
      </node-three>
   </node-one>
</root>

Is there an elegant way to achieve it using XmlDocument/XDocument ? Any help would greatly appreciated.

Upvotes: 0

Views: 440

Answers (2)

jdweng
jdweng

Reputation: 34419

Try a recursive algorithm

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;


namespace ConsoleApplication4
{
    class Program
    {
        const string FILENAME = @"c:\temp\test.xml";
        static List<XElement> nodes;
        static void Main(string[] args)
        {
            XDocument doc = XDocument.Load(FILENAME);

            nodes = doc.Root.Elements().ToList();

            XElement parent = new XElement("root");
            RecursvieAdd(parent, "");
            XDocument doc2 = new XDocument(parent);
        }
        static void RecursvieAdd(XElement parent, string parentId)
        {
            foreach(XElement child in nodes.Where(x => (string)x.Element("parent-id") == parentId))
            {
               XElement newChild = new XElement(child);
               parent.Add(newChild);
               string id = (string)child.Element("node-id");
               RecursvieAdd(newChild, id);
            }

        }
    }

}

Upvotes: 1

b0wter
b0wter

Reputation: 148

You can either use XPaths or Linq to get this done. Since I used XPath recently I am going to show you how to go this way.

(You might have to add some references to System.Xml in the reference browser.)

Check this sample code (including comments for hints):

using System;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;

namespace xmlTest
{
    internal class Program
    {
        public static void Main(string[] args)
        {
            var document = CreateDocument();

            // Gets all children of the root element whose name starts with "node".
            var nodeElements = document.XPathSelectElements("/root/*[starts-with(name(), 'node')]").ToList();
            // Creates Tuples in the fashion: { 1, $NODE_ONE }, { 2, $NODE_TWO }, ... 
            // This is done because some values might be skipped.
            var indexedNodes = nodeElements.Select(x => new Tuple<int, XElement>(int.Parse(x.Descendants("node-id").First().Value), x)).ToList();

            foreach(var indexedNode in indexedNodes)
            {
                var parentId = GetParentNodeId(indexedNode.Item2);
                if (parentId != null)
                {
                    // Remove the node from its parent.
                    indexedNode.Item2.Remove();
                    // Add the node to the new parent.
                    var newParent = indexedNodes.First(x => x.Item1 == parentId).Item2;
                    newParent.Add(indexedNode.Item2);
                }
            }
            Console.WriteLine(document.ToString());
        }

        static int? GetParentNodeId(XElement element) {
            try {
                var parentId = int.Parse(element.Descendants("parent-id").First().Value);
                return parentId;
            }
            catch // Add some appropriate error handling here. 
            {
                return null;
            }
        }

        private static XDocument CreateDocument()
        {
            const string xml =
                "<root> <node-one> <parent-id /> <node-id>1</node-id> <value>foo</value> </node-one> <node-two> <parent-id>1</parent-id> <node-id>2</node-id> <value>bar</value> </node-two> <node-three> <parent-id>1</parent-id> <node-id>3</node-id> <value>baz</value> </node-three> <node-four> <parent-id>3</parent-id> <node-id>4</node-id> <value>qux</value> </node-four> </root>";
            return XDocument.Parse(xml);
        }
    }
}

As you can see from the output all the changes you have done to the elements are already reflected in the XDocument. That's because when working with the X-classes all changed are made in place.

Edit: You will need to change some code to include this into your application, e.g. the CreateDocument method but that should be easy enough :)

Upvotes: 0

Related Questions