Search code examples
c#xmlxpathxelement

Get the XPath to an XElement?


I've got an XElement deep within a document. Given the XElement (and XDocument?), is there an extension method to get its full (i.e. absolute, e.g. /root/item/element/child) XPath?

E.g. myXElement.GetXPath()?

EDIT: Okay, looks like I overlooked something very important. Whoops! The index of the element needs to be taken into account. See my last answer for the proposed corrected solution.


Solution

  • The extensions methods:

    public static class XExtensions
    {
        /// <summary>
        /// Get the absolute XPath to a given XElement
        /// (e.g. "/people/person[6]/name[1]/last[1]").
        /// </summary>
        public static string GetAbsoluteXPath(this XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
    
            Func<XElement, string> relativeXPath = e =>
            {
                int index = e.IndexPosition();
                string name = e.Name.LocalName;
    
                // If the element is the root, no index is required
    
                return (index == -1) ? "/" + name : string.Format
                (
                    "/{0}[{1}]",
                    name, 
                    index.ToString()
                );
            };
    
            var ancestors = from e in element.Ancestors()
                            select relativeXPath(e);
    
            return string.Concat(ancestors.Reverse().ToArray()) + 
                   relativeXPath(element);
        }
    
        /// <summary>
        /// Get the index of the given XElement relative to its
        /// siblings with identical names. If the given element is
        /// the root, -1 is returned.
        /// </summary>
        /// <param name="element">
        /// The element to get the index of.
        /// </param>
        public static int IndexPosition(this XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
    
            if (element.Parent == null)
            {
                return -1;
            }
    
            int i = 1; // Indexes for nodes start at 1, not 0
    
            foreach (var sibling in element.Parent.Elements(element.Name))
            {
                if (sibling == element)
                {
                    return i;
                }
    
                i++;
            }
    
            throw new InvalidOperationException
                ("element has been removed from its parent.");
        }
    }
    

    And the test:

    class Program
    {
        static void Main(string[] args)
        {
            Program.Process(XDocument.Load(@"C:\test.xml").Root);
            Console.Read();
        }
    
        static void Process(XElement element)
        {
            if (!element.HasElements)
            {
                Console.WriteLine(element.GetAbsoluteXPath());
            }
            else
            {
                foreach (XElement child in element.Elements())
                {
                    Process(child);
                }
            }
        }
    }
    

    And sample output:

    /tests/test[1]/date[1]
    /tests/test[1]/time[1]/start[1]
    /tests/test[1]/time[1]/end[1]
    /tests/test[1]/facility[1]/name[1]
    /tests/test[1]/facility[1]/website[1]
    /tests/test[1]/facility[1]/street[1]
    /tests/test[1]/facility[1]/state[1]
    /tests/test[1]/facility[1]/city[1]
    /tests/test[1]/facility[1]/zip[1]
    /tests/test[1]/facility[1]/phone[1]
    /tests/test[1]/info[1]
    /tests/test[2]/date[1]
    /tests/test[2]/time[1]/start[1]
    /tests/test[2]/time[1]/end[1]
    /tests/test[2]/facility[1]/name[1]
    /tests/test[2]/facility[1]/website[1]
    /tests/test[2]/facility[1]/street[1]
    /tests/test[2]/facility[1]/state[1]
    /tests/test[2]/facility[1]/city[1]
    /tests/test[2]/facility[1]/zip[1]
    /tests/test[2]/facility[1]/phone[1]
    /tests/test[2]/info[1]
    

    That should settle this. No?