Select an XML node via XPath at an arbitrary depth - c#

Having an XML document, I want to get the first node with a certain name, no matter in which nesting depth it is contained.
I tried several things without success:
var node1 = doc.SelectSingleNode(#"//Shortcut");
var node2 = doc.SelectSingleNode(#"/*/Shortcut");
var node3 = doc.SelectSingleNode(#"//*/Shortcut");
var node4 = doc.SelectSingleNode(#"*/Shortcut");
...
Each call results in a NULL node.
I think it should be some trivial XPath syntax. Can you help me?
(In case this matters: The XML document is an input file for a WiX project, so there could be some namespace issues involved?!?).
Edit
I also tried the following:
var nsm = new XmlNamespaceManager(doc.NameTable);
nsm.AddNamespace(string.Empty, #"http://schemas.microsoft.com/wix/2006/wi");
nsm.AddNamespace(#"ns", #"http://schemas.microsoft.com/wix/2006/wi");
together with:
var node1 = doc.SelectSingleNode(#"//Shortcut", nsm);
var node2 = doc.SelectSingleNode(#"/*/Shortcut", nsm);
var node3 = doc.SelectSingleNode(#"//*/Shortcut", nsm);
var node4 = doc.SelectSingleNode(#"*/Shortcut", nsm);
...
Leading to the same results.
Edit 2 - Solution
I found the solution:
var nsm = new XmlNamespaceManager(doc.NameTable);
nsm.AddNamespace(string.Empty, #"http://schemas.microsoft.com/wix/2006/wi");
nsm.AddNamespace(#"ns", #"http://schemas.microsoft.com/wix/2006/wi");
and then
var node1 = doc.SelectSingleNode(#"//ns:Shortcut", nsm);
This succeeded.

The XPath expression that selects exactly the wanted node (and nothing in addition) is:
(//x:Shortcut)[1]
So, using:
doc.SelectNodes("(//x:Shortcut)[1]", someNamespaceManager)
where
the prefix "x" is bound to the namespace "http://schemas.microsoft.com/wix/2006/wi"
in someNamespaceManager
This has an advantage over the proposed solution (to use SelectSingleNode()), because it can easily be adjusted to select the N-th wanted node in the XML document.
For example:
(//x:Shortcut)[3]
selects the 3rd (in document order) x:Shortcut element, and
(//x:Shortcut)[last()]
selects the last (in document order) x:Shortcut element in the XML document.

Why do not use XDocument?
XDocument doc = XDocument.Load("test.xml");
doc.Descendants("Shortcut").First();
IMO XDocument is faster and more readable than XPath.

I finally found the solution by myself:
var nsm = new XmlNamespaceManager(doc.NameTable);
nsm.AddNamespace(string.Empty, #"http://schemas.microsoft.com/wix/2006/wi");
nsm.AddNamespace(#"ns", #"http://schemas.microsoft.com/wix/2006/wi");
and then
var node1 = doc.SelectSingleNode(#"//ns:Shortcut", nsm);
This succeeded.

Related

C# How remove single xml parent element node without removing the contents inside the parent node

I am new to C#, kindly help me. How to remove the single xml element node 'PaymentRecord' and content inside that should not get deleted.
<Payments>
<PaymentRecord>
<PayId>2031</PayId>
<Reference>Policy03</Reference>
<StatusCode>ACV</StatusCode>
<MethodDetail>
<PaymentMethodDetail>
<CardPaymentDetails>
<CardHolderName>abcded</CardHolderName>
<CardTransactionDetails>
<StoredCard>N</StoredCard>
</CardTransactionDetails>
</CardPaymentDetails>
</PaymentMethodDetail>
</MethodDetail>
<CurrencyCode>USD</CurrencyCode>
</PaymentRecord>
</Payments>
I need to remove "PaymentRecord" element from the XML. I need like below
<Payments>
<PayId>2031</PayId>
<Reference>Policy03</Reference>
<StatusCode>ACV</StatusCode>
<MethodDetail>
<PaymentMethodDetail>
<CardPaymentDetails>
<CardHolderName>abcded</CardHolderName>
<CardTransactionDetails>
<StoredCard>N</StoredCard>
</CardTransactionDetails>
</CardPaymentDetails>
</PaymentMethodDetail>
</MethodDetail>
<CurrencyCode>USD</CurrencyCode>
</Payments>
I have tried my below code, but its deleting the complete node which I don't want to do :- here 'queuePayload' is the xml element
XmlNodeList payloadRecordList = queuePayload.SelectNodes("//Payments/PaymentRecord");
foreach (XmlElement singleNode in payloadRecordList)
{
XmlHelper.removeElem((XmlElement)singleNode.ParentNode, "//PaymentRecord");
XmlDocument xmlDoc = singleNode.OuterXml;
// my remaining logic goes based on "xmldoc" value - I will inserting this data to table
}
You can use System.Xml.Linq with its XDocument to achiev this. Load the input and create a new document out of it like:
XDocument inputDoc = XDocument.Load(inputFile, LoadOptions.None);
XDocument outputDoc = new XDocument();
var elements = inputDoc.Element("Payments").Elements().Select(pr => pr.Elements());
XElement newElement = new XElement("Payments");
foreach (var element in elements)
{
newElement.Add(element);
}
outputDoc.Add(newElement);
Console.WriteLine(outputDoc.ToString());
I think #Fructzwerg is slightly overcomplicating it.
You can remove the PaymentRecord node and add all its children to the root in one go.
var xDoc = XDocument.Load(filePath);
var paymentRecord = xDoc.Root.Element("PaymentRecord");
var nodes = xDoc.Root.Element("PaymentRecord").Elements();
paymentRecord.Remove();
xDoc.Root.Add(nodes);
Console.WriteLine(xDoc.ToString());
dotnetfiddle

Copy Node and change Value of an Attribute

I have the following XML File. I want to copy a new "Test" and change the ID of the Test. How is it possible?
I already can copy the nodes, unfortunately not on the correct position (see images) and I also canĀ“t change the ID.
Anyone have a solution for me?
Before:
After:
XmlDocument xmldoc = new XmlDocument();
xmldoc.Load(Before.xml");
XmlNode Set = xmldoc.DocumentElement;
string strXmlQuery = "/Toolings/Testing/Test1";
XmlNode NodeToCopy = Set.SelectSingleNode(strXmlQuery);
XmlNode NewNode = NodeToCopy.CloneNode(true);
NodeToCopy.AppendChild(NewNode);
Set.InsertAfter(NewNode, Set.LastChild);
XPathNavigator navigator = xmldoc.CreateNavigator();
navigator.MoveToRoot();
navigator.MoveToFirstChild();
navigator.MoveToFirstChild();
navigator.MoveToFirstChild();
navigator.MoveToFirstChild();
navigator.SetValue("5678");
xmldoc.Save(After.xml");
Here is an example using System.Xml.Linq.XDocument which is a much easier API than XmlDocument:
//You can also use Load(), this is just so I didn't have to make a file
XDocument doc = XDocument.Parse("<Toolings><Testing><Test><ID>1234</ID></Test></Testing></Toolings>");
//Grab the first Test node (change the predicate if you have other search criteria)
var elTest = doc.Descendants().First(d => d.Name == "Test");
//Copy the node, only necessary if you don't know the structure at design time
XElement el = new XElement(elTest);
el.Element("ID").Value = "5678";
//inject new node
elTest.AddAfterSelf(el);
doc.Save("After.xml");

C# XML XPath Query Not working

I cant get my Xpath query to work though on paper it should be right. I even tried to get a single node without the attribute and could not even get this ...
What am I doing wrong ?
var trxXml = new XmlDocument();
trxXml.Load(InputTrxFile);
XmlElement root = trxXml.DocumentElement;
var unitTestResult = trxXml.GetElementsByTagName("UnitTestResult");
foreach (XmlElement runinfo in unitTestResult)
{
// Find failed tests, works fine then...
string TestName = runinfo.GetAttribute("testName"); // works fine
// Want to find equivalent TestDefinitions/UnitTest
/* Tried
TestRun/TestDefinitions/UnitTest[#name='thetest']
/TestRun/TestDefinitions/UnitTest[#name='thetest']
TestDefinitions/UnitTest[#name='thetest']
/TestDefinitions/UnitTest[#name='thetest']
UnitTest[#name='thetest']
variations with no attribute test JUST to get a node
Example http://www.csharp-examples.net/xml-nodes-by-attribute-value/
*/
var xpath = string.Format(#"/TestRun/TestDefinitions/UnitTest[#name='{0}']", TestName);
XmlNode node = trxXml.SelectSingleNode(xpath);
XmlNode node2 = root.SelectSingleNode(xpath);
// These all return null
Any query that begins with a / represents an absolute path, i.e. it is from the root of the document. It seems your UnitTestResult element (at the very least) encloses your TestRun elements.
To have your query take into account the current context, you need to reference the current context. This can be selected using ..
Secondly, your XML elements all have a namespace, and this needs to make up part of your query. A prefix needs to be added to a namespace manager, used in your query and the manager passed into the query method.
So, taking these together, define the prefix:
var manager = new XmlNamespaceManager(new NameTable());
manager.AddNamespace("t", "http://microsoft.com/schemas/VisualStudio/TeamTest/2010");
Change your query:
./t:TestRun/t:TestDefinitions/t:UnitTest[#name='{0}']
And pass the namespace manager to the method:
trxXml.SelectSingleNode(xpath, manager);
XPath and XmlDocument are pain, though. This would all be a lot more straightforward in LINQ to XML:
var doc = XDocument.Load(InputTrxFile);
var unitTestQuery =
from result in doc.Descendants(ns + "UnitTestResult")
let name = (string)result.Attribute("testName")
from unitTest in result.Descendants(ns + "UnitTest")
where (string)unitTest.Attribute("name") == name
select unitTest;
var unitTest = unitTestQuery.Single();

Trouble reading iTunes XML feed

I am trying to read an XML feed from http://itunes.apple.com/us/rss/topsongs/limit=10/genre=2/xml.
I want to access the fields like this:
<im:price amount="1.29000" currency="USD">$1.29</im:price>
<im:releaseDate label="December 31, 1960">1960-12-31T16:00:00-07:00</im:releaseDate>
Here is what I have done so far:
var xml = "http://itunes.apple.com/us/rss/topsongs/limit=10/genre=2/xml";
XmlDocument doc = new XmlDocument();
doc.Load(xml);
XmlNodeList items = doc.SelectNodes("//entry");
foreach (var item in items) {
// Do something with item.
}
No luck, though. items is null. Why? What am I doing wrong?
You need to create a namespace manager to map the RSS and also the iTunes custom tags namespace URIs to short prefixes (itunes and im in the example below):
var xml = "http://itunes.apple.com/us/rss/topsongs/limit=10/genre=2/xml";
XmlDocument doc = new XmlDocument();
doc.Load(xml);
var namespaceManager = new XmlNamespaceManager(doc.NameTable);
namespaceManager.AddNamespace("itunes", "http://www.w3.org/2005/Atom");
namespaceManager.AddNamespace("im", "http://itunes.apple.com/rss");
XmlNodeList items = doc.SelectNodes("//itunes:entry", namespaceManager);
foreach (XmlNode item in items)
{
var price = item.SelectSingleNode("im:price", namespaceManager);
var releaseDate = item.SelectSingleNode("im:releaseDate", namespaceManager);
if (price != null)
{
Console.WriteLine(price.Attributes["amount"].InnerText);
}
if (releaseDate != null)
{
Console.WriteLine(releaseDate.Attributes["label"].InnerText);
}
}
For that specific feed you should get 10 entries.
It's in the docs as well:
If the XPath expression does not include a prefix, it is assumed that
the namespace URI is the empty namespace. If your XML includes a
default namespace, you must still use the XmlNamespaceManager and add
a prefix and namespace URI to it; otherwise, you will not get any
nodes selected. For more information, see Select Nodes Using XPath
Navigation.
Alternatively you can use a namespace-agnostic XPath (from here):
XmlNodeList items = doc.SelectNodes("//*[local-name() = 'entry']");
Finally, not sure why you said items is null. It cannot be. When running your original code you should get this:

stuck on xpath query

I have an xmlnodelist which results in the below snippet
<updi:ProductName xmlns:updi="urn:rosettanet:specification:universal:ProductIdentification:xsd:schema:01.04">Packet Processing Card (PPC) 16GB</updi:ProductName>
<ulc:AlternativeIdentifier xmlns:ulc="urn:rosettanet:specification:universal:Locations:xsd:schema:01.04">
<ulc:Authority>PID</ulc:Authority>
<ulc:Identifier>ASR5K-PPC-K9=</ulc:Identifier>
</ulc:AlternativeIdentifier>
<ulc:AlternativeIdentifier xmlns:ulc="urn:rosettanet:specification:universal:Locations:xsd:schema:01.04">
<ulc:Authority>CPN</ulc:Authority>
<ulc:Identifier />
</ulc:AlternativeIdentifier>
How can I grab the two authority and identifier tags? I tried adding a root element but I have issues with the namespaces. The original namespace declaration is very large.
Well, I doubt there's anything wrong with the XPath queries you've tried - more likely it's the namespaces that are tripping you up. There are a few ways to compensate/deal with this, ranging from "strip out all the namespaces" to using the XmlNamespaceManager - here's an example of that:
void Main()
{
var doc = new XmlDocument();
var namespaceMgr = new XmlNamespaceManager(doc.NameTable);
namespaceMgr.AddNamespace("updi", "urn:rosettanet:specification:universal:ProductIdentification:xsd:schema:01.04");
namespaceMgr.AddNamespace("ulc", "urn:rosettanet:specification:universal:Locations:xsd:schema:01.04");
doc.LoadXml(xml);
var authorityTags = doc.SelectNodes("//ulc:Authority", namespaceMgr);
var identifierTags = doc.SelectNodes("//ulc:Identifier", namespaceMgr);
}
Assuming xml is:
string xml = #"
<ROOT>
<updi:ProductName xmlns:updi=""urn:rosettanet:specification:universal:ProductIdentification:xsd:schema:01.04"">
Packet Processing Card (PPC) 16GB
</updi:ProductName>
<ulc:AlternativeIdentifier xmlns:ulc=""urn:rosettanet:specification:universal:Locations:xsd:schema:01.04"">
<ulc:Authority>PID</ulc:Authority>
<ulc:Identifier>ASR5K-PPC-K9=</ulc:Identifier>
</ulc:AlternativeIdentifier>
<ulc:AlternativeIdentifier xmlns:ulc=""urn:rosettanet:specification:universal:Locations:xsd:schema:01.04"">
<ulc:Authority>CPN</ulc:Authority>
<ulc:Identifier />
</ulc:AlternativeIdentifier>
</ROOT>";
The below will return all four nodes from your sample. I did have to wrap it in a root node for my testing. The | operator allows for the union functionality.
//ulc:AlternativeIdentifier/ulc:Authority | //ulc:AlternativeIdentifier/ulc:Identifier
I tested this using Notepad++ with the XPatherizerNPP plugin, which I highly recommend.
Something like this will do the trick:
XmlDocument doc = new XmlDocument();
doc.Load("YourXmlFile");
foreach (XmlNode node in doc.SelectNodes("//*[local-name() = \"Authority\"]"))
{
Console.WriteLine("Authority: " + node.InnerText);
}
foreach (XmlNode node in doc.SelectNodes("//*[local-name() = \"Identifier\"]"))
{
Console.WriteLine("Identifier: " + node.InnerText);
}
Basically, SelectNodes("//*[local-name() = \"Identifier\"]") tells it to search for node in the xml with that name, regardless of namespace, etc.

Categories