I have the following XML Document (that can be redesigned if necessary) that stores records and errors.
<MYROOT>
<RECORDS>
<RECORD>
<DATETIME>11/03/2010 14:12:41</DATETIME>
<DOCUMENTID>1</DOCUMENTID>
</RECORD>
<RECORD>
<DATETIME>11/03/2010 14:12:44</DATETIME>
<DOCUMENTID>2</DOCUMENTID>
</RECORD>
<RECORD>
<DATETIME>11/03/2010 14:12:45</DATETIME>
<DOCUMENTID>3</DOCUMENTID>
</RECORD>
</RECORDS>
<ERRORS>
<ERROR TYPE="ERR">
<DATETIME>11/03/2010 14:12:41</DATETIME>
<DETAIL>There has been a error on page 1</DETAIL>
</ERROR>
<ERROR TYPE="ERR">
<DATETIME>11/03/2010 14:13:03</DATETIME>
<DETAIL>There has been a error on page 101</DETAIL>
</ERROR>
<ERROR TYPE="SEQ">
<DATETIME>11/03/2010 14:13:03</DATETIME>
<DETAIL>Sequence Error, expected Sequence No. 101 Read 1</DETAIL>
</ERROR>
</ERRORS>
</MYROOT>
I want to output the records and errors but obviously have to sort them by date so they appear in order.
How can I sort them by date, get a collection of XElements and then just do a foreach loop over them?
XDocument xml = System.Xml.Linq.XDocument.Parse(YOUR_XML);
IEnumerable<XElement> records = xml.Root.Element("RECORDS").Elements();
IEnumerable<XElement> errors = xml.Root.Element("ERRORS").Elements();
IEnumerable<XElement> elements = from el in records.Concat(errors)
orderby DateTime.Parse(el.Element("DATETIME").Value)
select el;
foreach (XElement el in elements)
{
// do something.
}
var elements = doc.Descendants("RECORD").Concat(doc.Descendants("ERROR")).
OrderBy(x => DateTime.Parse(x.Element("DATETIME").Value));
foreach (XElement element in elements)
{
// do something interesting with element
}
The IEnumerable is not very flexible, the best option may be to remove the elements from the enumerable, sort them and re-insert them, maintaining the correct order (relative to previous neighbors). It's a little more complicated if a sub element is the sort key
This will remove the named elements from the IEnumerable, sort them by a sub-element (may or may not be what you need) and re-insert them in the right place.
private void SortIdNodes(XElement parent, String elementName, String sortElementname)
{
XNode prevElem = null;
XNode nextElem = null;
// Initial node count, to verify sets are equal
int initialElementsCount = parent.Descendants().Count();
List<XElement> unOrdered = parent.Descendants(elementName).ToList<XElement>();
if (unOrdered.Count() < 2){
return; // No sorting needed
}
// Make note of the neighbors
prevElem = unOrdered[0].PreviousNode;
nextElem = unOrdered.Last().NextNode;
// Remove set from parent
unOrdered.ForEach(el =>
{
el.Remove();
});
// Order the set, language (IEnumerable) semantics prevents us from changing order in place
List <XElement> ordered = unOrdered.OrderBy(x => x.Descendants(sortElementname).FirstOrDefault().Value).ToList<XElement>();
// Add to parent in correct order
if (prevElem != null) // If there's a first neighbor
{
ordered.ForEach(el =>
{
prevElem.AddAfterSelf(el);
prevElem = el;
});
}
else if (nextElem != null) // If there's only an end neighbor
{
ordered.Reverse();
ordered.ForEach(el =>
{
nextElem.AddBeforeSelf(el);
nextElem = el;
});
}
else // we're the only children of the parent, just add
{
ordered.ForEach(el =>
{
parent.Add(el); // add in order
});
}
int finalElementCount = parent.Descendants().Count();
if (initialElementsCount != finalElementCount)
{
throw new Exception("Error with element sorting, output collection not the same size as the input set.");
}
}
Related
I'm trying to load contents of a XML file into a list of custom types using Linq following the instructions in the official microsoft dotNet api:
https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.find?view=netframework-4.8
My xml file looks like this:
<directives>
<dir directive="Question" response="Response"></dir>
<dir directive="Q2" response="Response2"></dir>
<dir directive="Q3" response="Response3"></dir>
</directives>
and the importing code is pretty much the same as in the example linked above.
public static void ImportDirectives()
{
// Create XML elements from a source file.
XElement xTree = XElement.Load(AppDomain.CurrentDomain.BaseDirectory + "directives.xml");
// Create an enumerable collection of the elements.
IEnumerable<XElement> elements = xTree.Elements();
// Evaluate each element and set set values in the book object.
foreach (XElement el in elements)
{
Directive dir = new Directive();
dir.directive = el.Attribute("directive").Value;
IEnumerable<XElement> props = el.Elements();
foreach (XElement p in props)
{
if (p.Name.ToString().ToLower() == "response")
{
dir.response = p.Value;
}
}
Dir.Add(dir);
}
}
The code works fine if I remove the root element but only add the root element into my list if I add one.
I'd prefer having a root element just to make my XML look proper.
How would I access the elements within the root element using this code?
When you add root then xml like
<Root>
<directives>
<dir directive="Question" response="Response"></dir>
<dir directive="Q2" response="Response2"></dir>
<dir directive="Q3" response="Response3"></dir>
</directives>
</Root>
When you fetch first Elements() then
<directives>
<dir directive="Question" response="Response"></dir>
<dir directive="Q2" response="Response2"></dir>
<dir directive="Q3" response="Response3"></dir>
</directives>
Again fetch Elements() then you'll get Node
<dir directive="Question" response="Response"></dir>
Then you access attribute and values
public static void ImportDirectives()
{
// Create XML elements from a source file.
XElement xTree = XElement.Load(AppDomain.CurrentDomain.BaseDirectory + "directives.xml");
// Create an enumerable collection of the elements.
IEnumerable<XElement> elements = xTree.Elements();
// Evaluate each element and set set values in the book object.
foreach (XElement el in elements.Elements())
{
Directive dir = new Directive();
dir.directive = el.Attribute("directive").Value;
IEnumerable<XElement> props = el.Elements();
foreach (XElement p in props)
{
if (p.Name.ToString().ToLower() == "response")
{
dir.response = p.Value;
}
}
Dir.Add(dir);
}
}
If you are sure that your XML Schema is going to be fixed, you can access the attribute values of the XML elements directly.
I have attached a sample code.
public static void ImportDirectives()
{
string fileName = AppDomain.CurrentDomain.BaseDirectory + "directives.xml";
// Create XML elements from a source file.
XElement xTree = XElement.Load(fileName);
// Create an enumerable collection of the elements.
IEnumerable<XElement> elements = xTree.Elements();
// Evaluate each element and set set values in the book object.
foreach (XElement el in elements)
{
string directive = el.Attribute("directive").Value;
string response = el.Attribute("response").Value;
Console.WriteLine(directive + ":" + response);
}
}
This is Xml
<ItemWarehouseInfo>
<row>
<MinimalStock>0.000000</MinimalStock>
<MaximalStock>0.000000</MaximalStock>
<MinimalOrder>0.000000</MinimalOrder>
<StandardAveragePrice>0.000000</StandardAveragePrice>
<Locked>tNO</Locked>
<WarehouseCode>Mc</WarehouseCode>
<DefaultBinEnforced>tNO</DefaultBinEnforced>
</row>
...other equal lines
</ItemWarehouseInfo>
I have to remove all child nodes from every row node except for the WarehouseCode node
I tried this method but obviously I'm wrong and nothing changes:
XDocument xdoc = XmlHelper.LoadXDocFromString(xmlOITM);
XElement magaRow = xdoc.Root.Descendants(Constants.Articoli.ART_MAGA_NODES).FirstOrDefault();//ItemWarehouseInfo node
List<XElement> row = magaRow.Elements().ToList();//row node
foreach(XElement child in row.Elements())
{
if (child.Name != "WarehouseCode")
{
child.Remove();
}
}
This is the final result that I expect:
<ItemWarehouseInfo>
<row>
<WarehouseCode>Mc</WarehouseCode>
</row>
...other equal lines
</ItemWarehouseInfo>
doc.Descendants("row")
.Elements()
.Where(e => e.Name != "WarehouseCode")
.Remove();
Explanation:
doc.Descendants("row") - Finds all row elements no matter how deep they are.
Elements() - Gets all immediate children elements
Where() - Gets all elements whose name is not WarehouseCode
Remove() - Deletes all found elements
You don't need to call .Elements() for each row, you already got the list of elements:
foreach(XElement child in row)
{
if (child.Name != "WarehouseCode")
{
child.Remove();
}
}
Get row, remove it. Create new row, add the row's WarehouseCode, and then add that row to the magaRow. If this doesn't work, I suspect namespaces are causing issues.
XElement row = magaRow.Element("row");
row.Remove();
XElement newRow = new XElement("row");
newRow.Add(row.Element("WarehouseCode"));
magaRow.Add(newRow);
I have the following XML and query through the ID,how do get the Parent Hierarchy
<Child>
<Child1 Id="1">
<Child2 Id="2">
<Child3 Id="3">
<Child4 Id="4">
<Child5 Id="5"/>
<Child6 Id="6"/>
</Child4>
</Child3>
</Child2>
</Child1>
</Child>
In this if i query(Id = 4) and find out the Parent elements using Linq in the particular element how to get the following output with Hierarchy.
<Child>
<Child1 Id="1">
<Child2 Id="2">
<Child3 Id="3">
<Child4 Id="4"/>
</Child3>
</Child2>
</Child1>
</Child>
Thanks In Advance.
Assume you want just one node parent tree:
string xml = #"<Child>
<Child1 Id="1">
<Child2 Id="2">
<Child3 Id="3">
<Child4 Id="4">
<Child5 Id="5"/>
<Child6 Id="6"/>
</Child4>
</Child3>
</Child2>
</Child1>
</Child>";
TextReader tr = new StringReader(xml);
XDocument doc = XDocument.Load(tr);
IEnumerable<XElement> myList =
from el in doc.Descendants()
where (string)el.Attribute("Id") == "4" // here whatever you want
select el;
// select your hero element in some way
XElement hero = myList.FirstOrDefault();
foreach (XElement ancestor in hero.Ancestors())
{
Console.WriteLine(ancestor.Name); // rebuild your tree in a separate document, I print ;)
}
To search for every element of your tree iterate retrieve the node with the select query without the where clause and call the foreach for every element.
Based on the sample XML provided, you could walk up the tree to find the parent node once you've found the node in question:
string xml =
#"<Child>
<Child1 Id='1'>
<Child2 Id='2'>
<Child3 Id='3'>
<Child4 Id='4'>
<Child5 Id='5'/>
<Child6 Id='6'/>
</Child4>
</Child3>
</Child2>
</Child1>
</Child>";
var doc = XDocument.Parse( xml );
// assumes there will always be an Id attribute for each node
// and there will be an Id with a value of 4
// otherwise an exception will be thrown.
XElement el = doc.Root.Descendants().First( x => x.Attribute( "Id" ).Value == "4" );
// discared all child nodes
el.RemoveNodes();
// walk up the tree to find the parent; when the
// parent is null, then the current node is the
// top most parent.
while( true )
{
if( el.Parent == null )
{
break;
}
el = el.Parent;
}
In Linq to XML there is a method called AncestorsAndSelf on XElement that
Returns a collection of elements that contain this element, and the
ancestors of this element.
But it will not transform your XML tree the way you want it.
What you want is:
For a given element, find the parent
Remove all elements from parent but the given element
Remove all elements from the given element
Something like this in Linq (no error handling):
XDocument doc = XDocument.Parse("<xml content>");
//finding element having 4 as ID for example
XElement el = doc.Descendants().First(el => el.Attribute("Id").Value == "4");
el.RemoveNodes();
XElement parent = el.Parent;
parent.RemoveNodes();
parent.Add(el);
[Edit]
doc.ToString() must give you what you want as a string.
[Edit]
Using RemoveNodes instead of RemoveAll, the last one also removes attributes.
Removing nodes from the chosen element too.
I found the following way
XElement elementNode = element.Descendants()
.FirstOrDefault(id => id.Attribute("id").Value == "4");
elementNode.RemoveNodes();
while (elementNode.Parent != null)
{
XElement lastNode = new XElement(elementNode);
elementNode = elementNode.Parent;
elementNode.RemoveNodes();
elementNode.DescendantsAndSelf().Last().AddFirst(lastNode);
}
return or Print elementNode.
I have the following XDocument called XDoc:
<?xml version="1.0" encoding="utf-8"?>
<DatabaseList>
<Database DatabaseName="c2501_data">
<Plugin PluginName="FooPlugin" LastRun="1/21/2013 3:22:08 PM" />
<Plugin PluginName="SpecialPlugin" LastRun="2013-01-21T15:22:09.3791103-05:00" />
<Plugin PluginName="BarPlugin" LastRun="2013-01-21T15:23:13.0964814-05:00" />
</Database>
</DatabaseList>
I'm writing a program that searches to see when the last time a plugin was run on a database, if at all. I use the following two pieces of code to figure out if an entry exists for a plugin on a database:
var h = (from el in XDoc.Root.Elements("Database")
where el.Element("Plugin").Attribute("PluginName").Value=="FooPlugin"
&& el.Attribute("DatabaseName").Value=="c2501_data"
select el.Element("Plugin"));
var e = (from el in XDoc.Root.Elements("Database")
where el.Element("Plugin").Attribute("PluginName").Value=="BarPlugin"
&& el.Attribute("DatabaseName").Value == "c2501_data"
select el.Element("Plugin"));
if ((from el in XDoc.Root.Elements("Database")
where el.Element("Plugin").Attribute("PluginName").Value == "BarPlugin"
&& el.Attribute("DatabaseName").Value == "c2501_data"
select el.Element("Plugin")).Count() == 0)
{
XElement SpecialPlugin = new XElement("Plugin",
new XAttribute("PluginName", "BarPlugin"),
new XAttribute("LastRun", DateTime.Now));
var CurNode = from node in XDoc.Root.Elements("Database")
where (string)node.Attribute("DatabaseName").Value == "c2501_data"
select node;
foreach (var node in CurNode)
node.Add(SpecialPlugin);
XDoc.Save(RuntimesPath);
//XDoc.Root.Elements("Database").Attribute("DatabaseName").
}
The problem that I'm having is that even though there is clearly an entry for BarPlugin, the count will always return 0 and e will always be unable to create an enumberable. Can anyone explain to me why this might be? FooPlugin always works correctly and returns the Plugin information for h.
Thanks for any help.
You're selecting a Database element where it contains a child element called Plugin with a given name. Since you have only one Database element, you're getting the same outer element each time. You then take that database element and return the first Plugin child, which will always be Foo, in this case. You need to find the appropriate Database element and then query through each of the child elements so you can return them:
public static XElement GetPlugin(XDocument XDoc, string databaseName, string pluginName)
{
var h = from database in XDoc.Root.Elements("Database")
where database.Attribute("DatabaseName").Value == databaseName
from plugin in database.Elements("Plugin")
where plugin.Attribute("PluginName").Value == pluginName
select plugin;
return h.FirstOrDefault();
}
Or, if you prefer, in method syntax:
var q = XDoc.Root.Elements("Database")
.Where(db => db.Attribute("DatabaseName").Value == databaseName)
.SelectMany(db => db.Elements("Plugin"))
.Where(plugin => plugin.Attribute("PluginName").Value == pluginName);
return q.FirstOrDefault();
Try this:
var db = XDoc.Root.Elements("Database");
var z = (from el in db.Elements("Plugin")
where el.Attribute("PluginName").Value == "BarPlugin"
&& el.Parent.Attribute("DatabaseName").Value == "c2501_data"
select el).FirstOrDefault();
if(z != null)
.....
I'm using Elements() method, to get all child elements and Parent property to look for the parent element "DatabaseName".
Problem in your code is that your el.Element() is searching only for the first element, thus it can find only "FooPlugin", which is on the first position in the xml.
From MSDN doc Element():
Gets the first (in document order) child element with the specified XName.
This is my xml file
<profiles>
<profile id='8404'>
<name>john</name>
<name>mark</name>
</profile>
<profile id='8405'>
<name>john</name>
</profile>
</profiles>
and I want to select profiles where last "name" child value= john, the result should contains the profile with id=8405 only
what it the xpath that can evaluate this?
here is my trial:
var filterdFile = profilefileXML.XPathSelectElements("/profiles/profile[name[last()]='john']");
but it doesn't make sense.
Updated:
My trail is correct, there was only a syntax error in it. Thanks all
You can apply multiple indexing operations with successive [...]:
var doc = XDocument.Parse(xml); //the xml from your question
var node = doc.XPathSelectElement("/profiles/profile[name='john'][last()]");
Console.WriteLine(node.Attribute("id").Value); //outputs 8405
This will return the last profile element that contains the element name with a value of john.
If you on the other hand want to return all elements which last name element has a value of john, your XPath should work already:
var nodes = doc.XPathSelectElements("/profiles/profile[name[last()]='john']");
foreach (var node in nodes)
{
Console.WriteLine(node.Attribute("id").Value);
}
You can also try LINQ
XDocument xDoc = XDocument.Load("data.xml");
var matches = xDoc.Descendants("profile")
.Where(profile => XElement.Parse(profile.LastNode.ToString()).Value == "john");
And you can access the xml data with a foreach
foreach(XElement xEle in lastEle)
{
var xAttribute = xEle.Attribute("id");
if (xAttribute != null)
{
var id = xAttribute.Value;
}
var lastName = XElement.Parse(xEle.LastNode.ToString()).Value;
}