I am using xpath to query my xml file in C#.
Here is my xml file
<?xml version="1.0" encoding="ISO-8859-1"?>
<bookstore>
<book>
<title lang="eng">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title lang="eng">Learning XML</title>
<price>39.95</price>
</book>
</bookstore>
And my C# code is
XPathNavigator nav;
XPathDocument docNav;
XPathNodeIterator NodeIter;
String strExpression;
docNav = new XPathDocument(#"C:\\DCU\\XQUE.xml");
nav = docNav.CreateNavigator();
// This expression uses standard XPath syntax.
strExpression = "/bookstore[./book/price>35.00]";
NodeIter = nav.Select(strExpression);
while (NodeIter.MoveNext())
{
Console.WriteLine("{0}", NodeIter.Current.OuterXml);
}
But I want to get output like this,
<bookstore>
<book>
<title lang="eng">Learning XML</title>
<price>39.95</price>
</book>
</bookstore>
I think anything missing with my xpath query line, please lead me a way out..
It seems that you're misunderstanding the purpose of XPath selection and what it can do. XPath won't create a whole new XML document for you. The typical choice for taking an XML input and producing a different XML output is to use XSLT. This would do it in your case:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="book[price <= 35]" />
</xsl:stylesheet>
If you really want to use just C# to do this, you could always do the following:
XPathNavigator nav;
XPathDocument docNav;
XPathNodeIterator NodeIter;
String strExpression;
docNav = new XPathDocument(#"C:\\DCU\\XQUE.xml");
nav = docNav.CreateNavigator();
// This expression uses standard XPath syntax.
strExpression = "/bookstore/book[price > 35.00]";
NodeIter = nav.Select(strExpression);
Console.WriteLine("<bookstore>");
while (NodeIter.MoveNext())
{
Console.WriteLine("{0}", NodeIter.Current.OuterXml);
}
Console.WriteLine("</bookstore>");
That expression selects <bookstore> elements, so the output will be the whole <bookstore> (with all its child book elements). If you want specific books you need to use a different XPath
strExpression = "/bookstore/book[price>35.00]";
which will print just the <book> elements that match, but without the surrounding <bookstore> tags.
Related
I get an error when I am studying XSL through C#, and I don't know how to fix.
I knwo there are some XSL syntax errors, but I don't know how to fix it afer searching some XSL docs.
Following are sample code of XML/XSL/C#.
Test XML file, Books.xml
<?xml version='1.0'?>
<Books>
<Publishers>
<Level1Publishers>
<PublisherA>
<Book bid="A1"><Name>Test Book from A</Name></Book>
</PublisherA>
<PublisherB>
<Book bid="B1"><Name>Test Book from B</Name></Book>
</PublisherB>
</Level1Publishers>
</Publishers>
</Books>
XSL file, Books.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:param name="PUBLISHER_NAME"/>
<xsl:template match="//Publishers">
<xsl:variable name="PUBLISHER_PATH" select="./Level1Publishers/$PUBLISHER_NAME/Book"/>
<Book>
<BookId>
<xsl:value-of select="$PUBLISHER_PATH/#bid"/>
</BookId>
<Name>
<xsl:value-of select="$PUBLISHER_PATH/Name"/>
</Name>
</Book>
</xsl:template>
</xsl:stylesheet>
C# code to perform the transformation
using System;
using System.IO;
using System.Xml.Xsl;
namespace TestXSL
{
class Program
{
static void Main(string[] ags)
{
XsltArgumentList args = new XsltArgumentList();
args.AddParam("PUBLISHER_NAME", "", "PublisherB");
XslCompiledTransform xslt = new XslCompiledTransform();
xslt.Load("Books.xsl");
StringWriter writer = new StringWriter();
xslt.Transform("Books.xml", args, writer);
Console.WriteLine(writer.ToString());
}
}
}
It reports exception when load XSL file.
Unhandled Exception: System.Xml.Xsl.XslLoadException: Unexpected token '$' in the expression.
./Level1Publishers/ -->$<-- PUBLISHER_NAME/Book
at System.Xml.Xsl.XslCompiledTransform.LoadInternal(Object stylesheet, XsltSettings settings, XmlResolver stylesheetResolver)
at System.Xml.Xsl.XslCompiledTransform.Load(String stylesheetUri)
at TestXSL.Program.Main(String[] ags) in Program.cs:line 14
I then change the define PUBLISHER_PATH in the following way.
<xsl:variable name="PUBLISHER_PATH" select="concat('./Level1Publishers/',$PUBLISHER_NAME,'/Book')"/>
It reports another exception, it seems no syntax errors, but runtime error
Unhandled Exception: System.Xml.Xsl.XslTransformException: Expression must evaluate to a node-set.
at System.Xml.Xsl.Runtime.XsltConvert.EnsureNodeSet(IList`1 listItems)
at <xsl:template match="//Publishers">(XmlQueryRuntime {urn:schemas-microsoft-com:xslt-debug}runtime)
at <xsl:apply-templates>(XmlQueryRuntime {urn:schemas-microsoft-com:xslt-debug}runtime, XPathNavigator )
at <xsl:apply-templates>(XmlQueryRuntime {urn:schemas-microsoft-com:xslt-debug}runtime, XPathNavigator )
at Root(XmlQueryRuntime {urn:schemas-microsoft-com:xslt-debug}runtime)
at Execute(XmlQueryRuntime {urn:schemas-microsoft-com:xslt-debug}runtime)
at System.Xml.Xsl.XmlILCommand.Execute(Object defaultDocument, XmlResolver dataSources, XsltArgumentList argumentList, XmlSequenceWriter results)
at System.Xml.Xsl.XmlILCommand.Execute(Object defaultDocument, XmlResolver dataSources, XsltArgumentList argumentList, XmlWriter writer)
at System.Xml.Xsl.XslCompiledTransform.Transform(String inputUri, XsltArgumentList arguments, TextWriter results)
at TestXSL.Program.Main(String[] ags) in Program.cs:line 16
I hope this helps you.
<xsl:param name="PUBLISHER_NAME"/>
<xsl:template match="//Publishers/Level1Publishers">
<Book>
<BookId>
<xsl:value-of select=".//Book[Name=$PUBLISHER_NAME]/#bid"/>
</BookId>
<Name>
<xsl:value-of select="$PUBLISHER_NAME"/>
</Name>
</Book>
</xsl:template>
<xsl:template match="node()">
<xsl:apply-templates/>
</xsl:template>
I am developing a app with C# and trying to complete a XML that I have got from a JSON. And for the XML to be valid for my app, I need to group the elements with the same name under a father element.
For example, I got this XML
<root>
<row>
<id>0001</id>
<type>credit</type>
<investment>1000</investment>
<ppr>0.83</ppr>
<candidate>
<id>5001</id>
<name>Hugo</name>
</candidate>
<candidate>
<id>5002</id>
<name>Jack</name>
</candidate>
<candidate>
<id>5005</id>
<name>Kate</name>
</candidate>
</row>
And I need to group all the elements with the name candidate, under a father node candidates, like this
<root>
<row>
<id>0001</id>
<type>credit</type>
<investment>1000</investment>
<ppr>0.83</ppr>
<candidates>
<candidate>
<id>5001</id>
<name>Hugo</name>
</candidate>
<candidate>
<id>5002</id>
<name>Jack</name>
</candidate>
<candidate>
<id>5005</id>
<name>Kate</name>
</candidate>
</candidates>
</row>
But here is my problem: I don't know the names that I can receive from the JSON. So I need to do this comparison and complete the XML without knowing the "candidate" node name. I need this for any name that I can receive.
Also in this example the XML only has 2 levels, but it can have any number of levels. I can iterate over the XML without problem with this function:
public void findAllNodes(XmlNode node)
{
Console.WriteLine(node.Name);
foreach (XmlNode n in node.ChildNodes)
findAllNodes(n);
}
How can I make the comparison and group the nodes?
A fairly naive implementation could use LINQ to group elements by name and add a parent element for those that have more than 1 item in a group. This would be recursive, so child elements of an element were grouped until the tree was exhausted.
The naive-ness is that such a solution would break if there were mixed content elements, and it would group elements that weren't siblings (basically, both issues will result in things ending up in the wrong order). It should give you a good start, and could be enough for your purposes.
private static IEnumerable<XElement> GroupElements(IEnumerable<XElement> elements)
{
var elementsByName = elements.GroupBy(x => x.Name);
foreach (var grouping in elementsByName)
{
var transformed = grouping.Select(e =>
new XElement(e.Name,
GroupElements(e.Elements()),
e.Attributes(),
e.Nodes().OfType<XText>()));
if (grouping.Count() == 1)
{
yield return transformed.Single();
}
else
{
var groupName = grouping.Key + "s";
yield return new XElement(groupName, transformed);
}
}
}
You can use this by parsing/loading your existing XML and then transforming the root elements and creating a new document from those:
var original = XDocument.Parse(xml);
var grouped = new XDocument(GroupElements(original.Elements()));
See this fiddle for a working demo.
Here's an XSLT 2.0 solution:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:template match="*[*]">
<xsl:copy>
<xsl:for-each-group select="*" group-adjacent="node-name(.)">
<xsl:choose>
<xsl:when test="count(current-group()) > 1">
<xsl:element name="{name()}s" namespace="{namespace-uri()}">
<xsl:apply-templates select="current-group()"/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
<xsl:template match="*">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
Output:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<row>
<id>0001</id>
<type>credit</type>
<investment>1000</investment>
<ppr>0.83</ppr>
<candidates>
<candidate>
<id>5001</id>
<name>Hugo</name>
</candidate>
<candidate>
<id>5002</id>
<name>Jack</name>
</candidate>
<candidate>
<id>5005</id>
<name>Kate</name>
</candidate>
</candidates>
</row>
</root>
Limitations
It doesn't handle mixed content (elements with children plus text content)
It drops attributes (easily fixed)
For sending atomic data types will use like
transformer.SetParameter(new QName("", "", customXml), new XdmAtomicValue("true"));
how to pass a XML/Node as a param to XSLT from C# ?
Can you please help me
followed your code it's working fine but i am getting only text inside the xml(what i am passing in parameter) but not Nodes
XSLT :
<xsl:param name="look-up" as="document-node()"/>
<xsl:template match="xpp:document">
<w:document xml:space="preserve">
<xsl:value-of select="$look-up"/>
</w:document>
</xsl:template>
XML
<?xml version="1.0" encoding="UTF-8"?>
<document version="1.0" xmlns="http://www.sdl.com/xpp">
//some tags
</document>
passing parameter (xml)
<Job>
</id>
</Job>
I think you should use the Processor object to construct an XdmNode, see the documentation which says:
The Processor provides a method NewDocumentBuilder which, as the name
implies, returns a DocumentBuilder. This may be used to construct a
document (specifically, an XdmNode) from a variety of sources. The
input can come from raw lexical XML by specifying a Stream or a Uri,
or it may come from a DOM document built using the Microsoft XML
parser by specifying an XmlNode, or it may be supplied
programmatically by nominating an XmlReader.
Then you can pass in the XdmNode to the SetParameter method http://saxonica.com/documentation/html/dotnetdoc/Saxon/Api/XsltTransformer.html#SetParameter%28Saxon.Api.QName,Saxon.Api.XdmValue%29 as XdmValue is a base class of XdmNode (XmlValue -> XdmItem -> XdmNode).
Here is an example that works for me with Saxon 9.5 HE on .NET:
Processor proc = new Processor();
DocumentBuilder db = proc.NewDocumentBuilder();
XsltTransformer trans;
using (XmlReader xr = XmlReader.Create("../../XSLTFile1.xslt"))
{
trans = proc.NewXsltCompiler().Compile(xr).Load();
}
XdmNode input, lookup;
using (XmlReader xr = XmlReader.Create("../../XMLFile1.xml"))
{
input = db.Build(xr);
}
using (XmlReader xr = XmlReader.Create("../../XMLFile2.xml"))
{
lookup = db.Build(xr);
}
trans.InitialContextNode = input;
trans.SetParameter(new QName("lookup-doc"), lookup);
using (XmlWriter xw = XmlWriter.Create(Console.Out))
{
trans.Run(new TextWriterDestination(xw));
}
The XSLT is
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:param name="lookup-doc"/>
<xsl:key name="map" match="map" use="key"/>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="item">
<xsl:copy>
<xsl:value-of select="key('map', ., $lookup-doc)/value"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
the XML documents are
<root>
<item>foo</item>
</root>
and
<root>
<map>
<key>foo</key>
<value>bar</value>
</map>
</root>
the resulting output is
<root>
<item>bar</item>
</root>
I have some xml (in a file, but can be a string) which I need to parse, e.g.:
var xmlDocument = new XmlDocument();
xmlDocument.LoadXml(xmlText);
Given the following XML:
<foo>
<cat>...</cat>
<cat>...</cat>
<dog>...</dog>
<cat>...</cat>
<dog>...</dog>
</foo>
I'm not sure how I can extract all the cat and dog elements and put them into the following output :-
<foo>
<cat>...</cat>
<cat>...</cat>
....
</foo>
and the same with dogs.
What's the trick to extract those nodes and put them into separate XMLDocuments.
Use Linq to XML as it has a much nicer API.
var doc = XElement.Parse(
#"<foo>
<cat>...</cat>
<cat>...</cat>
<dog>...</dog>
<cat>...</cat>
<dog>...</dog>
</foo>");
doc.Descendants("dog").Remove();
doc now contains this:
<foo>
<cat>...</cat>
<cat>...</cat>
<cat>...</cat>
</foo>
Edit:
While Linq to XML itself provides a nice API to work with XML, the power of Linq and its projection capabilities enables you to shape your data as you see fit.
Consider this, for example. Here the descendant elements are grouped by name and projected into a new root element which is then wrapped into a XDocument. Note that this creates an enumerable of XDocument.
var docs=
from d in doc.Descendants()
group d by d.Name into g
select new XDocument(
new XElement("root", g)
);
docs now contains:
<root>
<cat>...</cat>
<cat>...</cat>
<cat>...</cat>
</root>
---
<root>
<dog>...</dog>
<dog>...</dog>
</root>
Oh, by the way. The Descendants method goes through all descendant elements, use Elements if you only want the immediate child elements.
Here are the Linq to XML docs on MSDN
The easiest way will be to use XSLT and apply it on you XMLDocument in such way you won't modify your source and have as much outputs as you need.
The code for applying transform is
XslCompiledTransform xslTransform = new XslCompiledTransform();
StringWriter writer = new StringWriter();
xslTransform.Load("cat.xslt");
xslTransform.Transform(doc.CreateNavigator(),null, writer);
return writer.ToString();
And the simple cat.xslt is
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="foo">
<xsl:copy>
<xsl:copy-of select = "cat" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Since you are using XmlDocument: Load it twice from the same file and remove the unwanted nodes. Here is a link that shows you how: Removing nodes from an XmlDocument.
var xmlDocument = new XmlDocument();
xmlDocument.LoadXml(xmlText);
XmlNode root = doc.DocumentElement;
nodeList = root.SelectNodes("//cat");
foreach (XmlNode node on nodeList)
{
root.RemoveChild(node);
}
I am trying to read the book.xml file provided as an example on the MSDN website.
<?xml version="1.0" encoding="utf-8" ?>
<bookstore>
<book genre="autobiography" publicationdate="1981-03-22" ISBN="1-861003-11-0">
<title>The Autobiography of Benjamin Franklin</title>
<author>
<first-name>Benjamin</first-name>
<last-name>Franklin</last-name>
</author>
<price>8.99</price>
</book>
<book genre="novel" publicationdate="1967-11-17" ISBN="0-201-63361-2">
<title>The Confidence Man</title>
<author>
<first-name>Herman</first-name>
<last-name>Melville</last-name>
</author>
<price>11.99</price>
</book>
<book genre="philosophy" publicationdate="1991-02-15" ISBN="1-861001-57-6">
<title>The Gorgias</title>
<author>
<name>Plato</name>
</author>
<price>9.99</price>
</book>
</bookstore>
I have the following code until now:
static void Main()
{
XmlDocument document = new XmlDocument();
document.Load(#"c:\books.xml");
XPathNavigator navigator = document.CreateNavigator();
XPathNodeIterator nodes = navigator.Select("/bookstore/book");
while (nodes.MoveNext())
{
Console.WriteLine(nodes.Current.HasAttributes);
}
}
It seems this code is reading everything, but from here on if I want to display, say, just the titles of all book etc., how do I access them?
You can iterate over the titles if you change the XPath expression to select all title nodes:
XPathDocument document = new XPathDocument(#"c:\tmp\smpl5.xml");
XPathNavigator navigator = document.CreateNavigator();
XPathNodeIterator nodes = navigator.Select("/bookstore/book/title");
foreach (XPathNavigator item in nodes)
{
Console.WriteLine(item.Value);
}
Note that you don't need to create an XmlDocument if you don't plan to modify the document. Using an XPathDocument is usually more light-weight.
you can also use this "//title" instead of "/bookstore/book"