How to get elements without child elements? - c#

Here is my xml:
<Root>
<FirstChild id="1" att="a">
<SecondChild id="11" att="aa">
<ThirdChild>123</ThirdChild>
<ThirdChild>456</ThirdChild>
<ThirdChild>789</ThirdChild>
</SecondChild>
<SecondChild id="12" att="ab">12</SecondChild>
<SecondChild id="13" att="ac">13</SecondChild>
</FirstChild>
<FirstChild id="2" att="b">2</FirstChild>
<FirstChild id="3" att="c">3</FirstChild>
</Root>
This xml doc is very big and may be 1 GB size or more. For better performance in querying, i want to read xml doc step by step. So, in first step i want to read only "First Child"s and their attributes like below:
<FirstChild id="1" att="a"></FirstChild>
<FirstChild id="2" att="b">2</FirstChild>
<FirstChild id="3" att="c">3</FirstChild>
And after that, I maybe want to get "SecondChild"s by id of their parent and so ...
<SecondChild id="11" att="aa"></SecondChild>
<SecondChild id="12" att="ab">12</SecondChild>
<SecondChild id="13" att="ac">13</SecondChild>
How can I do it?
Note: XDoc.Descendants() or XDoc.Elements() load all specific elements with all child elements!

Provided that you have memory available to hold the file, I suggest treating each search step as an item in the outer collection of a PLINQ pipeline.
I would start with an XName collection for the node collections that you want to retrieve. By nesting queries within XElement constructors, you can return new instances of your target nodes, with only name and attribute information.
With a .Where(...) statement or two, you could also filter the attributes being kept, allow for some child nodes to be retained, etc.
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace LinqToXmlExample
{
public class Program
{
public static void Main(string[] args)
{
XElement root = XElement.Load("[your file path here]");
XName[] names = new XName[] { "firstChild", "secondChild", "thirdChild" };
IEnumerable<XElement> elements =
names.AsParallel()
.Select(
name =>
new XElement(
$"result_{name}",
root.Descendants(name)
.AsParallel()
.Select(
x => new XElement(name, x.Attributes()))))
.ToArray();
}
}
}

I suggest creating a new element and copy the attributes.
var sourceElement = ...get "<FirstChild id="1" att="a">...</FirstChild>" through looping, xpath or any method.
var element = new XElement(sourceElement.Name);
foreach( var attribute in sourceElement.Attributes()){
element.Add(new XAttribute(attribute.Name, attribute.Value));
}

In VB this you could do this to get a list of FirstChild
'Dim yourpath As String = "your path here"
Dim xe As XElement
'to load from a file
'xe = XElement.Load(yourpath)
'for testing
xe = <Root>
<FirstChild id="1" att="a">
<SecondChild id="11" att="aa">
<ThirdChild>123</ThirdChild>
<ThirdChild>456</ThirdChild>
<ThirdChild>789</ThirdChild>
</SecondChild>
<SecondChild id="12" att="ab">12</SecondChild>
<SecondChild id="13" att="ac">13</SecondChild>
</FirstChild>
<FirstChild id="2" att="b">2</FirstChild>
<FirstChild id="3" att="c">3</FirstChild>
</Root>
Dim ie As IEnumerable(Of XElement)
ie = xe...<FirstChild>.Select(Function(el)
'create a copy
Dim foo As New XElement(el)
foo.RemoveNodes()
Return foo
End Function)

Related

C# XML Select single sub node in node by value

