Inconsistent XPATH query results - c#

I'm in the process of running a conversion process to update some of our xml files. Here's a sample file:
<?xml version="1.0" encoding="utf-8"?>
<Jobs xmlns="urn:mynamespace" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Job name="Job1">
<Category>Maintenance</Category>
<Description>Purge records</Description>
<Steps>
<TSql name="Start Job" database="IADS">
<CommandText>Exec StoredProcedureName</CommandText>
<OnSuccess action="GotoNextStep" />
<Retries>0</Retries>
<OnFailure action="QuitFailure" />
<OutputFile />
</TSql>
<TSql name="Start LoadRequestManagementReportTables" database="msdb">
<CommandText>exec sp_start_job #job_name = 'Load Request Management Report Tables'</CommandText>
<OnSuccess action="QuitSuccess" />
<Retries waitInMinutes="0">0</Retries>
<OnFailure action="QuitFailure" />
<OutputFile />
</TSql>
</Steps>
<Schedules>
<Schedule>
<Weekly name="Every Sunday at 7:00 AM" enabled="false">
<BeginDate>2008-11-01</BeginDate>
<RunTimes>
<Once>07:00</Once>
</RunTimes>
<DaysOfWeek>Sunday</DaysOfWeek>
</Weekly>
</Schedule>
<Schedule>
<OneTime name="OneTime" enabled="false" rundate="2011-01-12T03:00:00" />
</Schedule>
</Schedules>
<Notifications>
<EventLog on="Failure" />
</Notifications>
</Job>
</Jobs>
Now, I'm trying to rename the first step in each of the files that I process to convert it from a TSql step to a CmdExec step. Therefore, I wrote my own "RenameNode" method to do this conversion and copy all attributes and nodes to the newly-named node.
Here's the RenameNode method:
private static void RenameNode(XmlNode node, string namespaceURI, string newName)
{
if (node.NodeType != XmlNodeType.Element)
return;
XmlElement oldElement = (XmlElement)node;
XmlElement newElement = node.OwnerDocument.CreateElement(newName, namespaceURI);
while (oldElement.HasAttributes)
newElement.SetAttributeNode(oldElement.RemoveAttributeNode(oldElement.Attributes[0]));
while (oldElement.HasChildNodes)
newElement.AppendChild(oldElement.FirstChild);
if (oldElement.ParentNode != null)
oldElement.ParentNode.ReplaceChild(newElement, oldElement);
}
The problem I'm having is that the second query I run is not returning results.
Here's query #1 that I run:
XmlNodeList stepNodes = xSchedule.SelectNodes("/mns:Jobs/mns:Job/mns:Steps/mns:TSql", nsm);
This works great, returns 2 nodes in my stepNodes variable. I process the "RenameNode" method for stepNodes[0]. Fantastic.
The next thing I want to do is remove the namespace attributes that the "RenameNode" method generates (I'm guessing ya'll will not like that....but that's not the issue). So, in order to do that, I try running a very similar XPATH query that looks like this, but it does NOT return any records:
stepNodes = xSchedule.SelectNodes("/mns:Jobs/mns:Job/mns:Steps/mns:CmdExec", nsm);
I've tried resetting the namespace manager, I've tried saving the xml file (xSchedule.Save()), tried running the query with and without the namespace prefixes/parameter, etc. It NEVER returns any nodes. Even after doing the rename to the node, after saving the document I can see that it successfully renamed that node (and thus added the xmlns attribute to EVERYTHING in that newly-named node). I even verified that the original query that returns results now returns only 1 node this time.
// Let's assume that the very first node is the node that we want to change
XmlNodeList stepNodes = xSchedule.SelectNodes("/mvst:Jobs/mvst:Job/mvst:Steps/mvst:TSql", nsm);
if (stepNodes.Count >= 1)
{
RenameNode(stepNodes[0], String.Empty, "CmdExec");
// After renaming the node, let's remove the "database" attribute if it exists
//XmlElement e = (XmlElement)stepNodes[0];
//e.RemoveAttribute("database");
xSchedule.Save(scheduleXmlFile + ".bak");
}
Where am I going wrong with this code?

Just need to pass the namespace information to the RenameNode method.
RenameNode(stepNodes[0], "urn:mynamespace", "CmdExec");

Related

How do I find a specific XML attribute in a specific place in an XDocument

I have an example of my XML below (which i'm converting from SGML to XML prior to the check but that's besides the point). The XML IS valid at the point of checking.
I need check the XDocument and find out if any of the <version> elements contained within the <status> section at the top of the file contain the attriubute "RNWB". I need to be explicit about the fact that i'm only interested in checking <version> elements that are children of <status> elements because there might be other <version> elements within the document which i don't care about.
<dmodule>
<idstatus>
<dmaddres>
<dmc>Blah</dmc>
<dmtitle><techname>System</techname><infoname>Introduction</infoname></dmtitle>
<issno issno="006" type="revised"/>
<issdate year="2016" month="11" day="30"/>
</dmaddres>
<status>
<security class="2"/>
<rpc></rpc>
<orig></orig>
<applic>
<model model="2093">
<version version="BASE"></version>
<version version="RNWB"></version></model>
</applic>
<techstd>
<autandtp>
<authblk></authblk>
<tpbase></tpbase>
</autandtp>
<authex></authex>
<notes></notes>
</techstd>
<qa>
<firstver type="tabtop"/></qa>
<remarks></remarks>
</status>
</idstatus>
<content>
<refs><norefs></refs>
<descript>
<para0><title>Introduction</title>
<para>The system many new features included which are safe and accurate blah blah blah.</para>
</descript>
</content>
</dmodule>
I've tried all sorts but can't seem to get a result. Here's one example of what i've tried:
var result = (from ele in doc.Descendants("applic").Descendants("version").Attributes("RNWB")
select ele).ToList();
foreach (var v in result)
{
File.Move(file, Path.Combine(outputFolder, fileName)); // move the file to the new folder
}
If you have to be explicit that the version element is within a status element, then you need to be explicit about that in your query too. Your example doesn't include version anywhere.
You then need to find a version attribute with the value RNWB. You're currently looking for an RNWB attribute, which doesn't exist.
var hasFlag = doc
.Descendants("status")
.Descendants("version")
.Attributes("version")
.Any(x => x.Value == "RNWB");
See this fiddle for a demo.
Something like this should work:
// Load document
XDocument _doc = XDocument.Load("C://t//My File2.txt");
Select path to model
Select the elements version
Where the attribute == RNWB
Put them in a list for the result
// if you need to get the version element where the atrribute == something
List<XElement> model = _doc
.XPathSelectElement("dmodule/idstatus/status/applic/model")
.Elements("version")
.Where(x => x.Attribute("version").Value == "RNWB")
.ToList();
// if you only need to check if the attribute exists
bool contains = _doc
.XPathSelectElement("dmodule/idstatus/status/applic/model")
.Elements("version")
.Any(x => x.Attribute("version").Value == "RNWB");
The XML is not in valid format: I made some adjustment and got it working
<dmodule>
<idstatus>
<dmaddres>
<dmc>Blah</dmc>
<dmtitle><techname>System</techname><infoname>Introduction</infoname></dmtitle>
<issno issno="006" type="revised"/>
<issdate year="2016" month="11" day="30"/>
</dmaddres>
<status>
<security class="2"/>
<rpc></rpc>
<orig></orig>
<applic>
<model model="2093">
<version version="BASE"></version>
<version version="RNWB"></version>
</model>
</applic>
<techstd>
<autandtp>
<authblk></authblk>
<tpbase></tpbase>
</autandtp>
<authex></authex>
<notes></notes>
</techstd>
<qa>
<firstver type="tabtop"/></qa>
<remarks></remarks>
</status>
</idstatus>
<content>
<refs>
<norefs>
</norefs>
</refs>
<descript>
<para0>
<title>Introduction</title>
<para>The system many new features included which are safe and accurate blah blah blah.</para>
</para0>
</descript>
</content>
</dmodule>

XDocument LINQ search based on attribute

I have an XML document which I load from the disk
XDocument events = XDocument.Load("Content/GameData/events.xml");
The contents of this xml are the following:
<?xml version='1.0' encoding='UTF-8'?>
<Events>
<level1>
<NarrationEvent code="lvl1_fridge">
I can't even remember when I last ate.
I am not hungry though.
</NarrationEvent>
<NarrationEvent code="lvl1_tv">
Why do I even have a TV?
Oh right, I use it as a screen for my laptop.
</NarrationEvent>
<NarrationEvent code="lvl1_bed">
Oh man, I am beat.
</NarrationEvent>
<NarrationEvent code="lvl1_computer">
Oh, look at that. The project has been compiled.
</NarrationEvent>
</level1>
<level2>
</level2>
<level3>
</level3>
<level4>
</level4>
<cave>
</cave>
</Events>
I use this code here supposedly select the appropriate NarrationEvent element, based on its attribute "code"
IEnumerable<XElement> v =
(from narrationEvent in events.Elements("NarrationEvent")
where (string)narrationEvent.Attribute("code") == code
select narrationEvent);
foreach (XElement page in v)
{
//Console.WriteLine("ff");
narration.Add(page.Value);
}
This returns nothing, my XElement Ienumerable is empty. I used breakpoints and the code value is passed to this method just fine. e.g. "lvl1_bed"
What is wrong with this code?
You can use the Descendants-Method to get your NarrationEvent-Element. I have updated your code accordingly.
IEnumerable<XElement> v = from narrationEvent in events.Descendants("NarrationEvent")
where narrationEvent.Attribute("code").Value == code
select narrationEvent;

Locating a value in XML

I have an xml file loaded into an XDocument that I need to extract a value from, and I'm not sure of the best way to do it. Most of the things I'm coming up with seem to be overkill or don't make good use of xml rules. I have the following snippet of xml:
<entry>
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.6.2.12" />
<code code="121070" codeSystem="1.2.840.10008.2.16.4" codeSystemName="DCM" displayName="Findings">
</code>
<value xsi:type="ED">
<reference value="#121071">
</reference>
</value>
</observation>
</entry>
There can be any number of <entry> nodes, and they will all follow a similar pattern. The value under the root attribute on the templateId element contains a known UID that identifies this entry as the one I want. I need to get the reference value.
My thought is to find the correct templateID node, back out to the observation node, find <valuexsi:type="ED"> and then get the reference value. This seems overly complex, and I am wondering if there is another way to do this?
EDIT
The xml I receive can sometimes have xml nested under the same node name. In other words, <observation> may be located under another node named <observation>.
You have problems, because your document uses Namespaces, and your query is missing them.
First of all, you have to find xsi namespace declaration somewhere in your XML (probably in the most top element).
It will look like that:
xmlns:xsi="http://test.namespace"
The, take the namespace Uri and create XNamespace instance according to it's value:
var xsi = XNamespace.Get("http://test.namespace");
And use that xsi variable within your query:
var query = from o in xdoc.Root.Element("entries").Elements("entry").Elements("observation")
let tId = o.Element("templateId")
where tId != null && (string)tId.Attribute("root") == "2.16.840.1.113883.10.20.6.2.12"
let v = o.Element("value")
where v != null && (string)v.Attribute(xsi + "type") != null
let r = v.Element("reference")
where r != null
select (string)r.Attribute("value");
var result = query.FirstOrDefault();
I have tested it for following XML structure:
<root xmlns:xsi="http://test.namespace">
<entries>
<entry>
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.6.2.12" />
<code code="121070" codeSystem="1.2.840.10008.2.16.4" codeSystemName="DCM" displayName="Findings">
</code>
<value xsi:type="ED">
<reference value="#121071">
</reference>
</value>
</observation>
</entry>
</entries>
</root>
The query returns #121071 for it.
For your input XML you will probably have to change first line of query:
from o in xdoc.Root.Element("entries").Elements("entry").Elements("observation")
to match <observation> elements from your XML structure.
Would something along the lines of the following help?
XDocument xdoc = GetYourDocumentHere();
var obsvlookfor =
xdoc.Root.Descendants("observation")
.SingleOrDefault(el =>
el.Element("templateId")
.Attribute("root").Value == "root value to look for");
if (obsvlookfor != null)
{
var reference = obsvlookfor
.Element("value")
.Element("reference").Attribute("value").Value;
}
My thought is as follows:
Pull out all the observation elements in the document
Find the only one (or null) where the observation's templateId element has a root attribute you're looking for
If you find that observation element, pull out the value attribute against the reference element which is under the value element.
You might have to include the Namespace in your LINQ. To retrieve that you would do something like this:
XNamespace ns = xdoc.Root.GetDefaultNamespace();
Then in your linq:
var obsvlookfor = xdoc.Root.Descendants(ns + "observation")
I know I had some issues retrieving data once without this. Not saying its the issue just something to keep in mind particularly if your XML file is very in depth.

XDocument.XPathSelectElements : I can't seem get the xpath syntax right

I'm trying to work out why my xpath won't select the nodes I specificy
My xpath expression is //DefaultValue, so I expect all elements of name DefaultValue to be selected
My test file (cut down) is :
<?xml version="1.0" encoding="utf-8"?>
<SharedDataSet xmlns:rd="http://schemas.microsoft.com/SQLServer/reporting/reportdesigner" xmlns="http://schemas.microsoft.com/sqlserver/reporting/2010/01/shareddatasetdefinition">
<Description />
<DataSet Name="ddd">
<Query>
<DataSourceReference>xxx</DataSourceReference>
<DataSetParameters>
<DataSetParameter Name="p1">
<DefaultValue>baaaah</DefaultValue> <!-- this node should be selected eh? -->
</DataSetParameter>
<DataSetParameter Name="p2">
<DefaultValue>fooo</DefaultValue> <!-- this node should be selected too eh? -->
</DataSetParameter>
</DataSetParameters>
</Query>
</SharedDataSet>
Code is :
XmlNamespaceManager xn = new XmlNamespaceManager(new NameTable());
xn.AddNamespace("ns", "http://schemas.microsoft.com/sqlserver/reporting/2010/01/shareddatasetdefinition");
xn.AddNamespace("rd", "http://schemas.microsoft.com/SQLServer/reporting/reportdesigner");
xn.AddNamespace("cl", "http://schemas.microsoft.com/sqlserver/reporting/2010/01/componentdefinition");
XDocument document = XDocument.Parse(reportBuffer, LoadOptions.PreserveWhitespace);
IEnumerable<XElement> elements = document.XPathSelectElements(xpath, xn);
at this point elements.Count() equals 0
Can anyone see what foolishness I present?
I can't seem to make the xml demons happy... :-(
The <DefaultValue> element is actually bound to the namespace http://schemas.microsoft.com/sqlserver/reporting/2010/01/shareddatasetdefinition.
The <SharedDataSet> document element has it declared without a prefix, so it is easy to miss. Since <DefaultValue> is a descendant, it inherits the namespace.
If you want to select the <DefaultValue> element you need to adjust your XPath:
//ns:DefaultValue

Inserting XML Fragment after last specific node/element

I want to add an XML fragment to the last element to an XML document and I having problems i.e. the error I get is:
"The reference node is not a child of
this node".
So my existing XML document looks like this:
<MAP>
<LAYER name ="My first Layer">
<DATASET name="foo dataset" />
<SYMBOLOGY>
<SYMBOL colour="red" />
</SYMBOLOGY>
</LAYER>
<LAYER name="My second Layer">
<DATASET name="bar dataset" />
<SYMBOLOGY>
<SYMBOL colour="blue" />
</SYMBOLOGY>
</LAYER>
</MAP>
The XML fragment I want to insert after the last LAYER element is:
<LAYER name="My third Layer">
<DATASET name="whatever dataset" />
<SYMBOLOGY>
<SYMBOL colour="yellow" />
</SYMBOLOGY>
</LAYER>
The code I am using is:
XmlDocumentFragment xmlDocFrag = xmlDocument.CreateDocumentFragment();
xmlDocFrag.InnerXml = inputXML; //which is basically the third layer example - see above.
XmlElement rootElement = xmlDocument.DocumentElement;
XmlNode lastLayerNode = rootElement.SelectSingleNode(#"//LAYER[last()]");
rootElement.InsertAfter(xmlDocFrag, lastLayerNode); //error raised here.
Any ideas on what I'm doing wrong here would be much appreciated. My XPath query seems find and it seems to select the correct last layer it just won't insert after it for some bizarre reason.
UPDATE/SOLUTION - How to do this with XPATH
Finally figured it out in XPath - see the code below, I think it was down to basically not selecting the correct parent node in the first place, it's incorrect to select the last LAYER then try and InsertAfter() on this node. Better to select the level above i.e. MAP then AppendChild(). See below:
XmlDocumentFragment xmlDocFrag = xmlDocument.CreateDocumentFragment();
xmlDocFrag.InnerXml = inputXML;
XmlElement mapElement = (XmlElement)xmlDocument.SelectSingleNode(#"//MAP[last()]");
mapElement.AppendChild(xmlDocFrag);
Thanks to all the replies and help too :)
Taking into consideration that you need this to work with Framework 2.0, here's another solution:
string xml = "<map><layer>1</layer><layer>2</layer></map>";
string addMe = "<layer>3</layer>";
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.LoadXml(xml);
XmlDocumentFragment xmlDocFrag = xmlDocument.CreateDocumentFragment();
xmlDocFrag.InnerXml = addMe;
XmlElement rootElement = xmlDocument.DocumentElement;
rootElement.AppendChild(xmlDocFrag);
This results in:
<map><layer>1</layer><layer>2</layer><layer>3</layer></map>
Things look pretty good, but I would first try to avoid the xpath selection for the last node, and instead just use this:
rootElement.InsertAfter(xmlDocFrag, rootElement.LastChild);
I had similar issue, I used the ImportNode method to solve it
Here is a small example how you can use it to add node from different xml (stored in string) to your example at desired node in xml tree
string xmlstring =#"<tag>.....</tag>"; // holds xml tree to be appended
XmlDocument xml2 = new XmlDocument();
xml2.Load(#"path_of_main_xml");
XmlDocument xml1 = new XmlDocument();
xml1.Load(new StringReader(xmlString));
// get the node you want to import which in this icase is string
XmlNode elem = xml1.DocumentElement;
// use importNode to import it
XmlNode impnode = xml2.ImportNode(elem,true);
// get the node list of all node of particular tag name
XmlNodeList eNode = xml2.GetElementsByTagName("tag_name_of_parent");
eNode[0].AppendChild(impnode); // append new node
// write back the updates to same file
XmlWriter writer = XmlWriter.Create(#"path_of_main_xml");
xml2.Save(writer);

Categories

Resources