Linq to xml parsing - c#

I need assistance in retrieving the element Country and Its value 1 in the below code.
Thanks in advance.
<?xml version="1.0" encoding="utf-8"?>
<env:Contentname xmlns:env="http://data.schemas" xmlns="http://2013-02-01/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<env:Body>
<env:Content action="Hello">
<env:Data xsi:type="Yellow">
</env:Data>
</env:Content>
<env:Content action="Hello">
<env:Data xsi:type="Red">
<Status xmlns="http://2010-10-10/">
<Id >39681</Id>
<Name>Published</Name>
</Status>
</env:Data>
</env:Content>
<env:Content action="Hello">
<env:Data xsi:type="green">
<Document>
<Country>1</Country>
</Document>
</env:Data>
</env:Content>
</env:Body>
</env:Contentname>
I tried this,
var result = from x in root.Descendants(aw + "Data")
where (string)x.Attribute(kw + "type") == "green"
select x;
foreach (var item in result)
{
var str = item.Element("Document").Element("Country");
Console.WriteLine(str.Value);
}
But i am getting error.(Object reference not set to an instance of an object.)
kindly help me with this.

This is the problem, for two reasons:
var str = item.Element("Document").Element("country");
Firstly, XML is case-sensitive - you want Country, not country.
Secondly, those elements inherit the namespace declared with xmlns=... in the root element. You need:
XNamespace ns = "http://2013-02-01/";
...
var element = item.Element(ns + "Document").Element(ns + "Country");
I'd also encourage you to avoid query expressions where they don't actually buy you much. In this case, you could perform the whole query in one go using the Elements extension method which works on sequences of input elements, assuming you don't mind finding every Document -> Country element rather than just one per Data:
var query = root.Descendants(aw + "Data")
.Where(x => (string)x.Attribute(kw + "type") == "green")
.Elements(ns + "Document")
.Elements(ns + "Country")
.Select(x => x.Value);
foreach (var item in query)
{
Console.WriteLine(item);
}
One significant difference - this won't fall over with an exception if there is a Data element with xsi:type='green' which doesn't have a Document -> Country element. If you want it to (to find bad data) you could use:
var query = root.Descendants(aw + "Data")
.Where(x => (string)x.Attribute(kw + "type") == "green")
.Select(x => x.Element(ns + "Document")
.Element(ns + "Country")
.Value);
To show a short but complete example, this code (with your XML as test.xml) gives an output of "1":
using System;
using System.Linq;
using System.Xml.Linq;
public class Program
{
static void Main(string[] args)
{
var doc = XDocument.Load("test.xml");
XNamespace ns = "http://2013-02-01/";
XNamespace kw = "http://www.w3.org/2001/XMLSchema-instance";
XNamespace aw = "http://data.schemas";
var query = doc.Descendants(aw + "Data")
.Where(x => (string)x.Attribute(kw + "type") == "green")
.Elements(ns + "Document")
.Elements(ns + "Country")
.Select(x => x.Value);
foreach (var item in query)
{
Console.WriteLine(item);
}
}
}

Related

When writing file 3 lines are deleted, and should not