I have the following problem, I want to select the book with the author "Johnny Dapp33", which unfortunately does not work.
XML Code:
<employees xmlns:bar="http://www.bar.org">
<employee id="Test1">
<name>Johnny Dapp</name>
<author>Al Pacino</author>
</employee>
<employee id="Test2">
<name>Johnny Dapp33</name>
<author>Al Pacino</author>
</employee>
</employees>
I would have tried it via ".SelectSingleNode", unfortunately I always fail with the XPath.
Thank you for your help!
Let's say we have a file called Employees.xml in our project directory.
We can load the xml file in our memory by using this assignment:
XmlDocument doc = new XmlDocument();
doc.Load("Employees.xml");
Second we try to find a single node (presuambly) by its id in a structure employees/employee (this is our path), no we have to add the search param (id in this case) like this:
XmlNode singleNode = doc.SelectSingleNode("/employees/employee[#id='Test1']");
Console.WriteLine(singleNode.OuterXml);
However if we only know the name we are looking for we can also search for that specific value like this. We search in the employee node for the node value of name with the value of Johnny Dapp33:
XmlNode singleNode = doc.SelectSingleNode("descendant::employee[name='Johnny Dapp33']");
Console.WriteLine(singleNode.OuterXml);
While dealing with XMl, it is better to use LINQ to XML API.
It is available in the .Net Framework since 2007.
c#
void Main()
{
const string filePath = #"e:\Temp\WizardZZ.xml";
XDocument xdoc = XDocument.Load(filePath);
var employee = xdoc.Descendants("employee")
.Where(d => d.Elements("name").FirstOrDefault().Value.Equals("Johnny Dapp33"));
Console.WriteLine(employee);
}
Output
<employee id="Test2">
<name>Johnny Dapp33</name>
<author>Al Pacino</author>
</employee>
If you need to use XPath (and there must be a very strong reason for it), you can use it with XElement:
var xml = """
<employees xmlns:bar="http://www.bar.org">
<employee id="Test1">
<name>Johnny Dapp</name>
<author>Al Pacino</author>
</employee>
<employee id="Test2">
<name>Johnny Dapp33</name>
<author>Al Pacino</author>
</employee>
</employees>
""";
var x = XElement.Parse(xml);
var employees = x.XPathSelectElements("/employee[name='Johnny Dapp33']");
if (employees is not null)
{
foreach (var employee in employees)
{
WriteLine((string)employee.Element("name") ?? "[name] not found");
}
}
else
{
WriteLine("did not find any employees");
}

Change XML element value in c#

