C# IEnumerable<XElement> - How to Xpath Filter on “root”? - c#

In C#, I have an IEnumerable of XElements. All of the XElements have the same name, but different types. I would like to perform an "xpath filter" on the "root" element of each XElement.
Sample XML:
<xml>
<Location>
<Type>Airport</Type>
<Buildings></Buildings>
</Location>
<Location>
<Type>Mine</Type>
<Buildings></Buildings>
</Location>
<Location>
<Type>Airport</Type>
<Buildings></Buildings>
</Location>
</xml>
Sample C#:
var elements = xml.Elements("Location");
What I need is to get all the Buildings where the Location/Type is "Airport". What I would like to do is something like:
elements.SelectMany(el => el.XPathSelectElements(".[Type = 'Airport']/Buildings/Building"));
However, I cannot figure out the xpath syntax for filtering at the "root" of the XElement (the ".[Type" part).
What I can do is:
Add the elements to a made-up root element, and then apply my xpath filter (because Location would no longer be at the "root").
Filter the Locations using Linq eg: elements.Where(loc => loc.Element("Type").Value == "Airport")
But I would like to learn if there is an xpath way.
Can anyone point me in the right direction for the xpath syntax?
Thanks!
EDIT
The above XML is an extremely dumbed-down sample. The actual XML is tens of thousands of lines long, relatively unpredictable (a change in the source object can change thousands of lines of XML), and its schema is not fully known (on my end). Some of the structures repeat and/or nest. Therefore, using "//" is unlikely sufficient. Apologies for the confusion.

Try this:
var buildings = xml.XPathSelectElements("//xml/Location[Type=\"Airport\"]/Buildings");
Example:
string xmlString =
#"<xml>
<Location>
<Type>Airport</Type>
<Buildings>First airport buildings</Buildings>
</Location>
<Type>Mine</Type>
<Buildings>Mine buildings</Buildings>
<Location>
<Type>Airport</Type>
<Buildings>Second airport buildings</Buildings>
</Location>
</xml>";
XDocument xml = XDocument.Parse(xmlString);
var buildings =
xml.XPathSelectElements("//xml/Location[Type=\"Airport\"]/Buildings");
foreach (var b in buildings)
{
Console.WriteLine(b.Value);
}
Result:
First airport buildings
Second airport buildings

Related

Is it possible to have non reapating element in an xml file?

I m working on a simple adventure game and right now I have an xml file to load the world and another for the player.
here is the simplified xml I'd like to create:
<root>
<world>
<location>
//room 1
<inventory>
talble, chair...
</inventory>
</location>
<location>
//room 2
</location>
</world>
<player>
//some player stats i.e the location he is currently in.
<inventory>
the player bag containing objects
</inventory>
</player>
</root>
Will I be able to parse that file to get the player inventory or a location inventory? is it allowed to create that asymetrical xml or does it always have to be only repeating element?
Right now the game works but I am not sure it's good practice to separate those too files since I must always update theim at the same time. I'd rather merge those together.
Thanks a lot for the help.
Mike
XDocument provides methods to access specific elements via a "path".
To get all elements of the player's inventory, you would need code similar to this:
IEnumerable<XElement> playerItems = x.Element("player").Element("inventory").Elements();
And to get the objects of the first location:
IEnumerable<XElement> locationObjects = x.Element("world").Elements("location").First().Element("inventory").Elements();
Note the difference between Element() and Elements(), the first returns only one element; the second returns an IEnumerable<XElement>.
This is important when you want to retrieve the objects of all locations.
After you have retrieved all elements, you would need to process them; you can access the value of an element by the Value property which is a String.

SelectSingleNode in XML for select second element doesn't work in C#

I have problem to select second node from root element in following example xml code:
<?xml version="1.0"?>
<config>
<FirstNode>
<ShowBlahBlah>
</ShowBlahBlah>
</FirstNode>
<SecondNode>
<ShowBlahBlah>
</ShowBlahBlah>
</SecondNode>
</config>
and using this C# code to select SecondNode:
XmlDocument doc = new XmlDocument();
doc.LoadXml(sReadXML);
XmlNode sChangesLog = doc.SelectSingleNode("config").SelectSingleNode("//SecondNode").SelectSingleNode("//ShowBlahBlah")
XmlNodeList sChildNodes = sChangesLog.ChildNodes;
but it selecting first node and return its value!
how can I fix this problem?
You're using // at the start of each of your selections - which means "find descendant nodes starting at the root" (so the context is irrelevant). You could either do things in one step as per Jeffrey's answer, or use relative paths:
doc.SelectSingleNode("config")
.SelectSingleNode("SecondNode")
.SelectSingleNode("ShowBlahBlah")
Personally I'd use LINQ to XML instead, if at all possible:
var doc = XDocument.Parse(sReadXml);
var changes = doc.Root.Element("SecondNode").Element("ShowBlahBlah");
LINQ to XML is generally a much cleaner API than XmlDocument et al.

How to count the Number of Nodes with same innertext value in XML

