I'm trying to convert a XML file to a list. The XML file contains different products, and each product has different values, e.g.:
<product>
<id></id>
<name>Commentarii de Bello Gallico et Civili</name>
<price>449</price>
<type>Book</type>
<author>Gaius Julius Caesar</author>
<genre>Historia</genre>
<format>Inbunden</format>
</product>
<product>
<id></id>
<name>Katana Zero</name>
<price>199</price>
<type>Game</type>
<platform>PC, Switch</platform>
</product>
The problem is that some elements does not have all fields, some books can look like this for example:
<product>
<id></id>
<name>House of Leaves</name>
<price>49</price>
<type>Bok</type>
<author>Mark Z. Danielewski</author>
<genre>Romance</genre>
</product>
When I try adding these elements to the list, it works until I get an element that does not have all fields. When that happens, I get "System.NullReferenceException: 'Object reference not set to an instance of an object'."
List<Product> products= new List<Product>();
XElement xelement = XElement.Load(path);
IEnumerable<XElement> pr = xelement.Elements();
foreach (var p in pr)
{
switch (p.Element("type").Value)
{
case "Book":
temp.Add(new Book(1, int.Parse(employee.Element("price").Value),
0 ,
p.Element("name").Value,
p.Element("author").Value,
p.Element("genre").Value,
p.Element("format").Value");
break;
}
What I would like is to get a null value or "Not specified" when that happens, but I don't know how to do that in a good way. All I can think of are try catch for each variable but that seems uneccesary complicated.
How can I handle these cases in a good way?
Use a null check - ?
p.Element("name")?.Value,
p.Element("author")?.Value,
p.Element("genre")?.Value,
p.Element("format")?.Value");
I usually use a nested dictionary :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ConsoleApplication186
{
class Program
{
const string FILENAME = #"c:\temp\test.xml";
static void Main(string[] args)
{
XDocument doc = XDocument.Load(FILENAME);
Dictionary<string, Dictionary<string, string>> dict = doc.Descendants("product")
.GroupBy(x => (string)x.Element("name"), y => y.Elements()
.GroupBy(a => a.Name.LocalName, b => (string)b)
.ToDictionary(a => a.Key, b => b.FirstOrDefault()))
.ToDictionary(x => x.Key, y => y.FirstOrDefault());
}
}
}
Related
I have two XML documents:
XmlDocument languagesXML = new XmlDocument();
languagesXML.LoadXml(
#"<languages>
<language>
<name>English</name>
<country>8</country>
<country>9</country>
<country>3</country>
<country>12</country>
</language>
<language>
<name>French</name>
<country>1</country>
<country>3</country>
<country>7</country>
<country>13</country>
</language>
</languages>");
XmlDocument productsXML = new XmlDocument();
productsXML.LoadXml(#"<products>
<product>
<name>Screws</name>
<country>3</country>
<country>12</country>
<country>29</country>
</product>
<product>
<name>Hammers</name>
<country>1</country>
<country>13</country>
</product>
</products>");
I am trying to add the relative information, such as name and country of each language and product, to a list as I want to compare the two and group the languages that correspond to a certain language. For example, taking the above into account, my goal is to have an output similar to this:
Screws -> English, French
Hammers -> French
English and French correspond to Screws as they all share a common country value. Same with Hammers. (The above XML is just a snapshot of the entire XML).
I have tried using How to read a XML file and write into List<>? and XML to String List. While this piece of code:
var languages = new List<string>();
XmlNode xmlNode;
foreach(var node in languagesXML.LastChild.FirstChild.ChildNodes)
{
xmlNode = node as XmlNode;
languages.Add(xmlNode.InnerXml);
}
languages.ForEach(Console.WriteLine);
works, it will only add "English", "8", "9", "3", and "12" to the list. The rest of the document seems to be ignored. Is there a better way of doing what I'm trying to achieve? Would I even be able to compare and attain an output like what I need even if I got everything adding to a list? Would Muenchian grouping be something I should be looking at?
This is a job for LINQ to XML. Eg
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace ConsoleApp18
{
static class EnumerableUtils
{
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> col)
{
return new HashSet<T>(col);
}
}
class Program
{
static void Main(string[] args)
{
XDocument languagesXML = XDocument.Parse(
#"<languages>
<language>
<name>English</name>
<country>8</country>
<country>9</country>
<country>3</country>
<country>12</country>
</language>
<language>
<name>French</name>
<country>1</country>
<country>3</country>
<country>7</country>
<country>13</country>
</language>
</languages>");
var languages = languagesXML.Root
.Elements("language")
.Select(e =>
new
{
Name = (string)e.Element("name"),
Countries = e.Elements("country").Select(c => (int)c).ToHashSet()
})
.ToList();
XDocument productsXML = XDocument.Parse(#"<products>
<product>
<name>Screws</name>
<country>3</country>
<country>12</country>
<country>29</country>
</product>
<product>
<name>Hammers</name>
<country>1</country>
<country>13</country>
</product>
</products>");
var products = productsXML.Root
.Elements("product")
.Select(e =>
new
{
Name = (string)e.Element("name"),
Countries = e.Elements("country").Select(c => (int)c).ToHashSet()
})
.ToList();
var q = from p in products
from l in languages
where p.Countries.Overlaps(l.Countries)
let pl = new { p, l, }
group pl by p.Name into byProductName
select new
{
ProductName = byProductName.Key,
Languages = byProductName.Select(e => e.l.Name).ToList()
};
foreach (var p in q.ToList())
{
Console.WriteLine($"Product: {p.ProductName} is available in languages: {String.Join(",", p.Languages.ToArray())}");
}
}
}
}
outputs
Product Screws is available in languages English,French
Product Hammers is available in languages French
I'm trying to select a single node from an XML file based on two queries, I have a product ID for which I need the latest entry - highest issue number.
This is the format of my XML file:
<MyProducts>
<Product code="1011234">
<ProductName>Product Name A</ProductName>
<ProductId>101</ProductId>
<IssueNumber>1234</IssueNumber>
</Product>
<Product code="1029999">
<ProductName>Product Name B</ProductName>
<ProductId>102</ProductId>
<IssueNumber>9999</IssueNumber>
</Product>
<Product code="1015678">
<ProductName>Product Name A2</ProductName>
<ProductId>101</ProductId>
<IssueNumber>5678</IssueNumber>
</Product>
</MyProducts>
I need to get the <product> node from a ProductId that has the highest IssueNumber. For example if the ProductId is 101 I want the third node, if it's 102, I want the second node. There are around 50 different products in the file, split over three different product ids.
I've tried a number of XPath combinations using SelectSingleNode either by using the specific ProductID and IssueNumber nodes, or by using the code attribute of the product node (which is a combination of Id and Issue) without any success.
The code currently uses the code attribute, but only because we're also passing in the issue number and I want to be able to do this without the issue number (to decrease front end maintenance) as it's always the highest issue we want.
Current code is this:
XmlNode productNode = productXml.SelectSingleNode("/MyProducts/Product[#code='" + productCode + "']");
I've used these as well, they kind of work, but select the inner nodes, not the outer Product node:
XmlNodeList productNodes = productXml.SelectNodes("/MyProducts/Product/ProductId[text()='101']");
XmlNodeList productNodes = productXml.SelectNodes("/MyProducts/Product[not (../Product/IssueNumber > IssueNumber)]/IssueNumber");
I would like to use a combination of the two, something like this:
XmlNode productNode = productXml.SelectSingleNode("/MyProducts/Product/ProductId[text()='101'] and /MyProducts/Product[not (../Product/IssueNumber > IssueNumber)]/IssueNumber");
But that returns the error "...threw an exception of type 'System.Xml.XPath.XPathException'", but I also expect it won't return the Product node anyway.
Can this even be done in a single line, or will I have to loop through the nodes to find the right one?
Use Xml Linq
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ConsoleApplication167
{
class Program
{
const string FILENAME = #"c:\temp\test.xml";
static void Main(string[] args)
{
XDocument doc = XDocument.Load(FILENAME);
var products = doc.Descendants("Product")
.OrderByDescending(x => (int)x.Element("IssueNumber"))
.GroupBy(x => (int)x.Element("ProductId"))
.Select(x => x.First())
.ToList();
Dictionary<int, XElement> dict = products
.GroupBy(x => (int)x.Element("ProductId"), y => y)
.ToDictionary(x => x.Key, y => y.FirstOrDefault());
XElement highestId = dict[101];
}
}
}
Your last idea is almost there. You need to put the two clauses inside the [] selector. There is also max() available which I think clarifies the logic. This should work:
/MyProducts/Product[ProductId='101'
and IssueNumber=max(/MyProducts/Product[ProductId='101']/IssueNumber)]
This selects the Product which both has id 101 and has the highest IssueNumber of all id-101-products.
My program could be given two different kinds of xml files. The only way to tell the difference is by seeing what device it came from. How would I get the device name from this xml document?
<?xml version="1.0" encoding="UTF-8"?>
<DataFileSetup>
<System Name="Local">
<SysInfo>
<Devices>
<RealMeasurement>
<Hardware></Hardware>
<Device Type="MultiDevice">
<DriverBuffSizeInSec>5</DriverBuffSizeInSec>
<Card Index="0">
<DeviceName>SIRIUSi</DeviceName>
<DeviceSerialNumber>D017F09216</DeviceSerialNumber>
<FirmwareVersion>7.3.45.75</FirmwareVersion>
<VCXOValue>8802</VCXOValue>
</Card>
</Device>
</RealMeasurement>
</Devices>
</SysInfo>
</System>
</DataFileSetup>
The simple
var deviceType = xdoc.Element("DeviceName").Value;
either errors because there is nothing there or if i delete .Value it is just null.
Is there a simple way to get this value?
Please try the following.
c#
void Main()
{
const string fileName = #"e:\temp\device.xml";
XDocument xdoc = XDocument.Load(fileName);
Console.WriteLine(xdoc.Descendants("DeviceName").FirstOrDefault()?.Value);
}
Output
SIRIUSi
I like using a Dictionary in this case :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ConsoleApplication1
{
class Program
{
const string FILENAME = #"c:\temp\test.xml";
static void Main(string[] args)
{
XDocument doc = XDocument.Load(FILENAME);
Dictionary<string, XElement> dict = doc.Descendants("Device")
.GroupBy(x => (string)x.Descendants("DeviceName").FirstOrDefault(), y => y)
.ToDictionary(x => x.Key, y => y.FirstOrDefault());
}
}
}
Essentially, I have tried everything and for some reason I can't get the value of the elements in my XML based off the parameter I need it to meet. I feel like I'm close but I just don't know where I'm going wrong. I'm trying to get the value of the elements and put them into a list to be used elsewhere. Currently it doesn't put anything in the list.
I've tried XML Reader so now I'm giving Linq to XML a try but this won't work either.
private List<string> outputPath = new List<string>();
var doc = XDocument.Load(Path.Combine(projectDirectory, "JobPaths.xml"));
foreach (var child in doc.Element("Jobs").Elements("Job").Where(x => x.Attribute("Name").ToString() == jobName).Elements())
{
outputPath.Add(child.Name.ToString());
}
return outputPath;
Here's the XML:
<?xml version="1.0" encoding="utf-8" ?>
<Jobs>
<Job Name="events_monitoring_c">
<Path>\\stadb4412\</Path>
</Job>
<Job Name="events_monitoring_d">
<Path>\\stadb4412\</Path>
<Path>\\stadb1111\</Path>
<Path>\\stadb2412\</Path>
</Job>
</Jobs>
The jobName comes from the XML File, so I'm trying to get all the path elements based on the job name, regardless of how many there are. I want to get all the paths in the list to be used elsewhere.
To find nodes of a specific type/tag from an XDocument or XElement you use .Descendants(name), then you have .Attribute(name) that returns an XAttribute. To get its value, you use .Value, not .ToString().
Your code gets the Job elements, but then it gets the children elements as an IEnumerable of nodes and for each of them adds the Name of the tags, which is always Path.
What you are looking for is doc.Descendants("Job").Where(job=>job.Attribute("Name")?.Value==jobName).SelectMany(job=>job.Elements()).Select(elem=>elem.Value).ToList();
I did it without compiling, so I may be wrong.
You parse into a dictionary using Xml Linq :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ConsoleApplication1
{
class Program
{
const string FILENAME = #"c:\temp\test.xml";
static void Main(string[] args)
{
XDocument doc = XDocument.Load(FILENAME);
Dictionary<string, List<string>> dict = doc.Descendants("Job")
.GroupBy(x => (string)x.Attribute("Name"), y => y)
.ToDictionary(x => x.Key, y => y.Elements("Path").Select(z => (string)z).ToList());
}
}
}
I have a vendor provided XML file that I need to modify programmatically. The items (nodes, elements, attributes) are several levels deep and have multiple name value paired entries.
<root>
<VendorEntries>
<VendorEntry Name="Entry1">
<Attributes>
<Attribute Name="A" Value="abc"/>
<Attribute Name="B" Value="xyz"/>
</Attributes>
</VendorEntry>
<VendorEntry Name="Entry2">
<Attributes>
<Attribute Name="A" Value="lmn"/>
<Attribute Name="B" Value="qrs"/>
</Attributes>
</VendorEntry>
</VendorItems>
</root>
When looping through the following (in the VS2015 debugger), I see each of the ChildNodes, but don't see how to gain access to Entry1/A so it can be updated from "abc" to "efg"...
XmlDocument vendorXML = new XmlDocument();
vendorXML.Load(#"C:\path\file.xml");
XmlNodeList entries= vendorXML.SelectNodes("/root/VendorEntries/VendorEntry");
foreach (XmlNode entry in entries) { // /root/VendorEntries/VendorEntry(s) nodes
XmlAttribute entryName = entry.Attributes["Name"];
Console.WriteLine($"{entry.Name} {entryName.Value}"); // VendorEntry
foreach (XmlNode atNodes in entry.ChildNodes) { // /root/VendorEntries/VendorEntry/Attributes(s) nodes
foreach (XmlNode atNode in atNodes.ChildNodes) { // /root/VendorEntries/VendorEntry/Attributes/Attribute(s) nodes
XmlAttribute atName = atNode.Attributes["Name"];
XmlAttribute atValue = atNode.Attributes["Value"];
Console.WriteLine($"..{atNode.Name} {atName.Value} {atValue.Value}"); // ..Attribute Name Value>
if (entryName.Value.Equals("SOME_ENTRY") && atName.Value.Equals("SOME_PARAM"))
{
atValue.Value = "NEW PARAM ENTRY";
}
}
}
}
vendorXML.Save(#"C:\path\file.xml");
Modified: (Thanks to elgonzo) the code works now.
However, I still don't see a way to directly access the specific attribute to modify without looping through all of the ones that don't need modification. Does someone have a way to do this?
With XDocument, you can use Linq to XML to select specifically what you want to modify:
var vendorXml = XDocument.Load(#"c:\path\file.xml");
vendorXml.Descendants("VendorEntry")
.Where(a => a.Attribute("Name").Value == "Entry1")
.Descendants("Attribute")
.SingleOrDefault(a => a.Attribute("Name").Value == "A")
.SetAttributeValue("Value", "efg");
Or, as #Prany suggested, you can select the element using XPath:
vendorXml
.XPathSelectElement("//VendorEntry[#Name='Entry1']/Attributes/Attribute[#Name='A']")
.SetAttributeValue("Value", "efg");
Or if for some reason you want to use XmlDocument, you can use the same approach with that:
XmlDocument vendorXml = new XmlDocument();
vendorXml.Load(#"c:\path\file.xml");
var node = (XmlElement)vendorXml.SelectSingleNode("//VendorEntry[#Name='Entry1']/Attributes/Attribute[#Name='A']");
node.SetAttribute("Value", "efg");
You can use XpathSelectElement
XDocument doc = XDocument.Load(path);
string value = doc.XPathSelectElement("//VendorEntries/VendorEntry[1]/Attributes/Attribute[1]").LastAttribute.Value;
//This will select value from Entry1/A
I like using Xml Linq and putting results in a nested dictionary :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ConsoleApplication1
{
class Program
{
const string FILENAME = #"c:\temp\test.xml";
static void Main(string[] args)
{
XDocument doc = XDocument.Load(FILENAME);
Dictionary<string, Dictionary<string, XElement>> dict = doc.Descendants("VendorEntry")
.GroupBy(x => (string)x.Attribute("Name"), y => y.Descendants("Attribute")
.GroupBy(a => (string)a.Attribute("Name"), b => b)
.ToDictionary(a => a.Key, b => b.FirstOrDefault()))
.ToDictionary(x => x.Key, y => y.FirstOrDefault());
Dictionary<string, XElement> entry2 = dict["Entry2"];
entry2["B"].SetAttributeValue("Value", "xyz");
doc.Save(FILENAME);
}
}
}