Performant way to get single element from XML - C# - c#

I got XML code like this:
<Body>
<Schoolyear>2016</Schoolyear>
<ClassLeader>
<Id>200555</Id>
<Name>Martin</Name>
<Short>ma</Short>
</ClassLeader>
<Info>
some very useful information :)
</Info>
</Body>
I only need one tag, e. g. SchoolYear
I tried this:
foreach (XElement element in Document.Descendants("Schoolyear"))
{
myDestinationVariable = element.Value;
}
It works, but I think maybe there is a more performant and easier solution.

You can take it using LINQ or just use Element with the specified XName
Add namespace
using System.Xml.Linq;
And use one of these examples
string xml = #"<Body>
<Schoolyear>2016</Schoolyear>
<ClassLeader>
<Id>200555</Id>
<Name>Martin</Name>
<Short>ma</Short>
</ClassLeader>
<Info>
some very useful information :)
</Info>
</Body>";
XDocument dox = XDocument.Parse(xml);
var exampl1 = dox.Element("Body").Element("Schoolyear").Value;
var exampl2 = dox.Descendants().FirstOrDefault(d => d.Name == "Schoolyear").Value;

Related

Parsing XML excluding elements

I'm trying to parse a xml file, which isn't the problem. But my xml file has multiple <name> tags. One is for a song, and the other one is for an artist. I want to exclude the artist name tag, but I can't figure out how.
Here's my code so far:
string xmlString;
using (WebClient wc = new WebClient())
{
xmlString = wc.DownloadString(#"http://ws.audioscrobbler.com/2.0/?method=album.getInfo&album=awake&artist=Dream%20Theater&api_key=b5cbf8dcef4c6acfc5698f8709841949");
}
List<XElement> alleElements = new List<XElement>();
XDocument myXMLDoc = XDocument.Parse(xmlString);
List<XElement> trackNames = myXMLDoc.Descendants("album")
.Elements("tracks")
.Elements("name")
.ToList();
foreach (XElement el in trackNames)
{
Console.WriteLine(el);
}
Console.WriteLine("-----End-----");
Console.ReadLine();
I tried using .Elements("name"); instead of Descendants, but that returns nothing at all.
Here's a small part of my xml file:
<track rank="1">
<name>6:00</name>
<url>http://www.last.fm/music/Dream+Theater/_/6:00</url>
<duration>331</duration>
<streamable fulltrack="0">0</streamable>
<artist>
<name>Dream Theater</name>
<mbid>28503ab7-8bf2-4666-a7bd-2644bfc7cb1d</mbid>
<url>http://www.last.fm/music/Dream+Theater</url>
</artist>
</track>
Is there a way to exclude the <name> tag inside the <artist> tag.
If I'm not clear enough, please let me know and I'll explain it more!
It sounds like you just want to use Elements instead of Descendants, but at the right point - it's not clear where you tried using it. I'd also recommend using ToList to make things simpler. Given the sample in the documentation, it looks like this would work and be clearer in terms of where we expect there to be multiple elements:
List<XElement> trackNames = doc.Root
.Element("tracks")
.Elements("track")
.Elements("name")
.ToList();
You can try excluding with a condition, like myXMLDoc. ... .Descendants("name").Where(x => x.Parent.Name != "artist").

Read value of specific child node using LINQ

I have a small question. My XML document looks like this:
<?xml version="1.0" encoding="utf-8"?>
<BACKUP>
<HEAD>
<NAME>Test</NAME>
<DATE>19.05.2015 17:05:42</DATE>
</HEAD>
<DIRECTORYSRC>
<SOURCEDIR>C:\Users\User\Pictures</SOURCEDIR>
</DIRECTORYSRC>
<DIRECTORYTRG>
<TARGETDIR>D:\_backup</TARGETDIR>
</DIRECTORYTRG>
</BACKUP>
What is the best way to read the content of NAME and TARGETDIR value? Is there a simple way to read only these specific nodes?
And what if I have more then one TARGETDIR node? Can I do this by using a foreach loop?
You can easily do this using LINQ to XML:
var doc = XDocument.Parse(xml);
var name = (string)doc.Descendants("NAME").Single();
var targetDirs = doc.Descendants("TARGETDIR").Select(e => e.Value).ToList();
name contains your single NAME value, and targetDirs is a List<string> containing all TARGETDIR values.

Getting an XElement with a namespace via XPathSelectElements

I have an XML e.g.
<?xml version="1.0" encoding="utf-8"?>
<A1>
<B2>
<C3 id="1">
<D7>
<E5 id="abc" />
</D7>
<D4 id="1">
<E5 id="abc" />
</D4>
<D4 id="2">
<E5 id="abc" />
</D4>
</C3>
</B2>
</A1>
This is may sample code:
var xDoc = XDocument.Load("Test.xml");
string xPath = "//B2/C3/D4";
//or string xPath = "//B2/C3/D4[#id='1']";
var eleList = xDoc.XPathSelectElements(xPath).ToList();
foreach (var xElement in eleList)
{
Console.WriteLine(xElement);
}
It works perfectly, but if I add a namespace to the root node A1, this code doesn't work.
Upon searching for solutions, I found this one, but it uses the Descendants() method to query the XML. From my understanding, this solution would fail if I was searching for <E5> because the same tag exists for <D7>, <D4 id="1"> and <D4 id="2">
My requirement is to search if a node exists at a particular XPath. If there is a way of doing this using Descendants, I'd be delighted to use it. If not, please guide me on how to search using the name space.
My apologies in case this is a duplicate.
To keep using XPath, you can use something link this:
var xDoc = XDocument.Parse(#"<?xml version='1.0' encoding='utf-8'?>
<A1 xmlns='urn:sample'>
<B2>
<C3 id='1'>
<D7><E5 id='abc' /></D7>
<D4 id='1'><E5 id='abc' /></D4>
<D4 id='2'><E5 id='abc' /></D4>
</C3>
</B2>
</A1>");
// Notice this
XmlNamespaceManager nsmgr = new XmlNamespaceManager(new NameTable());
nsmgr.AddNamespace("sample", "urn:sample");
string xPath = "//sample:B2/sample:C3/sample:D4";
var eleList = xDoc.XPathSelectElements(xPath, nsmgr).ToList();
foreach (var xElement in eleList)
{
Console.WriteLine(xElement);
}
but it uses the Descendants() method to query the XML. From my understanding, this solution would fail if I was searching for because the same tag exists for , and
I'm pretty sure you're not quite understanding how that works. From the MSDN documentation:
Returns a filtered collection of the descendant elements for this document or element, in document order. Only elements that have a matching XName are included in the collection.
So in your case, just do this:
xDoc.RootNode
.Descendants("E5")
.Where(n => n.Parent.Name.LocalName == "B4");
Try this
var xDoc = XDocument.Parse("<A1><B2><C3 id=\"1\"><D7><E5 id=\"abc\" /></D7><D4 id=\"1\"><E5 id=\"abc\" /></D4><D4 id=\"2\"><E5 id=\"abc\" /></D4></C3></B2></A1>");
foreach (XElement item in xDoc.Element("A1").Elements("B2").Elements("C3").Elements("D4"))
{
Console.WriteLine(item.Element("E5").Value);//to get the value of E5
Console.WriteLine(item.Element("E5").Attribute("id").Value);//to get the value of attribute
}

Search XDocument using LINQ without knowing the namespace

Is there a way to search an XDocument without knowing the namespace? I have a process that logs all SOAP requests and encrypts the sensitive data. I want to find any elements based on name. Something like, give me all elements where the name is CreditCard. I don't care what the namespace is.
My problem seems to be with LINQ and requiring a xml namespace.
I have other processes that retrieve values from XML, but I know the namespace for these other process.
XDocument xDocument = XDocument.Load(#"C:\temp\Packet.xml");
XNamespace xNamespace = "http://CompanyName.AppName.Service.Contracts";
var elements = xDocument.Root
.DescendantsAndSelf()
.Elements()
.Where(d => d.Name == xNamespace + "CreditCardNumber");
I really want to have the ability to search xml without knowing about namespaces, something like this:
XDocument xDocument = XDocument.Load(#"C:\temp\Packet.xml");
var elements = xDocument.Root
.DescendantsAndSelf()
.Elements()
.Where(d => d.Name == "CreditCardNumber")
This will not work because I don't know the namespace beforehand at compile time.
How can this be done?
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Request xmlns="http://CompanyName.AppName.Service.ContractA">
<Person>
<CreditCardNumber>83838</CreditCardNumber>
<FirstName>Tom</FirstName>
<LastName>Jackson</LastName>
</Person>
<Person>
<CreditCardNumber>789875</CreditCardNumber>
<FirstName>Chris</FirstName>
<LastName>Smith</LastName>
</Person>
...
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Request xmlns="http://CompanyName.AppName.Service.ContractsB">
<Transaction>
<CreditCardNumber>83838</CreditCardNumber>
<TransactionID>64588</FirstName>
</Transaction>
...
As Adam precises in the comment, XName are convertible to a string, but that string requires the namespace when there is one. That's why the comparison of .Name to a string fails, or why you can't pass "Person" as a parameter to the XLinq Method to filter on their name.
XName consists of a prefix (the Namespace) and a LocalName. The local name is what you want to query on if you are ignoring namespaces.
Thank you Adam :)
You can't put the Name of the node as a parameter of the .Descendants() method, but you can query that way :
var doc= XElement.Parse(
#"<s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/"">
<s:Body xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"">
<Request xmlns=""http://CompanyName.AppName.Service.ContractA"">
<Person>
<CreditCardNumber>83838</CreditCardNumber>
<FirstName>Tom</FirstName>
<LastName>Jackson</LastName>
</Person>
<Person>
<CreditCardNumber>789875</CreditCardNumber>
<FirstName>Chris</FirstName>
<LastName>Smith</LastName>
</Person>
</Request>
</s:Body>
</s:Envelope>");
EDIT : bad copy/past from my test :)
var persons = from p in doc.Descendants()
where p.Name.LocalName == "Person"
select p;
foreach (var p in persons)
{
Console.WriteLine(p);
}
That works for me...
You could take the namespace from the root-element:
XDocument xDocument = XDocument.Load(#"C:\temp\Packet.xml");
var ns = xDocument.Root.Name.Namespace;
Now you can get all desired elements easily using the plus-operator:
root.Elements(ns + "CreditCardNumber")
I think I found what I was looking for. You can see in the following code I do the evaluation Element.Name.LocalName == "CreditCardNumber". This seemed to work in my tests. I'm not sure if it's a best practice, but I'm going to use it.
XDocument xDocument = XDocument.Load(#"C:\temp\Packet.xml");
var elements = xDocument.Root.DescendantsAndSelf().Elements().Where(d => d.Name.LocalName == "CreditCardNumber");
Now I have elements where I can encrypt the values.
If anyone has a better solution, please provide it. Thanks.
There's a couple answers with extension methods that have been deleted. Not sure why. Here's my version that works for my needs.
public static class XElementExtensions
{
public static XElement ElementByLocalName(this XElement element, string localName)
{
return element.Descendants().FirstOrDefault(e => e.Name.LocalName == localName && !e.IsEmpty);
}
}
The IsEmpty is to filter out nodes with x:nil="true"
There may be additional subtleties - so use with caution.
If your XML documents always defines the namespace in the same node (Request node in the two examples given), you can determine it by making a query and seeing what namespace the result has:
XDocument xDoc = XDocument.Load("filename.xml");
//Initial query to get namespace:
var reqNodes = from el in xDoc.Root.Descendants()
where el.Name.LocalName == "Request"
select el;
foreach(var reqNode in reqNodes)
{
XNamespace xns = reqNode.Name.Namespace;
//Queries making use of namespace:
var person = from el in reqNode.Elements(xns + "Person")
select el;
}
I a suffering from a major case of "I know that is the solution, but I am disappointed that that is the solution"... I recently wrote a query like the one below (which I will shortly replace, but it has educational value):
var result = xdoc.Descendants("{urn:schemas-microsoft-com:rowset}data")
.FirstOrDefault()?
.Descendants("{#RowsetSchema}row");
If I remove the namespaces from the XML, I can write the same query like this:
var result = xdoc.Descendants("data")
.FirstOrDefault()?
.Descendants("row");
I plan to write my own extension methods that should allow me to leave the namespaces alone and search for nodes like this:
var result = xdoc.Descendants("rs:data")
.FirstOrDefault()?
.Descendants("z:row");
//'rs:' {refers to urn:schemas-microsoft-com:rowset}
//'z:' {refers to xmlns:z=#RowsetSchema}
My comments just below the code point to how I would like to hide the ugliness of the solution in an Extension Methods library. Again, I'm aware of the solutions posted earlier - but I wish the API itself handled this more fluently. (See what I did there?)
Just use the Descendents method:
XDocument doc = XDocument.Load(filename);
String[] creditCards = (from creditCardNode in doc.Root.Descendents("CreditCardNumber")
select creditCardNode.Value).ToArray<string>();

Can I avoid having to use fully-qualified element names in LINQ to XML?

Say I call XElement.Parse() with the following XML string:
var xml = XElement.Parse(#"
<?xml version="1.0" encoding="UTF-8"?>
<AccessControlPolicy xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Owner>
<ID>7c75442509c41100b6a413b88b523bd6f46554cdbee5b6cbe27bc08cb3f6a865</ID>
<DisplayName>me</DisplayName>
</Owner>
<AccessControlList>
<Grant>
<Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Group">
...
");
When it comes time to query the element, I'm forced to use fully-qualified element names because that XML document contains an xmlns attribute in its root. This requires cumbersome creations of XName instances:
var AWS_XMLNS = "http://s3.amazonaws.com/doc/2006-03-01/";
var ownerElement = xml.Element(XName.Get("AccessControlPolicy", AWS_XMLNS)).Element(XName.Get("Owner", AWS_XMLNS));
When what I really want is simply,
var ownerElement = xml.Element("AccessControlPolicy").Element("Owner");
Is there a way to make LINQ to XML assume a specific namespace so I don't have to keep specifying it?
You could simplify by using
XNamespace ns = "http://s3.amazonaws.com/doc/2006-03-01/";
var ownerElement = xml.Element(ns + "AccessControlPolicy").Element(ns + "Owner");
I don't think you can (see Jon Skeet's comment), but there are a few tricks you can do.
1) create an extension method that appends the XNamespace to your string
2) Use VB?!?

Categories