I am removing the duplicated entries in one XML file, and the code is removing them and summing the values to have only one entry of each invoice.
But when it reaches the end it removes 3 lines the following ones:
<NumberOfEntries>11972</NumberOfEntries>
<TotalDebit>0</TotalDebit>
<TotalCredit>34422.86</TotalCredit>
So instead of having the final file like:
...
<SourceDocuments>
<SalesInvoices>
<NumberOfEntries>11972</NumberOfEntries>
<TotalDebit>0</TotalDebit>
<TotalCredit>34422.86</TotalCredit>
<Invoice>
<InvoiceNo>FS 006120180101/19959</InvoiceNo>
...
It appears like:
...
<SourceDocuments>
<SalesInvoices>
<Invoice>
<InvoiceNo>FS 006120180101/19959</InvoiceNo>
...
My code is the following one:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ConsoleApplication2
{
class Program
{
//Ficheiro
const string FILENAME = "ccc.xml";
static void Main(string[] args)
{
XDocument doc = XDocument.Load(FILENAME);
XNamespace ns = doc.Root.Name.Namespace;
List<XElement> originalInvoices = doc.Descendants(ns + "Invoice").ToList();
var groups = originalInvoices.GroupBy(x => (string)x.Element(ns + "Hash")).ToList();
var finalInvoices = groups.Select(x => new
{
unit = x.Descendants(ns + "UnitPrice").Sum(z => (decimal)z),
credit = x.Descendants(ns + "CreditAmount").Sum(z => (decimal)z),
tax = x.Descendants(ns + "TaxPayable").Sum(z => (decimal)z),
net = x.Descendants(ns + "NetTotal").Sum(z => (decimal)z),
gross = x.Descendants(ns + "GrossTotal").Sum(z => (decimal)z),
first = x.First()
}).ToList();
foreach (var finalInvoice in finalInvoices)
{
finalInvoice.first.Element(ns + "Line").SetElementValue(ns + "UnitPrice", finalInvoice.unit);
finalInvoice.first.Element(ns + "Line").SetElementValue(ns + "CreditAmount", finalInvoice.credit);
finalInvoice.first.Element(ns + "DocumentTotals").SetElementValue(ns + "TaxPayable", finalInvoice.tax);
finalInvoice.first.Element(ns + "DocumentTotals").SetElementValue(ns + "NetTotal", finalInvoice.net);
finalInvoice.first.Element(ns + "DocumentTotals").SetElementValue(ns + "GrossTotal", finalInvoice.gross);
}
doc.Descendants(ns + "SalesInvoices").FirstOrDefault().ReplaceWith(new XElement(ns + "SalesInvoices", finalInvoices.Select(x => x.first)));
doc.Descendants(ns + "SalesInvoices").
Console.WriteLine(doc);
doc.Save("Root.xml");
Console.ReadKey();
}
}
}
And you can see a sample of my XML file here: Pastebin Link
Can someone help me with this, how can I make it to not remove those 3 lines?
The problem is probably on the last line where it writes the file, but I'm not sure.
doc.Descendants(ns + "SalesInvoices").FirstOrDefault().ReplaceWith(new XElement(ns + "SalesInvoices", finalInvoices.Select(x => x.first)));
Small Update on the question:
Well I really think that the problem is on the line above because if I change for example SalesInvoices with TotalCredit which is the last line of the ones that are disappearing the file still wrong but instead of:
...
<SourceDocuments>
<SalesInvoices>
<Invoice>
<InvoiceNo>FS 006120180101/19959</InvoiceNo>
...
I'm getting:
...
<SourceDocuments>
<SalesInvoices>
<NumberOfEntries>11972</NumberOfEntries>
<TotalDebit>0</TotalDebit>
<TotalCredit>
<Invoice>
<InvoiceNo>FS 006120180101/19959</InvoiceNo>
...
there is a missing 34422.86</TotalCredit> before tag <Invoice>
and it's adding the </TotalCredit> after the first closed element </Invoice> as you can test here: Link to test the code
Easy to fix. Just add the missing 3 elements :
foreach (var finalInvoice in finalInvoices)
{
finalInvoice.first.Element(ns + "Line").SetElementValue(ns + "UnitPrice", finalInvoice.unit);
finalInvoice.first.Element(ns + "Line").SetElementValue(ns + "CreditAmount", finalInvoice.credit);
finalInvoice.first.Element(ns + "DocumentTotals").SetElementValue(ns + "TaxPayable", finalInvoice.tax);
finalInvoice.first.Element(ns + "DocumentTotals").SetElementValue(ns + "NetTotal", finalInvoice.net);
finalInvoice.first.Element(ns + "DocumentTotals").SetElementValue(ns + "GrossTotal", finalInvoice.gross);
}
XElement salesInvoices = doc.Descendants(ns + "SalesInvoices").FirstOrDefault();
XElement numberOfEntries = salesInvoices.Element(ns + "NumberOfEntries");
XElement totalDebit = salesInvoices.Element(ns + "TotalDebit");
XElement totalCredit = salesInvoices.Element(ns + "TotalCredit");
salesInvoices.ReplaceWith(new XElement(ns + "SalesInvoices", new object[] {
numberOfEntries,
totalDebit,
totalCredit,
finalInvoices.Select(x => x.first)
}));

Reading XML with namespace and repeating nodes