I am working on a C# code. I have my XML document with with different nodes, Out of which I need to count the number of nodes having the same innertext value. I am Using XmlDocument. Let's say I have a xml file where I have to count the number of nodes having InnerText value BSS.
Code:
<Annotations>
<Objects>
<ObjectId>BSS</ObjectId>
<ObjectId>CAT</ObjectId>
</Objects>
<Objects>
<ObjectId>BSS</ObjectId>
<ObjectId>ABC</ObjectId>
</Objects>
<Objects>
<ObjectId>BSS</ObjectId>
<ObjectId>PCT</ObjectId>
</Objects>
</Annotations>
Here in the Above code I want to count the number of nodes having innertext value BSS.like here the count should be 3.Your help will be greatly appreciated.
Edited Code:
<Annotations>
<Objects>
<ObjectId>BSS|SAF|PAT</ObjectId>
<ObjectId>CAT</ObjectId>
</Objects>
<Objects>
<ObjectId>BSS|SAF|PAT</ObjectId>
<ObjectId>ABC</ObjectId>
</Objects>
<Objects>
<ObjectId>BSS|SAF|PAT</ObjectId>
<ObjectId>PCT</ObjectId>
</Objects>
</Annotations>
If you have it loaded as an XmlDocument, you can do
doc.selectNodes("//*[.='BSS']").Count;
Although I think there might be a more efficient way of writing that XPath, matching everything then checking the text content of it feels like it's probably inefficient.
If that's a complete and representative document, then you can do
doc.selectNodes("Annotations/Objects/ObjectId[.='BSS']").Count;
This will be more efficient, as it's targetting specific elements rather than the entire document.
If the search text is contained within a pipe (|) separated list, you can use this predicate instead of [.='BSS']:
[contains(concat('|',.,'|'),'|BSS|')]
Although this will be slower. Personally, I'd recommend that wherever your source comes from, it be modified to separate this list into separate elements, it'll make querying your data much easier. I'm a strong advocate of the principle that XML documents should follow the same principle as the 'First Normal Form' rule that databases do, and not include multiple values in any one text node/attribute.
I would use the new Linq to XML api instead :
var xDocument = XDocument.Load("path");
int count = xDocument.Descendants().Count(x => (string)x == "BSS");
XmlDocument xmlD = new XmlDocument();
xmlD.Load(Server.MapPath("sample.xml"));
XmlNodeList xmlNL = xmlD.GetElementsByTagName("tagName");
xmlNL.Count;

I want to read a single element of an XML file and output it to an aspx control

My XML looks like this:
<product id="1">
<name>A thing</name>
<description>This is what it's like</description>
</product>
I've been looking for an example that might look something like this:
string productID = 1;
XDocument productsXML = XDocument.Load("Products.xml");
for item in productsXML {
if (item.ID == productID) {
productNameLabel.Text = item.name;
}
}
The idea being, I'll be able to output a single xml child element to the .aspx page.
A thing
Does that make sense? I've been looking for examples for a few hours, and I'm starting to think that there is lot more scaffolding that I need, but since all the examples are so different, I'm not sure which one to follow!
So, how can I grab the contents of a single XML child element, and output it to the .aspx? Take this:
<product id="1">
<name>A thing</name>
<description>This is what it's like</description
</product>
and output this:
A thing
You can use XPath (in System.Xml.XPath)
string value = productsXML.XPathSelectElement("//product[#id='1']/name").Value;
In this case, I agree with L.B.'s answer since it is rather simple. If you like Linq (I noticed you were using XDocument, which is a Linq to XML object), here's an alternative solution:
XDocument productsXML = XDocument.Load("Products.xml");
string item = productsXML.Elements("product")
.Where(p => p.Attribute("id").Value == productID)
.First()
.Element("name").Value;
productsXML.Elements("product") This gets all product nodes in the document.
.Where(p => p.Attribute("id").Value == productID) This gets only the product node(s) with the id that matches your productiD.
.First() Since the .Where function returns a collection of nodes, this grabs the first one.
.Element("name").Value; This finds an element within the product node called name and returns its value.
For such a simple schema, the XPath seems much less verbose, but a bit harder to understand if you don't know XPath in the first place. Linq is a lot more lines of code (in this instance), but is a lot more readable if you don't know XPath.

How do I find a XML node by path in Linq-to-XML

If I get the path to a specific node as a string can I somehow easily find said node by using Linq/Method of the XElement ( or XDocument ).
There are so many different types of XML objects it would also be nice if as a added bonus you could point me to a guide on why/how to use different types.
EDIT: Ok after being pointed towards XPathSelectElement I'm trying it out so I can give him the right answer I can't quite get it to work though. This is the XML I'm trying out
<Product>
<Name>SomeName</Name>
<Type>SomeType</Type>
<Quantity>Alot</Quantity>
</Product>
and my code
string path = "Product/Name";
string name = xml.XPathSelectElement(path).Value;
note my string is coming from elsewhere so I guess it doesn't have to be literal ( at least in debug mode it looks like the one above). I've also tried adding / in front. It gives me a null ref.
Try using the XPathSelectElement extension method of XElement. You can pass the method an XPath expression to evaluate. For example:
XElement myElement = rootElement.XPathSelectElement("//Book[#ISBN='22542']");
Edit:
In reply to your edit, check your XPath expression. If your document only contains that small snippet then /Product/Name will work as the leading slash performs a search from the root of the document:
XElement element = document.XPathSelectElement("/Product/Name");
If there are other products and <Product> is not the root node you'll need to modify the XPath you're using.
You can also use XPathEvaluate
XDocument document = XDocument.Load("temp.xml");
var found = document.XPathEvaluate("/documents/items/item") as IEnumerable<object>;
foreach (var obj in found)
{
Console.Out.WriteLine(obj);
}
Given the following xml:
<?xml version="1.0" encoding="utf-8" ?>
<documents>
<items>
<item name="Jamie"></item>
<item name="John"></item>
</items>
</documents>
This should print the contents from the items node.

Resources