I have the following xml received from a web service
<GRID xmlns="http://schemas.datastream.net/MP_functions/MP0118_GetGridHeaderData_001_Result">
<DATA>
<R>
<D>2645</D>
<D>HJIT.HRE#RGW.COM</D>
<D>2019-09-27 10:17:36.0</D>
<D>114041</D>
<D>Awaiting Planning</D>
<D>Work Planned</D>
</R>
<R>
<D>2649</D>
<D>HJIT.HRE#RGW.COM</D>
<D>2019-09-27 10:33:24.0</D>
<D>114043</D>
<D>Awaiting Release</D>
<D>Awaiting Planning</D>
</R>
<R>
<D>2652</D>
<D>HJIT.HRE#RGW.COM</D>
<D>2019-09-27 10:36:53.0</D>
<D>114041</D>
<D>Awaiting Planning</D>
<D>Work Planned</D>
</R>
</DATA>
</GRID>
I wrote the following piece of .NET code to extract the R nodes
HttpWebResponse resp = (HttpWebResponse)Req.GetResponse();
XPathDocument xpResDoc = new XPathDocument(resp.GetResponseStream());
XPathNavigator xpNav = xpResDoc.CreateNavigator();
XmlNamespaceManager nsmgr = new XmlNamespaceManager(xpNav.NameTable);
nsmgr.AddNamespace("g2", "http://schemas.datastream.net/MP_functions/MP0118_GetGridHeaderData_001_Result");
XPathNodeIterator xpNIter = xpNav.Select("//g2:R", nsmgr); // I can successfully get the three R elements
foreach (XPathNavigator nav in xpNIter)
{
/*
Now I want to iterate through each R element and use XPATH to select each of the six D nodes by its index position.
The order of the D nodes are a known dataset and I want to build a comma separated string by concatenating the value of each D node,
which will later be appended to a CSV file along with a pre-defined header row.
*/
/* I attempted the following XPATH */
// XPathNodeIterator xpDi = nav.Select("(//D)[1]"); -- This does not work and yields a null result
}
Now I want to iterate through each R element and use XPATH to select each of the six D nodes by its index position. The order of the D nodes are a known dataset and I want to build a comma separated string by concatenating the value of each D node, which will later be appended to a CSV file along with a pre-defined header row.
I didn't want to use anything like LINQ to XML as this is part of read-only data extraction program which needs to be as lite and as performant as possible.
What is the correct way to get the D elements by index with XPATH using the XPathNavigator ?
You have a few problems here:
xpNav.Select("//g2:R", nsmgr) does not work for the XML shown in your question.
This expression selects for nodes with local name R in the http://schemas.datastream.net/MP_functions/MP0118_GetGridHeaderData_001_Result namespace -- however in your actual XML none of the nodes are in this namespace. There's a namespace declaration xmlns:dstm="http://schemas.datastream.net/MP_functions/MP0118_GetGridHeaderData_001_Result" but it's not the default namespace, so none of the nodes are actually in it, as they aren't using the dstm: prefix.
Instead, you should do xpNav.Select("//R", nsmgr) (or better yet xpNav.Select("/*/DATA/R", nsmgr)).
In your question you wrote I can successfully get the three R elements so maybe this is a typo in the question.
nav.Select("(//D)[1]"); -- This does not work and yields a null result.
I cannot reproduce this exact problem -- XPathNavigator.Select()never returns null. It will throw an exception on a malformed query, but not return null.
What I can reproduce is that this always returns the same result for every <R>, specifically the value of the first <D> element, <D>2645</D>. Demo fiddle #1 here.
The problem here is that the recursive descent operator //D selects for all nodes named R in the entire document. To select only the nodes in the current <R> element you need to restrict the scope by prefacing the XPath query with .: nav.Select("(.//D)[1]") (or better yet, nav.Select("(./D)[1]")).
Incidentally, since you expect 6 child <D> nodes of <R> it will be more performant to run one single XPath query and collect all 6 into a list, rather than running 6 queries for each specific node:
var nodes = nav.Select("./D").Cast<XPathNavigator>().ToList();
You indicated that performance is important, but you are using the recursive descent operator // which can have bad performance.
From Effective Xml Part 2: How to kill the performance of an app with XPath…:
// (descendant-or-self axis)
This is a very common pattern that very often leads to serious performance problems. The way it works is that it flattens the whole subtree (the most common usage I saw is flattening the whole xml document) and then it looks for the specified elements. Now in the .NET Framework there aren’t any specific optimizations for this patterns and using it is costly...
Instead, it's better to specify the path directly.
Pulling all of the above together, your code should look something like:
//xpNav and nsmgr set up as in the question
var csvLines = xpNav.Select("/*/DATA/R", nsmgr).Cast<XPathNavigator>()
.Select(nav => string.Join(",", nav.Select("./D").Cast<XPathNavigator>()))
.ToList();
Demo fiddle #2 here.
Notes:
If the XML in your question has been incorrectly edited and the nodes <R> and <D> are really in the dstm: namespace after all, add the g2: prefix to the node names in the XPath queries like so:
var csvLines = xpNav.Select("/*/g2:DATA/g2:R", nsmgr).Cast<XPathNavigator>()
.Select(nav => string.Join(",", nav.Select("./g2:D", nsmgr).Cast<XPathNavigator>()))
.ToList();
Demo fiddle #3 here.
As an aside, you might want to check your assumption that XPathDocument will be more performant than LINQ to XML. I am not sure this will be the case.
I was on the right path, just needed to use the right method which allows to specify the namespace as seen below:
HttpWebResponse resp = (HttpWebResponse)Req.GetResponse();
XPathDocument xpResDoc = new XPathDocument(resp.GetResponseStream());
XPathNavigator xpNav = xpResDoc.CreateNavigator();
XmlNamespaceManager nsmgr = new XmlNamespaceManager(xpNav.NameTable);
nsmgr.AddNamespace("g2", "http://schemas.datastream.net/MP_functions/MP0118_GetGridHeaderData_001_Result");
XPathNodeIterator xpNIter = xpNav.Select("//g2:R", nsmgr);
foreach (XPathNavigator nav in xpNIter)
{
string r =
$"{nav.SelectSingleNode("./g2:D[1]", nsmgr).Value}," +
$"{nav.SelectSingleNode("./g2:D[2]", nsmgr).Value}," +
$"{nav.SelectSingleNode("./g2:D[3]", nsmgr).Value}," +
$"{nav.SelectSingleNode("./g2:D[4]", nsmgr).Value}," +
$"{nav.SelectSingleNode("./g2:D[5]", nsmgr).Value}," +
$"{nav.SelectSingleNode("./g2:D[6]", nsmgr).Value}";
Console.WriteLine(r);
}
// Start writing to a file stream;
Related
I'm trying to get values from a XML document using the iXF format, but I'm having some issues with the XPath syntax.
I have the following XML document
<SOAP_ENV:Envelope xmlns:NS2="http://www.ixfstd.org/std/ns/core/classBehaviors/links/1.0" xmlns:NS1="CATIA/V5/Electrical/1.0" xmlns:tns="IXF_Schema.xsd" xmlns:ixf="http://www.ixfstd.org/std/ns/core/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP_ENV="http://schemas.xmlsoap.org/soap/envelope/" xsi:schemaLocation="IXF_Schema.xsd ElectricalSchema.xsd">
<SOAP_ENV:Body>
<ixf:object id="Electrical Physical System00000089.1" xsi:type="tns:Harness">
<tns:Name>Electrical Physical System00000089.1</tns:Name>
</ixf:object>
<ixf:object id="X10(1)//X11(1)" xsi:type="tns:Wire">
<tns:Name>X10(1)//X11(1)</tns:Name>
<NS1:Wire>
<NS1:Length>763,752mm</NS1:Length>
<NS1:Color>RD</NS1:Color>
<NS1:OuterDiameter>1,32mm</NS1:OuterDiameter>
</NS1:Wire>
</ixf:object>
</SOAP_ENV:Body>
</SOAP_ENV:Envelope>
And i'm trying to find all the Wire objects and get the Name and Length values with the following code.
XmlDocument xlDocument = new XmlDocument();
xlDocument.Load(importFile);
XmlNamespaceManager nsManager = new XmlNamespaceManager(xlDocument.NameTable);
nsManager.AddNamespace("tns", "IXF_Schema.xsd");
nsManager.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
nsManager.AddNamespace("ixf", "http://www.ixfstd.org/std/ns/core/1.0");
nsManager.AddNamespace("NS1", "CATIA/V6/Electrical/1.0");
nsManager.AddNamespace("NS2", "http://www.ixfstd.org/std/ns/core/classBehaviors/links/1.0");
//Get all wire objects
XmlNodeList wires = xlDocument.SelectNodes("descendant::ixf:object[#xsi:type = \"tns:Wire\"]", nsManager);
foreach (XmlNode wire in wires)
{
string wireName;
string wireLength;
XmlNode node = wire.SelectSingleNode("./tns:Name", nsManager);
wireName = node.InnerText;
XmlNode node1 = wire.SelectSingleNode("./NS1:Wire/NS1:Length", nsManager);
wireLength = node1.InnerText;
}
I can get the wireName value without any problems but the Length element selection always returns 0 matches and I can not figure out why. I also tried to only select the Wire element using the same syntax as the Name element ./NS1:Wire but that also returns 0 matches.
Your XML declares
xmlns:NS1="CATIA/V5/Electrical/1.0"
^^
Your C# declares a different namespacem
nsManager.AddNamespace("NS1", "CATIA/V6/Electrical/1.0")
^^
Make sure both namespaces match exactly.
Regarding your comment asking about the use of version numbers in namespaces...
It is an unfortunately common but certainly not widely accepted practice to include a version number in an XML namespace. Realize that by doing so, you're effectively saying that every namespaced XML component (element or attribute) should now be considered to differ from its counterpart in the old namespace. This is rarely what you want.
See also
Should I use a Namespace of an XML file to identify its version
What are the best practices for versioning XML schemas?
I have an xml file generated by Vector CANeds. This file contains information about CANopen Objects I want to read with my tool written in C#.
The (very basic) structure of the xml is as follows:
<ISO15745ProfileContainer xmlns="http://www.canopen.org/xml/1.0">
<ISO15745Profile>
<ProfileHeader></ProfileHeader>
<ProfileBody xsi:type="ProfileBody_Device_CANopen"</ProfileBody>
</ISO15745Profile>
<ISO15745Profile>
<ProfileHeader></ProfileHeader>
<ProfileBody xsi:type="ProfileBody_CommunicationNetwork_CANopen"</ProfileBody>
</ISO15745Profile>
</ISO15745ProfileContainer>
When I create an XmlNodeList with both ISO15745Profile nodes in it and loop through then i get a strange behaviour. By accessing the subnodes with explicit indexes, everything is as expected. When I am using xpath, allways the first node is used.
Code snippet:
const string filepath = "CANeds1.xdd";
const string s_ns = "//ns:";
var mDataXML = new XmlDocument();
mDataXML.Load(filepath);
var root = mDataXML.DocumentElement;
XmlNamespaceManager nsm = new XmlNamespaceManager(mDataXML.NameTable);
nsm.AddNamespace("ns", root.Attributes["xmlns"].Value);
foreach (XmlNode node in root.ChildNodes) {
Console.WriteLine(" " + node.ChildNodes[1].Attributes["xsi:type"].Value);
Console.WriteLine(" " + node.SelectSingleNode(s_ns + "ProfileBody", nsm).Attributes["xsi:type"].Value);
}
Console output:
ProfileBody_Device_CANopen
ProfileBody_Device_CANopen
ProfileBody_CommunicationNetwork_CANopen
ProfileBody_Device_CANopen
Since node references the 2nd node, the last output should be commNetwork to.
Does somebody see my mistake? I have already tried to rename one of the "ISO15745Profile" nodes but this did not change the outcome. I may have messed up something with the namespace...
Some more explanation to the answer given in the comments:
The important point is the // XPath expression. The definition from MSDN says:
Recursive descent; searches for the specified element at any depth. When this path operator appears at the start of the pattern, it indicates recursive descent from the root node.
This means an expression starting with // will always search for occurences the entire document, even if it's called from a specific child note. That's why SelectSingleNode will always return the first match in the entire document.
To search relative to the node that calls the selection method there is the . operator which indicates the current context.
Put together, an expression starting with .// will search for all occurrences of the following pattern, beginning at the current node.
In the specific case, this means changing //ns: to .//ns: to get the expected result.
I have an XML string like below:
<root>
<Test1>
<Result time="2">ProperEnding</Result>
</Test1>
<Test2></Test2>
I have to operate on these elements. Most of the time the elements are unique within their parent element. I am using XDocument. I can remember that there is a way to access an element like this.
XNode resultTest1 = GetNodes("/root//Test1//result")
But I forgot it. It is possible to access the same using linq:
doc.root.Elements.etc.etc.
But I want it using a single string as shown above. Can anybody say how to make it?
Descendants() will skip any number level of intermediate nodes, e.g. this will skip over root and Test1:
doc.Decendants("Result")
Also note that you can use XPath with Linq2Xml as well, e.g. XPathSelectElements
doc.XPathSelectElements("/root/Test1/Result");
You can skip intermediate levels of the hierarchy with // (or use // at the start of the xpath string to skip the root)
"/root//Result"
One caveat - Xml is case sensitive , so Result and result are not the same element.
The string you're referring to ("/root//Test1//result") is an XPath expression.
You can use it with LINQ to XML classes (like XDocument) using XPathEvaluate, XPathSelectElement, and XPathSelectElements extension methods.
You can find more info about these methods on MSDN: http://msdn.microsoft.com/en-us/library/vstudio/system.xml.xpath.extensions_methods(v=vs.90).aspx
To make them work, you need using System.Xml.XPath at the top of your file and System.Xml.Linq.dll assembly referenced (which is probably already there).
You can try to load your xml using XDocument:
// loads xml file with root element
XDocument xml = XDocument.Load("filename.xml");
Now you can append LINQ statements to your xml variable like this:
var retrieveSomeSpecificDataLikeListOfElementsAsAnonymousObjects = xml.Descendants("parentNodeName").Select(node => new { SomeSpecialValueYouWant = node.Element("elementNameUnderParentNode").Value }).ToList();
You can mix and do whatever you want - above is just an example.
Is this what you looking?
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.LoadXml("YourXML");
XmlNodeList xmlNodes = xmlDocument.SelectNodes("/root/Test1/result");
Somewhere in a C# program, I need to get an attribute value from an xml structure. I can reach this xml structure directly as an XElement and have a simple xpath string to get the attribute. However, using XPathEvaluate, I get an empty array most of the time. (Yes, sometimes, the attribute is returned, but mostly it isn't... for the exact same XElement and xpath string...)
However, if I first convert the xml to string and reparse it as an XDocument, I do always get the attribute back. Can somebody explain this behavior ? (Am using .NET 3.5)
Code that mostly returns an empty IEnumerable:
string xpath = "/exampleRoot/exampleSection[#name='test']/#value";
XElement myXelement = RetrieveXElement();
((IEnumerable)myXElement.XPathEvaluate(xpath)).Cast<XAttribute>().FirstOrDefault().Value;
Code that does always work (I get my attribute value):
string xpath = "/exampleRoot/exampleSection[#name='test']/#value";
string myXml = RetrieveXElement().ToString();
XDocument xdoc = XDocument.Parse(myXml);
((IEnumerable)xdoc.XPathEvaluate(xpath)).Cast<XAttribute>().FirstOrDefault().Value;
With the test xml:
<exampleRoot>
<exampleSection name="test" value="2" />
<exampleSection name="test2" value="2" />
</exampleRoot>
By suggestion related to a surrounding root, I did some 'dry tests' in a test program, using the same xml structure (txtbxXml and txtbxXpath representing the xml and xpath expression described above):
// 1. XDocument Trial:
((IEnumerable)XDocument.Parse(txtbxXml.Text).XPathEvaluate(txtbxXPath.Text)).Cast<XAttribute>().FirstOrDefault().Value.ToString();
// 2. XElement trial:
((IEnumerable)XElement.Parse(txtbxXml.Text).XPathEvaluate(txtbxXPath.Text)).Cast<XAttribute>().FirstOrDefault().Value.ToString();
// 3. XElement originating from other root:
((IEnumerable)(new XElement("otherRoot", XElement.Parse(txtbxXml.Text)).Element("exampleRoot")).XPathEvaluate(txtbxXPath.Text)).Cast<XAttribute>().FirstOrDefault().Value.ToString();
Result : case 1 and 3 produce the correct result, while case 2 throws a nullref exception.
If case 3 would fail and case 2 succeed, it would have made some sense to me, but now I don't get it...
The problem is that the XPath expression is starting with the children of the specified node. If you start with an XDocument, the root element is the child node. If you start with an XElement representing your exampleRoot node, then the children are the two exampleSection nodes.
If you change your XPath expression to "/exampleSection[#name='test']/#value", it will work from the element. If you change it to "//exampleSection[#name='test']/#value", it will work from both the XElement and the XDocument.
I need to get a list of tags that contain a specific attribute. I am using DITA xml and I need to find out all tags that has a href attribute.
The problem here is that the attribute may be inside any tag so XPath will not work in this case. For example, an image tag may contain a href, a topicref tag may contain a href, and so on.
So I need to get a XmlNodeList (as returned by the getElementByTagName method). Ideally I need a method getElementByAttributeName that should return XmlNodeList.
I might have misunderstood your problem here, but I think you could possibly use an XPath expression.
var nodes = doc.SelectNodes("//*[#href='pic1.jpg']");
The above should return all elements with href='pic1.jpg', where doc is the XmlDocument
If you're on C#, then the following approach might work for you:
XDocument document = XDocument.Load(xmlReader);
XAttribute xa = new XAttribute("href", "pic1.jpg");
var attrList = document.Descendants().Where (d => d.Attributes().Contains(xa));