Obtener el XPath a un XElement?

Tengo un XElement en lo profundo de un documento. Dado el XElement (¿y XDocument?), ¿Hay un método de extensión para obtener su XPath completo (es decir, absoluto /root/item/element/child eg /root/item/element/child )?

Por ejemplo, myXElement.GetXPath ()?

EDITAR: Bien, parece que pasé por alto algo muy importante. ¡Ups! El índice del elemento debe tenerse en cuenta. Vea mi última respuesta para la solución corregida propuesta.

Los métodos de extensiones:

 public static class XExtensions { ///  /// Get the absolute XPath to a given XElement /// (eg "/people/person[6]/name[1]/last[1]"). ///  public static string GetAbsoluteXPath(this XElement element) { if (element == null) { throw new ArgumentNullException("element"); } Func 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); } ///  /// Get the index of the given XElement relative to its /// siblings with identical names. If the given element is /// the root, -1 is returned. ///  ///  /// The element to get the index of. ///  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."); } } 

Y la prueba:

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

Y muestra de salida:

 /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] 

Eso debería resolver esto. ¿No?

Actualicé el código de Chris para tener en cuenta los prefijos del espacio de nombres. Solo se modifica el método GetAbsoluteXPath.

 public static class XExtensions { ///  /// Get the absolute XPath to a given XElement, including the namespace. /// (eg "/a:people/b:person[6]/c:name[1]/d:last[1]"). ///  public static string GetAbsoluteXPath(this XElement element) { if (element == null) { throw new ArgumentNullException("element"); } Func relativeXPath = e => { int index = e.IndexPosition(); var currentNamespace = e.Name.Namespace; string name; if (currentNamespace == null) { name = e.Name.LocalName; } else { string namespacePrefix = e.GetPrefixOfNamespace(currentNamespace); name = namespacePrefix + ":" + 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); } ///  /// Get the index of the given XElement relative to its /// siblings with identical names. If the given element is /// the root, -1 is returned. ///  ///  /// The element to get the index of. ///  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."); } } 

Esto es en realidad un duplicado de esta pregunta. Si bien no está marcado como la respuesta, el método en mi respuesta a esa pregunta es la única forma de formular inequívocamente el XPath a un nodo dentro de un documento XML que siempre funcionará bajo todas las circunstancias. (También funciona para todos los tipos de nodos, no solo para los elementos).

Como puede ver, el XPath que produce es feo y abstracto. pero aborda las preocupaciones que muchos que respondieron han planteado aquí. La mayoría de las sugerencias realizadas aquí producen una XPath que, cuando se utiliza para buscar en el documento original, producirá un conjunto de uno o más nodos que incluye el nodo de destino. Es ese “o más” ese es el problema. Por ejemplo, si tengo una representación XML de un DataSet, el XPath ingenuo para un elemento DataRow específico, /DataSet1/DataTable1 , también devuelve los elementos de todas las otras DataRows en la DataTable. No puede dejar de ambigüedad sin saber algo sobre cómo se enlala el XML (por ejemplo, ¿hay algún elemento de clave primaria?).

Pero /node()[1]/node()[4]/node()[11] , solo hay un nodo que devolverá, sin importar qué.

Permítanme compartir mi última modificación a esta clase. Básicamente, excluye el índice si el elemento no tiene hermanos e incluye espacios de nombres con el operador local-name (). Tenía problemas con el prefijo del espacio de nombres.

 public static class XExtensions { ///  /// Get the absolute XPath to a given XElement, including the namespace. /// (eg "/a:people/b:person[6]/c:name[1]/d:last[1]"). ///  public static string GetAbsoluteXPath(this XElement element) { if (element == null) { throw new ArgumentNullException("element"); } Func relativeXPath = e => { int index = e.IndexPosition(); var currentNamespace = e.Name.Namespace; string name; if (String.IsNullOrEmpty(currentNamespace.ToString())) { name = e.Name.LocalName; } else { name = "*[local-name()='" + e.Name.LocalName + "']"; //string namespacePrefix = e.GetPrefixOfNamespace(currentNamespace); //name = namespacePrefix + ":" + e.Name.LocalName; } // If the element is the root or has no sibling elements, no index is required return ((index == -1) || (index == -2)) ? "/" + 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); } ///  /// Get the index of the given XElement relative to its /// siblings with identical names. If the given element is /// the root, -1 is returned or -2 if element has no sibling elements. ///  ///  /// The element to get the index of. ///  public static int IndexPosition(this XElement element) { if (element == null) { throw new ArgumentNullException("element"); } if (element.Parent == null) { // Element is root return -1; } if (element.Parent.Elements(element.Name).Count() == 1) { // Element has no sibling elements return -2; } 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."); } } 

Como parte de un proyecto diferente, desarrollé un método de extensión para generar un XPath simple para un elemento. Es similar a la respuesta seleccionada, pero admite XAttribute, XText, XCData y XComment además de XElement. Está disponible como código nuget , página de proyecto aquí: xmlspecificationcompare.codeplex.com

Si buscas algo proporcionado de manera nativa por .NET, la respuesta es no. Tendría que escribir su propio método de extensión para hacer esto.

Puede haber varios xpaths que conducen al mismo elemento, por lo que encontrar el camino xpath más simple que conduce al nodo no es trivial.

Dicho esto, es bastante fácil encontrar un xpath para el nodo. Solo suba el árbol de nodos hasta que lea el nodo raíz y combine los nombres de los nodos y tenga un xpath válido.

Por “xpath completo” supongo que te refieres a una simple cadena de tags, ya que el número de xpaths que potencialmente podría coincidir con cualquier elemento podría ser muy grande.

El problema aquí es que es muy difícil, si no específicamente imposible, construir cualquier xpath dado que retroceda de manera reversible al mismo elemento. ¿Es eso una condición?

Si la respuesta es “no”, entonces quizás pueda generar una consulta mediante un bucle recursivo con referencia a los elementos actuales parentNode. Si la respuesta es “sí”, entonces se buscará ampliarla haciendo una referencia cruzada para la posición del índice dentro de conjuntos hermanos, haciendo referencia a los atributos tipo ID si existen, y esto dependerá mucho de su XSD si se trata de una solución general es posible.

Microsoft ha proporcionado un método de extensión para hacer esto desde .NET Framework 3.5:

http://msdn.microsoft.com/en-us/library/bb156083(v=vs.100).aspx

Simplemente agregue un uso a System.Xml.XPath e invoque los siguientes métodos:

  • XPathSelectElement : selecciona un elemento único
  • XPathSelectElements : selecciona elementos y regresa como IEnumerable
  • XPathEvaluate : seleccione nodos (no solo elementos, sino también texto, comentarios, etc.) y devuelva como IEnumerable