Pegue o XPath para um Xelement?
Pergunta
Eu tenho um Xelement no fundo de um documento. Dado o Xelement (e Xdocument?), Existe um método de extensão para obter seu completo (ou seja, absoluto, por exemplo /root/item/element/child
) Xpath?
Por exemplo, myxElement.getxPath ()?
EDIT: Ok, parece que eu esqueci algo muito importante. Opa! O índice do elemento precisa ser levado em consideração. Veja minha última resposta para a solução corrigida proposta.
Solução
Os métodos de extensão:
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.");
}
}
E o teste:
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);
}
}
}
}
E saída de amostra:
/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]
Isso deve resolver isso. Não?
Outras dicas
Atualizei o código da Chris para levar em consideração os prefixos do espaço para nome. Somente o método getABSolutexPath é modificado.
public static class XExtensions
{
/// <summary>
/// Get the absolute XPath to a given XElement, including the namespace.
/// (e.g. "/a:people/b:person[6]/c:name[1]/d: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();
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);
}
/// <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.");
}
}
Deixe -me compartilhar minha mais recente modificação para esta classe. Basicamente, exclui o índice se o elemento não tiver irmãos e inclui espaços para nome com o operador local-name () que eu estava tendo problemas com o prefixo de espaço para nome.
public static class XExtensions
{
/// <summary>
/// Get the absolute XPath to a given XElement, including the namespace.
/// (e.g. "/a:people/b:person[6]/c:name[1]/d: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();
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);
}
/// <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 or -2 if element has no sibling elements.
/// </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)
{
// 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.");
}
}
Isso é realmente uma duplicata de isto pergunta. Embora não esteja marcado como a resposta, o método em minha resposta Para essa questão, é a única maneira de formular inequivocamente o XPath para um nó em um documento XML que sempre funcionará em todas as circunstâncias. (Também funciona para todos os tipos de nós, não apenas elementos.)
Como você pode ver, o XPath que produz é feio e abstrato. Mas aborda as preocupações que muitos respondentes levantaram aqui. A maioria das sugestões feitas aqui produz um XPath que, quando usado para pesquisar no documento original, produzirá um conjunto de um ou mais nós que incluem o nó de destino. É isso "ou mais", esse é o problema. Por exemplo, se eu tiver uma representação XML de um conjunto de dados, o XPath ingênuo para um elemento específico de Datarow, /DataSet1/DataTable1
, também retorna os elementos de todos os outros dados do DataTable. Você não pode se desambiguar que, sem saber algo sobre como o XML é forumlado (como, existe um elemento de chave primária?).
Mas /node()[1]/node()[4]/node()[11]
, há apenas um nó que ele voltará, não importa o quê.
Como parte de um projeto diferente Desenvolvi um método de extensão para gerar um XPath simples para um elemento. É semelhante à resposta selecionada, mas suporta Xattribute, XText, Xcdata e XComment, além do Xelement. Está disponível como Código Nuget, página do projeto aqui: xmlspecificationCompare.codeplex.com
Se você está procurando algo fornecido nativamente pelo .NET, a resposta é não. Você teria que escrever seu próprio método de extensão para fazer isso.
Pode haver vários XPaths que levam ao mesmo elemento; portanto, encontrar o XPath mais simples que leva ao nó não é trivial.
Dito isto, é muito fácil encontrar um XPath no nó. Basta intensificar a árvore do nó até ler o nó raiz e combinar os nomes dos nó e você tem um XPath válido.
Por "Full XPath", suponho que você quer dizer uma simples cadeia de tags, pois o número de xpaths que poderia corresponder a qualquer elemento pode ser muito ampla.
O problema aqui é que é muito difícil, se não especificamente impossível, construir um determinado XPath que reversivelmente traçará ao mesmo elemento - essa é uma condição?
Se "não", talvez você possa construir uma consulta, voltando recursivamente com referência aos elementos atuais. Se "sim", então você estará tentando estendê-lo por referência cruzada para a posição do índice nos conjuntos de irmãos, referenciando atributos do tipo ID se houver, e isso será muito dependente do seu XSD se uma solução geral é possível.
A Microsoft forneceu um método de extensão para fazer isso desde o .NET Framework 3.5:
http://msdn.microsoft.com/en-us/library/bb156083(v=vs.100).aspx
Basta adicionar um uso a System.Xml.XPath
e invocar os seguintes métodos:
XPathSelectElement
: selecione um único elementoXPathSelectElements
: Selecione elementos e retorne como umIEnumerable<XElement>
XPathEvaluate
: Selecione nós (não apenas elementos, mas também texto, comentários etc.) e retorne como umIEnumerable<object>