My C# code:
XDocument doc = XDocument.Load(filename);
IEnumerable<XElement> collection =
doc.Elements("BCIRequest").Elements("Card").Elements("SelectedPIN");
My XML document:
<?xml version="1.0" encoding="utf-8"?>
<BCIRequest Version="2.0"
xmlns="urn:xxxxxx:bci:request">
<Header>
<SenderCode>XX99</SenderCode>
<SenderID>9999</SenderID>
<SequenceNumber>123</SequenceNumber>
<CardGroupCount>2</CardGroupCount>
<CardCount>4</CardCount>
<BlockCount>2</BlockCount>
</Header>
<!--card groups (must precede cards and blocks)-->
<CardGroup RequestID="1">
<CustomerNumber>XX01234567</CustomerNumber>
<CardGroupName Emboss="true">GROUP ONE</CardGroupName>
</CardGroup>
<CardGroup RequestID="2"
RequestRef="87416CB7-DAEF-483A-BD08-1A885531D958">
<CustomerNumber>XX12345678</CustomerNumber>
<CardGroupName Emboss="false">GROUP TWO</CardGroupName>
</CardGroup>
<Card RequestID="3">
<CustomerNumber>XX01234567</CustomerNumber>
<DriverCard>
<Driver Emboss="true">MARGE SIMPSON</Driver>
</DriverCard>
<CardTypeID>10</CardTypeID>
<PurchaseCategoryID>11</PurchaseCategoryID>
<Reissue>false</Reissue>
<GeneratedPIN/>
<OdoPrompt>false</OdoPrompt>
<CRNPrompt>false</CRNPrompt>
</Card>
<Card RequestID="4">
<CustomerNumber>XX12345678</CustomerNumber>
<VehicleCard>
<VRN Emboss="true">KYI 830</VRN>
</VehicleCard>
<CardTypeID>10</CardTypeID>
<PurchaseCategoryID>11</PurchaseCategoryID>
<Reissue>false</Reissue>
<SelectedPIN>0123</SelectedPIN>
<OdoPrompt>false</OdoPrompt>
<CRNPrompt>false</CRNPrompt>
</Card>
<Card RequestID="5">
<CustomerNumber>XX01234567</CustomerNumber>
<BearerCard>
<Bearer Emboss="true">OPEN XXXXXX</Bearer>
</BearerCard>
<CardTypeID>10</CardTypeID>
<PurchaseCategoryID>11</PurchaseCategoryID>
<Reissue>false</Reissue>
<FleetPIN/>
<OdoPrompt>false</OdoPrompt>
<CRNPrompt>false</CRNPrompt>
</Card>
<Block RequestID="6">
<CustomerNumber>XX01234567</CustomerNumber>
<PAN>7002999999999999991</PAN>
</Block>
<Card RequestID="7"
RequestRef="956EA6C5-7D7E-4622-94D0-38CAD9FCC8DF">
<CustomerNumber>XX01234567</CustomerNumber>
<DriverCard>
<Driver Emboss="true">HOMER SIMPSON</Driver>
<VRN Emboss="true">795 DVI</VRN>
</DriverCard>
<EmbossText>SPRINGFIELD POWER</EmbossText>
<CardTypeID>10</CardTypeID>
<TokenTypeID>20</TokenTypeID>
<PurchaseCategoryID>30</PurchaseCategoryID>
<ExpiryDate>2018-12</ExpiryDate>
<Reissue>true</Reissue>
<SelectedPIN>0123</SelectedPIN>
<OdoPrompt>true</OdoPrompt>
<CRNPrompt>true</CRNPrompt>
<!--address with optional fields specified-->
<CardDeliveryAddress OneTimeUse="false">
<ContactName>M xxxx</ContactName>
<ContactTitle>Mr</ContactTitle>
<CompanyName>Sxxxx</CompanyName>
<Line1>Sector 22-F</Line1>
<Line2>Springfield Power Plant</Line2>
<Line3>xxx Road</Line3>
<City>xxxx</City>
<Zipcode>xxxx</Zipcode>
<CountryCode>xxx</CountryCode>
</CardDeliveryAddress>
<!--address with only required fields-->
<PINDeliveryAddress OneTimeUse="true">
<Line1>xxxx</Line1>
<City>xxx</City>
<Zipcode>xxxx</Zipcode>
<CountryCode>xxxx</CountryCode>
</PINDeliveryAddress>
<Limits>
<Value Transaction="unlimited" Daily="200" Weekly="unlimited" Monthly="400"/>
<Volume Transaction="100" Daily="unlimited" Weekly="unlimited" Monthly="unlimited"/>
<Transactions Daily="unlimited" Weekly="unlimited" Monthly="unlimited"/>
<Day Monday="true" Tuesday="true" Wednesday="true" Thursday="true" Friday="true" Saturday="false" Sunday="false"/>
<Time Start="unlimited" End="17:00:00"/>
</Limits>
<Products>
<FuelProductRestrictionID>40</FuelProductRestrictionID>
<NonFuelProductRestrictionID>51</NonFuelProductRestrictionID>
<NonFuelProductRestrictionID>52</NonFuelProductRestrictionID>
<NonFuelProductRestrictionID>53</NonFuelProductRestrictionID>
<NonFuelProductRestrictionID>54</NonFuelProductRestrictionID>
<NonFuelProductRestrictionID>55</NonFuelProductRestrictionID>
</Products>
</Card>
<Block RequestID="8"
RequestRef="69A3E44D-DC10-4BEE-9249-1FC3C651BA0E">
<CustomerNumber>xxxxx</CustomerNumber>
<PAN>xxxxxx</PAN>
</Block>
</BCIRequest>
I need to update the element value in the above values. The old value is:
<SelectedPIN>0123</SelectedPIN>
And the new value should be:
<SelectedPIN EncryptedPIN="TKDS" FormNumber="000793906306">****</SelectedPIN>
Can anyone can help me on this?
If I selected the BCIRequest element, it's returning a null value. I've tried many solutions but unable to get one working on this XML file.
There many ways an Xml can be be modified, I prefer XDocument
XDocument doc = XDocument.Parse(input);
foreach (var element in doc.Descendants("SelectedPIN")) // filter if you want single element, in example I modifed for all elements.
{
element.Add(new XAttribute("EncryptedPIN", "TKDS"));
element.Add(new XAttribute("FormNumber", "000793906306"));
element.Value = "xxxxx"; //new value
}
and finally you can save the document using
doc.Save();
Take a look at this Demo
The root node (BCIRequest) contains a namespace so you need to include that into your query. Something like this should work:
XNamespace ns = "urn:xxxxxx:bci:request";
IEnumerable<XElement> collection = doc.Elements(ns + "BCIRequest").Elements(ns + "Card").Elements(ns + "SelectedPIN");

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
}

Creating XML from the parent node information