I have the following XML:
<resource>
<description>TTT</description>
<title>TEST</title>
<entity xmlns="TdmBLRuPlUz.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="TdmBLRuPlUz.xsd TdmBLRuPlUz.xsd">
<UzdProd>
<row>
<F_DAUDZ>50</F_DAUDZ>
<BR_DAUDZ/>
<DAUDZ>50</DAUDZ>
<U_DAUDZ/>
<NKODS>ST2.0_014_023</NKODS>
</row>
</UzdProd>
<UzdMat>
<row>
<NKODS>SAG 2.0_014_150</NKODS>
<NNOSAUK>Sagatave 2.0mm*0.14*150m</NNOSAUK>
<PK_VIEN>1</PK_VIEN>
<DAUDZ>0.077</DAUDZ>
<F_DAUDZ>0.077</F_DAUDZ>
</row>
</UzdMat>
</entity>
</resource>
And this is my C# code:
XNamespace ns = "TdmBLRuPlUz.xsd";
XDocument doc = XDocument.Parse(xml);
foreach (XElement element in doc.Descendants(ns + "row"))
{
Console.WriteLine(element.Element(ns + "NKODS").Value);
string NKODS = element.Element(ns + "NKODS").Value;
string F_DAUDZ = element.Element(ns + "F_DAUDZ").Value;
string DAUDZ = element.Element(ns + "DAUDZ").Value;
}
What I need is to read values from the XML nodes NKODS, F_DAUDZ and DAUDZ.
The problem is that there are repeating nodes with those names and with this code it gives me the last ones which are under UzdMat node. What would be the way to get the values for these nodes under UzdProd?
I tried to change row to UzdProd, but that didn't work.
You need to read the specific row you want rather than looping through all of them. For example:
var prodRow = doc.Descendants(ns + "UzdProd").Elements(ns + "row").Single();
var matRow = doc.Descendants(ns + "UzdMat").Elements(ns + "row").Single();
var prodNkods = (string) prodRow.Element(ns + "NKODS");
var matNkods = (string) matRow.Element(ns + "NKODS");
See this fiddle for a working demo.
See if this works :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ConsoleApplication25
{
class Program
{
const string FILENAME = #"c:\temp\test.xml";
static void Main(string[] args)
{
XDocument doc = XDocument.Load(FILENAME);
XElement entity = doc.Descendants().Where(x => x.Name.LocalName == "entity").FirstOrDefault();
XNamespace ns = entity.GetDefaultNamespace();
var results = entity.Elements().Select(x => new {
uzd = x.Name.LocalName,
dict = x.Descendants(ns + "row").Elements().GroupBy(y => y.Name.LocalName, z => (string)z)
.ToDictionary(y => y.Key, z => z.FirstOrDefault())
}).ToList();
}
}
}

How to extract the names of all bookmarks in a Word Xml Document

I have a word Document in Xml format with multiple entries like so :
<aml:annotation aml:id="0" w:type="Word.Bookmark.Start" w:name="CustomerName"/>
I want to retrieve a collection of these but cannot see how to get past
foreach (XElement ann in doc.Root.Descendants(aml + "annotation"))
In other words I can get all annotations, but cannot see how to filter to just retrieve bookmarks. The namespaces aml and w are declared like this
XNamespace w = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
XNamespace aml = "http://schemas.microsoft.com/aml/2001/core";
Could someone give me a push ?
I resolved the issue as follows
XNamespace w = doc.Root.GetNamespaceOfPrefix("w");
XNamespace aml = doc.Root.GetNamespaceOfPrefix("aml");
foreach (string bookm in doc.Descendants(aml + "annotation")
.Where(e => e.Attributes(w + "type")
.Any(a => a.Value == "Word.Bookmark.Start"))
.Select(b => b.Attribute(w + "name").Value))
{
...
}
var names = from a in doc.Root.Descendants(aml + "annotation"))
where (string)a.Attribute(w + "type") == "Word.Bookmark.Start"
select (string)a.Attribute(w + "name");
Lambda syntax:
doc.Root.Descendants(aml + "annotation")
.Where(a => (string)a.Attribute(w + "type") == "Word.Bookmark.Start")
.Select(a => (string)a.Attribute(w + "name"))
This solution is not for XML but maybe helps you.
System.Collections.Generic.IEnumerable<BookmarkStart> BookMarks = wordDoc.MainDocumentPart.RootElement.Descendants<BookmarkStart>();
foreach (BookmarkStart current in BookMarks)
{
//Do some...
}

Get List of Styles