I have an xml file that has information of the roots as follows:
usequery attribute has a SQL query from which the XML element data is populated from.
<ARAXmlFormat>
<root name="level1" index = "1" parentid ="0" haschildren="yes"/>
<root name="level2" index = "2" parentid ="1" haschildren="yes" usequery="query2"/>
<root name="level21" index = "3" parentid ="2" haschildren="no" usequery="query1"/>
<root name="level22" index = "4" parentid ="2" haschildren="no" usequery="query3"/>
<root name="level3" index = "5" parentid ="1" haschildren="yes"/>
<root name="level31" index = "6" parentid ="5" haschildren="no" usequery="query4"/>
</ARAXmlFormat>
From this I need to generate an XML tree as follows. As of now I already have individual XElements for leve2, level21, level22, level31. But how do I create XML but adding these elements in the XML format as below from the parentid information above?
<level1>
<level2>
<level21 attrib1 ="val1" attrib2="val2"/>
<level22 attrib1 ="val1" attrib2="val2"/>
</level2>
<level3>
<level31 attrib1 ="val1" attrib2="val2"/>
</level3>
</level1>
You can do it like this:
var source = XDocument.Parse(xml); // or whatever
var sourceElems = source.Root.Elements("root");
var result = new XDocument(new XElement("result"));
var resultElems = new Dictionary<int, XElement>();
resultElems.Add(0, result.Root);
foreach (var sourceElem in sourceElems)
{
var resultElem = new XElement((string)sourceElem.Attribute("name"));
int parentId = (int)sourceElem.Attribute("parentid");
resultElems[parentId].Add(resultElem);
resultElems.Add((int)sourceElem.Attribute("index"), resultElem);
}
Basically, walk through the elements, and for each find the parent in a dictionary, add it as a child to that parent and finally add it to the dictionary, so that it can be parent itself. This assumes that parent is always declared before all its children.
With your source data, it creates the following result:
<result>
<level1>
<level2>
<level21 />
<level22 />
</level2>
<level3>
<level31 />
</level3>
</level1>
</result>
I don't know where the value of attrib1 and attrib2 attributes come from. But as far as the hierarchical structure of the output XML tree is concerned, you could construct it recursively, using something like this:
var doc = XDocument.Load("roots.xml"); // read input xml file
Func<int, IEnumerable<XElement>> selectChildNodes = null; // declare delegate
selectChildNodes = parentId => doc.Elements("ARAXmlFormat").Elements().Where(
p => p.Attribute("parentid").Value == parentId.ToString()
).Select(
x => new XElement(
x.Attribute("name").Value,
selectChildNodes(Convert.ToInt32(x.Attribute("index").Value))
)
);
IEnumerable<XElement> levels = selectChildNodes(0);
This solution works independent of order of <root> elements in the input file. selectChildNodes is a delegate that accepts the index of parent node as the input parameter and returns all child nodes of that element recursively. The output would be like this, without attributes:
<level1>
<level2>
<level21 />
<level22 />
</level2>
<level3>
<level31 />
</level3>
</level1>
The value of node attributes could be included when constructing each node with new XElement.

Get InnerText from Collection

Is there a way to get the innertext of a node when the node is inside a collection
Currently i have this
Collection<string> DependentNodes = new Collection<string>();
foreach (XmlNode node in nodes)
{
for (int i = 0; i < node.ChildNodes.Count; i++)
{
DependentNodes.Add(node.ChildNodes[i].InnerXml);
//the reason i'm using InnerXml is that it will return all the child node of testfixture in one single line,then we can find the category & check if there's dependson
}
}
string selectedtestcase = "abc_somewords";
foreach (string s in DependentNodes)
{
if(s.Contains(selectedtestcase))
{
MessageBox.Show("aaa");
}
}
When i debug string s or the index has this inside of it[in a single line]
<testfixture name="1" description="a">
<categories>
<category>abc_somewords</category>
</categories>
<test name="a" description="a">
<dependencies>
<dependson typename="dependsonthis" />
</dependencies>
</test>
</testfixture>
What i'm trying to do is when we reach "testfixture 1" it will find "abc_somewords" & search the "dependson typename"node(if any) and get the "typename"(which is "dependonthis").
Could you use linq to xml. Something like the below might be a decent start
xml.Elements("categories").Where(x => x.Element("category").Value.Contains(selectedtestcase));
This is off the top of my head so might will need refining
P.S. Use XElement.Load or XElement.Parse to get your xml into XElements
Since you already working with XmlNode you could use a XPath expression to select the desired textfixture node, and select the dependency value:
XmlDocument doc = // ...
XmlNode node = doc.SelectSingleNode("//testfixture[contains(categories/category, \"abc\")]/test/dependencies/dependson/");
if (node != null)
{
MessageBox.Show(node.Attributes["typename"]);
}
This selects the dependson node which belongs to a testfixture node with a category containing "abc". node.Attributes["typename"] will return the value of the typename attribute.
Edited:
Updated XPath expression to the more specific question information
Assumptions
As you are looping in your code and wanting to create a collection I'm assuming the actual Xml File has several testfixture nodes inside such as the below assumed example:
<root>
<testfixture name="1" description="a">
<categories>
<category>abc_somewords</category>
</categories>
<test name="a" description="a">
<dependencies>
<dependson typename="dependsonthis" />
</dependencies>
</test>
</testfixture>
<testfixture name="2" description="a">
<categories>
<category>another_value</category>
</categories>
<test name="b" description="a">
<dependencies>
<dependson typename="secondentry" />
</dependencies>
</test>
</testfixture>
<testfixture name="3" description="a">
<categories>
<category>abc_somewords</category>
</categories>
<test name="c" description="a">
<dependencies>
<dependson typename="thirdentry" />
</dependencies>
</test>
</testfixture>
</root>
The Code using Linq to Xml
To use Linq you must reference the following name spaces:
using System.Linq;
using System.Xml.Linq;
Using Linq To Xml on the above assumed xml file structure would look like this:
// To Load Xml Content from File.
XDocument doc1 = XDocument.Load(#"C:\MyXml.xml");
Collection<string> DependentNodes = new Collection<string>();
var results =
doc1.Root.Elements("testfixture")
.Where(x => x.Element("categories").Element("category").Value.Contains("abc_somewords"))
.Elements("test").Elements("dependencies").Elements("dependson").Attributes("typename").ToArray();
foreach (XAttribute attribute in results)
{
DependentNodes.Add(attribute.Value.Trim());
}
Result
The resulting Collection will contain the following:
As you can see, only the text of the typename attribute has been extracted where the dependson nodes where in a testfixture node which contained a category node with the value of abc_somewords.
Additional Notes
If you read the xml from a string you can also use this:
// To Load Xml Content from a string.
XDocument doc = XDocument.Parse(myXml);
If your complete Xml structure is different, feel free to post it and I change the code to match.
Have Fun.
I don't know what is "nodes" you are using.
Here is code with your requirement(What I understood).
Collection<XmlNode> DependentNodes = new Collection<XmlNode>();
XmlDocument xDoc = new XmlDocument();
xDoc.Load(#"Path_Of_Your_xml");
foreach (XmlNode node in xDoc.SelectNodes("testfixture")) // Here I am accessing only root node. Give Xpath if ur requrement is changed
{
for (int i = 0; i < node.ChildNodes.Count; i++)
{
DependentNodes.Add(node.ChildNodes[i]);
}
}
string selectedtestcase = "abc_somewords";
foreach (var s in DependentNodes)
{
if (s.InnerText.Contains(selectedtestcase))
{
Console.Write("aaa");
}
}
using System;
using System.Xml;
namespace ConsoleApplication6
{
class Program
{
private const string XML = "<testfixture name=\"1\" description=\"a\">" +
"<categories>" +
"<category>abc_somewords</category>" +
"</categories>" +
"<test name=\"a\" description=\"a\">" +
"<dependencies>" +
"<dependson typename=\"dependsonthis\" />" +
"</dependencies>" +
"</test>" +
"</testfixture>";
static void Main(string[] args)
{
var document = new XmlDocument();
document.LoadXml(XML);
var testfixture = document.SelectSingleNode("//testfixture[#name = 1]");
var category = testfixture.SelectSingleNode(".//category[contains(text(), 'abc_somewords')]");
if(category != null)
{
var depends = testfixture.SelectSingleNode("//dependson");
Console.Out.WriteLine(depends.Attributes["typename"].Value);
}
Console.ReadKey();
}
}
}
Output: dependsonthis

Categories