I'm trying to get a List of Styles in the following xml file using xdoc and LINQ.
<?xml version="1.0" encoding="UTF-8"?>
<kml>
<Document>
<Style id="style62">
<IconStyle>
<Icon>
<href>http://maps.gstatic.com/mapfiles/ms2/micons/yellow-dot.png</href>
</Icon>
</IconStyle>
</Style>
</Document>
</kml>
I cannot get my syntax right in order to get the ID="style62" AND also the value within href in the same LINQ select, can anyone help?
var styles = xdoc.Descendants(ns + "Style")
.Select(s => new
{
//HELP!?!
//E.G
//
//id = s.something (style62)
//href = s.something (url)
}).ToList();
if you are talking about a kml file like here https://developers.google.com/kml/documentation/KML_Samples.kml
then below code should work. The problem here is that every "Style" does not contain "href" tag.
var xDoc = XDocument.Parse(xml);
XNamespace ns = "http://www.opengis.net/kml/2.2";
var items = xDoc.Descendants(ns + "Style")
.Select(d =>
{
var h = d.Descendants(ns + "href").FirstOrDefault();
return new
{
Id = d.Attribute("id").Value,
Href = h == null ? null : h.Value
};
})
.ToList();
With a simple extension method, you can simplify the query
XNamespace ns = "http://www.opengis.net/kml/2.2";
var items = xDoc.Descendants(ns + "Style")
.Select(d => new
{
Id = d.Attribute("id").Value,
HRef = d.Descendants(ns + "href").FirstOrDefault()
.IfNotNull(h=>h.Value)
})
.ToList();
public static class S_O_Extensions
{
public static S IfNotNull<T, S>(this T obj,Func<T,S> selector)
{
if (obj == null) return default(S);
return selector(obj);
}
}
Something like this should work:
xdoc.Descendants(ns + "Style")
.Select(s => new
{
id = s.Attribute("id").Value,
href = s.Element("IconStyle")
.Element("Icon")
.Element("href")
.Value
});
Run this through LinqPad:
XDocument doc = XDocument.Parse("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<kml>" +
"<Document>" +
"<Style id=\"style62\">" +
"<IconStyle>" +
"<Icon>" +
"<href>http://maps.gstatic.com/mapfiles/ms2/micons/yellow-dot.png</href>" +
"</Icon>" +
"</IconStyle>" +
"</Style>" +
"</Document>" +
"</kml>");
var styles = from document in doc.Root.Elements("Document")
from style in document.Elements("Style")
where style.Attribute("id").Value == "style62"
select new
{
StyleElement = style,
Href = style.Element("IconStyle").Element("Icon").Element("href").Value
};
styles.Dump();
you can use linq like
var items = doc.Descendants("field")
.Where(node => (string)node.Attribute("name") == "Name")
.Select(node => node.Value.ToString())
.ToList();

LINQ to XML without knowing the nodes

I have this LINQ query:
XNamespace ns = NAMESPACE;
var items = (from c in doc.Descendants(ns +"Item")
select new Item
{
Title = c.Element(ns + "ItemAttributes").Element(ns + "Title").Value,
MFR = c.Element(ns + "ItemAttributes").Element(ns + "Manufacturer").Value,
Offer = c.Element(ns + "Offers").Element(ns + "TotalOffers").Value,
Amazon = c.Element(ns + "Offer").Element(ns + "Merchant").Elements(ns + "MerchantId"),
LowPrice = Convert.ToDouble(c.Element(ns + "FormattedPrice").Value),
SalesRank = Convert.ToInt32(c.Element(ns +"SalesRank").Value),
ASIN = c.Element(ns + "ASIN").Value
}).ToList<Item>();
It works great expect for when a node is not present. For example it my not have a MFR or a sales rank. How can I make it so if it does not have the node in question, it gives me a default value or at the very doesn't make me try catch my whole query for one item.
As far as I'm aware LINQ to XML doesn't support this. However I ran into this same mess in a project I was working on and created this extension for XElement to allow it. Maybe it could work for you:
public static XElement ElementOrDummy(this XElement parentElement,
XName name,
bool ignoreCase)
{
XElement existingElement = null;
if (ignoreCase)
{
string sName = name.LocalName.ToLower();
foreach (var child in parentElement.Elements())
{
if (child.Name.LocalName.ToLower() == sName)
{
existingElement = child;
break;
}
}
}
else
existingElement = parentElement.Element(name);
if (existingElement == null)
existingElement = new XElement(name, string.Empty);
return existingElement;
}
Basically it just checks to see if the element exists and if it doesn't it returns one with the same name and an empty value.
You can use XElement Explicit Conversion, e.g.:
(int?)c.Element(ns +"SalesRank")
Reference: http://msdn.microsoft.com/en-us/library/bb340386.aspx
if the problem that the XElement exists, but the value is blank? i.e.
<Item>
<ItemAttributes>
<Manufacturer></Manufacturer>
</ItemAttributes>
</Item>
then you can use the string.IsNullOrEmpty function
XNamespace ns = NAMESPACE;
var items = (from c in doc.Descendants(ns +"Item")
select new Item
{
MFR = if (string.IsNullOrEmpty(c.Element(ns + "ItemAttributes").Element(ns + "Manufacturer").Value)) ? "default value here" : c.Element(ns + "ItemAttributes").Element(ns + "Manufacturer").Value,
// omitted for brevity
}).ToList<Item>();

Categories