C# - Clean way for reading data from DataSet created from XML - c#

I've got an app with config.xml file added for user settings.
I am reading it simply by:
DataSet config = new DataSet();
config.ReadXml(configPath);
Parameters in config.xml are in columns grouped under some tables:
<?xml version="1.0" standalone="yes"?>
<UserSetup>
<TableA>
<revBegin>value1</revBegin>
<revEnd>value2</revEnd>
...
</TableA>
...
</UserSetup>
What I'm looking for is a clean way to read from DataSet config, without memorizing table or column names.
In my current implementation I achieved that by following class:
public static class MyTables
{
public static class TableA
{
public const String name = "TableA";
public const String revBegin = "revBegin";
public const String revEnd = "revEnd";
...
}
...
}
And I read values like this:
String revEnd = config.Tables[MyTables.TableA.name].Rows[0][MyTables.TableA.revEnd].ToString();
But I somehow fill that it is quite easy problem solved in quite complicated - not to say nasty - way.
Do you have any idea how can I make things simpler or cleaner?
P.S. At some point I tried reducing config.Tables[MyTables.TableA.name] part to config.Tables[MyTables.TableA] but - how I see it - it would require adding Index[object] to sealed class DataTableCollection and overriding ToString() method in my static class - both impossible. Am I right?

Unless you absolutely have to use a DataSet to read the XML, you can achieve the same results using XML serialization. Annotate your classes with the below.
[XmlRoot]
[Serializable]
public class UserSetUp
{
[XmlElement]
public TableA TableA { get; set; }
}
[Serializable]
public class TableA
{
[XmlElement]
public string revBegin { get; set; }
[XmlElement]
public string revEnd { get; set; }
}
Assuming your config in on C:\ for this example.
var configStream = File.OpenRead(#"C:\Config.xml");
var reader = new StreamReader(configStream);
var xmlString = reader.ReadToEnd();
var serializer = new XmlSerializer(typeof(UserSetUp));
var stringReader = new StringReader(xmlString);
var userSetup = (UserSetUp)serializer.Deserialize(stringReader);
I've tested it with the below XML and it works ok.
<?xml version="1.0" encoding="utf-16" ?>
<UserSetUp>
<TableA>
<revBegin>1</revBegin>
<revEnd>2</revEnd>
</TableA>
</UserSetUp>
Hope that helps you on your way.

Related

Cannot load internal nodes from XML file

I have a xml schema in the form of
<?xml version="1.0" encoding="UTF-8"?>
<project ref="edward" name="Edward(A)">
<desc/>
<Zones>
<Zone ref="1" name="Zone1"/>
<Zone ref="2" name="Zone2"/>
<Zone ref="3" name="Zone3"/>
<Zone ref="4" name="Zone4"/>
</Zones>
</project>
I am trying to extract all the Zones value using Xml to Linq (not an expert)
I tried
string xmlString = System.IO.File.ReadAllText("..\\..\\..\\v1.xml");
XDocument xdoc = new XDocument();
xdoc=XDocument.Parse(xmlString);
var ele = xdoc.Descendants("Zones")
.Select(x => (string)x.Element("name"))
.FirstOrDefault();
// this is null
var result = xdoc.Element("project").Descendants("Zones").Descendants("Zone");
foreach (var item in result)
{
Console.WriteLine(item.name); //what should be here
}
You could add the items to a list or using a Dictionary<int, string> may look something like...
Dictionary<int, string> Zones = new Dictionary<int, string>();
foreach (var item in result) {
int.TryParse(item.Attribute("ref").Value, out int value);
Zones.Add(value, item.Attribute("name").Value);
Console.WriteLine(item.Attribute("name").Value);
Console.WriteLine(item.Attribute("ref").Value);
}
Another approach using two simple Classes…
Looking closer, I am betting using a couple of classes and the System.Xml.Serialization; library may make this easier. Given the XML file. One Class appears obvious… called a Zone object with the int and string properties. Then another Class we could call ZoneProject.
The ZoneProject Class would have three (3) properties. In this case two string properties called Ref and Name, and a List<Zone> of Zone objects called Zones. The bare minimum of these two classes may look something like…
The Zone Class
public class Zone {
[XmlAttribute("ref")]
public int Ref { get; set; }
[XmlAttribute("name")]
public string Name { get; set; }
}
The ZoneProject Class
[XmlRoot("project")]
public class ZoneProject {
[XmlAttribute("ref")]
public string Ref { get; set; }
[XmlAttribute("name")]
public string Name { get; set; }
[XmlArray("Zones")]
public List<Zone> Zones { get; set; }
}
To help “deserialize” the XML we added additional qualifiers to help mate the XML element/attributes to the particular property of each class. The XmlRoot is the base class ZoneProject with the XmlAttributes and the collection of Zone objects using the XmlArray qualifier. This property will be a List<Zone> collection. The same follows for the Zone Class.
With this set up, we can use the System.Xml.Serialization; library and deserialize the XML file into a single ZoneProject object. Something like…
string filePath = #"PathToYourXML_File\file.xml";
XmlSerializer serializer = new XmlSerializer(typeof(ZoneProject));
ZoneProject TheZoneProject;
using (FileStream fs = File.OpenRead(filePath)) {
TheZoneProject = (ZoneProject)serializer.Deserialize(fs);
}
It should be noted, that the answer from #Enyra on the SO question How to read XML file into List<>? …was helpful in the answer above.
I hope this makes sense.
Please check your xml file path. I think that , problem here.
string xmlString = System.IO.File.ReadAllText(dicorrectPath);
You can collect you zones attributes with Linq:
var result = xdoc.Element("project").Descendants("Zones").Descendants("Zone");
var zonesAtributes = result.Select(x =>
new
{
name = x.Attribute("name").Value,
#ref = x.Attribute("ref").Value
}).ToArray();

Reading XML with records with different columns in same records array

I need to parse a XML response in C# and load in SQL.
Just to brief,i know how to use XMLSerializer to parse the xml, so that is not i am looking for. My concern is my XML structure which i received from from web request. Below is the subset of xml, i received from xml
<apiXML>
<recordList>
<record id="31" >
<administration_name>admin1</administration_name>
<creator>Leekha, Mohit</creator>
<object_category>painting</object_category>
<object_number>1243</object_number>
<id>31</id>
<reproduction.reference>2458.jpg</reproduction.reference>
<title lang="nl-NL" invariant="false">The Title1</title>
<title lang="nl-NL" invariant="false">The Title</title>
<title lang="nl-NL" invariant="false">Different Title</title>
</record>
<record id="32" >
<administration_name>admin1</administration_name>
<creator>Leekha, Mohit</creator>
<object_category>painting</object_category>
<object_number>AL1111</object_number>
<id>32</id>
<reproduction.reference>AL1111.jpg</reproduction.reference>
<title lang="nl-NL" invariant="false">Health</title>
</record>
<record id="34" >
<administration_name>admin2</administration_name>
<creator>Leekha,Mohit</creator>
<creator>System</creator>
<object_category>earthenware</object_category>
<object_category>ABC</object_category>
<object_category>Remote</object_category>
<object_number>Z.567 & X-124</object_number>
<id>34</id>
<reproduction.reference>Z.567 & X-124(1).jpg</reproduction.reference>
<reproduction.reference>Z.567 & X-124(2).jpg</reproduction.reference>
<reproduction.reference>Z.567 & X-124(3).jpg</reproduction.reference>
</record>
</recordList>
</apiXML>
My Concerns:
Some records have multiple data members with same name. Like record id 31 has 3 titles
Number of columns are different for each record.
So all i am asking is for suggestions how could i deal with scenario. Any suggestions are welcome
You'll need a couple of supporting classes to get that XML deserialized as is, as you didn't specify any other requirements.
Your database wold have tables for your record elements and all the collections within.
Serialization classes
Those classes will hold an in memory representation of your XML. At the root will be the Api class.
[XmlRoot("apiXML")]
public class Api
{
[XmlArray("recordList")]
[XmlArrayItem("record", typeof(Record))]
public List<Record> RecordList {get;set;}
}
[Serializable]
public class Record
{
[XmlAttribute("id")]
public int RecordId {get;set;}
[XmlElement("id")]
public int Id {get;set;}
[XmlElement("administration_name")]
public string AdministrationName {get;set;}
[XmlElement("object_number")]
public string ObjectNumber {get;set;}
[XmlElement("creator")]
public List<Creator> Creators {get;set;}
[XmlElement("object_category")]
public List<ObjectCategory> ObjectCategories {get;set;}
[XmlElement("reproduction.reference")]
public List<ReproductionReference> ReproductionReferences {get;set;}
[XmlElement("title")]
public List<Title> Titles {get;set;}
}
[Serializable]
public class Title:Child
{
[XmlAttribute("invariant")]
public bool Invariant {get;set;}
[XmlAttribute("lang")]
public string Culture {get;set;}
[XmlText]
public string Text {get;set;}
}
public class Child
{
[XmlIgnore]
public int ParentId {get;set;}
}
[Serializable]
public class Creator:Child
{
[XmlText]
public string Text {get;set;}
}
[Serializable]
public class ObjectCategory:Child
{
[XmlText]
public string Text {get;set;}
}
[Serializable]
public class ReproductionReference:Child
{
[XmlText]
public string Text {get;set;}
}
Deserialization
With the classes correctly annotated deserializing the XML only needs a couple
of lines:
var ser = new XmlSerializer(typeof(Api));
var sr = new StringReader(xml);
var api = (Api) ser.Deserialize(sr);
Processing and building up tables
In the variable api we now have the in-memory object graph which you can project on a relational database schema. For normalized model you'll need the following tables:
Record(id, [fields in class])
Creator(id, ..)
Title(id, ...)
ObjectCategory(id, ...)
ObjectNumber (id, ...)
ReproductionReference(id, ...)
Between these tables you'll need link tables that follow all the same convention like the one between Record and Creator:
RecordCreator(RecordId, CreatorId)
I'll assume you know how to create those tables and create a connection to your database.
// use an SqlAdapter.Fill to get the below dataset call
// sqlAdapter.Fill(ds);
var ds = new DataSet();
// this is here so you can test without a database
// test mocking code
var recTable = ds.Tables.Add("Record");
recTable.Columns.Add("Id");
recTable.Columns.Add("AdministrationName");
recTable.Columns.Add("ObjectNumber");
var creTable = ds.Tables.Add("Creator");
creTable.Columns.Add("Id", typeof(int)).AutoIncrement = true;
creTable.Columns.Add("Text");
var reccreTable = ds.Tables.Add("RecordCreator");
reccreTable.Columns.Add("RecordId");
reccreTable.Columns.Add("CreatorId");
// end mocking code
// copy object graph and build link tables
foreach(var record in api.RecordList)
{
// each main record is created
var rtRow = recTable.NewRow();
rtRow["Id"] = record.Id;
rtRow["AdministrationName"] = record.AdministrationName;
rtRow["ObjectNumber"] = record.ObjectNumber;
recTable.Rows.Add(rtRow);
// handle each collection
foreach(var creator in record.Creators)
{
DataRow creRow; // will hold our Creator row
// first try to find if the Text is already there
var foundRows = creTable.Select(String.Format("Text='{0}'", creator.Text));
if (foundRows.Length < 1)
{
// if not, add it to the Creator table
creRow = creTable.NewRow(); // Id is autoincrement!
creRow["Text"] = creator.Text;
creTable.Rows.Add(creRow);
}
else
{
// otherwise, we found an existing one
creRow = foundRows[0];
}
// link record and creator
var reccreRow = reccreTable.NewRow();
reccreRow["RecordId"] = record.Id;
reccreRow["CreatorId"] = creRow["Id"];
reccreTable.Rows.Add(reccreRow);
}
// the other collections follow a similar pattern but is left for the reader
}
// now call Update to write the changes to the db.
// SqlDataAdapter.Update(ds);
That concludes the code and structure you'll need to store that SQL in an RDBMS database without losing information.

Deserialize XML element from InnerText and Value attribute

I have XML Serializable class with property Name
[Serializable]
public class Item
{
[XmlElement("name")]
public string Name { get; set; }
}
and I want it to be able to deserialize XML file that I have in two ways:
<item>
<name>Name</name>
</item>
and
<item>
<name value="Name" />
</item>
The first works fine but what should I do to be able to deserialize the second also with the same class?
XML Serialization attributes work both with serialization and deserialization. If we'll assume that it might be possible to use attributes for deserializing instance of Item from two different xml structures, then how serialization should work - should it serialize instance name to element value, or to attribute? Or to both? That's why you cannot deserialize two different xml structures into single class. Use two different classes or deserialize it manually without usage of XML Serialization attributes.
I found another way to solve my problem using only one class maybe someone will find this useful
[Serializable]
public class Item
{
[XmlElement("name")]
public NameElement NameElement { get; set; }
}
public class NameElement
{
[XmlAttribute("value")]
public string Value { get; set; }
[XmlText]
public string Text { get; set; }
[XmlIgnore]
public string Name
{
get { return String.IsNullOrEmpty(this.Value) ? this.Text : this.Value; }
set { this.Value = value; }
}
}
Maybe it's not super elegant but it works in both cases and uses the same class.
Since you have mentioned that XML data is coming from external sources, so obviously you don't have control over that.
Therefore you can follow any of the option as below:
Create separate class per XML data structure, because as far I know there is no way to control XML Deserialization when using XmlSerializer
You can use XDocument to read the XML by self, to overcome this limitation.
If going by second idea, I have created small Console Application to demonstrate that.
Main piece of code is as below:
MemoryStream xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xmlData));
XDocument doc = XDocument.Load(xmlStream);
var records = from record in doc.Descendants("item").Descendants()
select new Item(!record.IsEmpty ? record.Value : record.Attribute("value").Value);
Here I'm reading the element using LinqToXml and checking if the element is not empty, i.e. Value is not blank, then use Value otherwise read the value from element's Attribute.
Console application (Complete code):
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Xml.Serialization;
namespace Console.TestApp
{
class Program
{
static string xmltypeFirst = #"<item>
<name>John</name>
</item>";
static string xmltypeSecond = #"<item>
<name value='Smith' />
</item>";
static void Main(string[] args)
{
var data = xmltypeFirst;
var result = Deserialize(data).ToList();
Console.WriteLine("Name: " + result[0].Name);
data = xmltypeSecond;
result = Deserialize(data).ToList();
Console.WriteLine("Name: " + result[0].Name);
Console.WriteLine("Press any to key to exit..");
Console.ReadLine();
}
private static IEnumerable<Item> Deserialize(string xmlData)
{
MemoryStream xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xmlData));
XDocument doc = XDocument.Load(xmlStream);
var records = from record in doc.Descendants("item").Descendants()
select new Item(!record.IsEmpty ? record.Value : record.Attribute("value").Value);
return records;
}
}
[Serializable]
public class Item
{
public Item(string name)
{
this.Name = name;
}
[XmlElement("name")]
public string Name { get; set; }
}
}
Note: To run this you will need to add reference to System.Xml.Linq.dll in your project.
Reference: here

Deserialization of serialized data fails

I am trying to deserialize an XML document that I am also serializing at another time. I am using it to store a configuration file.
This is my Code:
namespace OrderTracker
{
[Serializable]
public class AutofillValues
{
private string fileName = Directory.GetCurrentDirectory() + "\\bin\\settings.db";
public ComboBox.ObjectCollection Vendors { get; set; }
public ComboBox.ObjectCollection Products { get; set; }
public ComboBox.ObjectCollection Companies { get; set; }
public void save(AutofillValues afv)
{
if (!File.Exists(fileName))
{
FileStream fs = File.Create(fileName);
fs.Close();
}
XmlSerializer x = new XmlSerializer(typeof(AutofillValues));
TextWriter writer = new StreamWriter(fileName);
x.Serialize(writer, afv);
writer.Close();
}
public AutofillValues load()
{
XmlSerializer x = new XmlSerializer(typeof(AutofillValues));
TextReader file = new StreamReader(fileName);
AutofillValues av = (AutofillValues)x.Deserialize(file);
file.Close();
return av;
}
}
}
The error message that I am getting when trying to deserialize the file is this;
An unhandled exception of type 'System.InvalidOperationException' occurred in System.Xml.dll
Additional information: There is an error in XML document (2, 2).*
This is the XML document:
<?xml version="1.0" encoding="utf-8"?>
<AutofillValues xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Vendors>
<anyType xsi:type="xsd:string">Test Vendor</anyType>
</Vendors>
<Products>
<anyType xsi:type="xsd:string">Test Product</anyType>
</Products>
<Companies>
<anyType xsi:type="xsd:string">Test Company</anyType>
</Companies>
</AutofillValues>
How can I deserialize the XML file and get back the serialized data?
I just changed this part and it worked for me.
You can not deserialize the XML back, because the class ComboBox.ObjectCollection does not have a standard (parameterless) constructor. This is a limitation of the XmlSerializer class, as stated in this SO post.
There is however another problem with your current code - even if the deserialization somehow works, than you still need to assign the collection to a ComboBox control, which the deserializer still can't do.
Instead of using the ComboBox.ObjectCollection class to store the items, I would suggest using either an array or a list of objects (as #kenlacoste suggested). Such collections can be easily inserted into the ComboBox using
the comboBox.Items.AddRange(arrayOfObjects) method.
Another refactoring would be to extract the serialization logic of the data class. Currently it is confusing to save and load the data, because I presume you want to save/fill the caller object:
save: object.save(object); - you can use the this keyword in the save method
load: object = object.load(); - same here, there is no need to return the value, use the this keyword to fill the existing properties
The changed code:
public class AutofillValues
{
private string fileName = #"d:\settings.db";
public object[] Vendors { get; set; }
public object[] Products { get; set; }
public object[] Companies { get; set; }
public void save()
{
XmlSerializer x = new XmlSerializer(typeof(AutofillValues));
// with using there is no need to close the writer explicitely
// second parameter - file is created if it does not exist
using (var writer = new StreamWriter(fileName, false))
{
x.Serialize(writer, this);
}
}
public void load()
{
XmlSerializer x = new XmlSerializer(typeof(AutofillValues));
AutofillValues av = (AutofillValues)x.Deserialize(new StreamReader(fileName));
this.Companies = av.Companies;
this.Vendors = av.Vendors;
this.Products = av.Products;
}
}
IMO the modified code is easier to read and understand:
var afv = new AutofillValues();
afv.load();
//use avf.Products
// or afv.save();
I would also suggest to extract the data that needs to be saved in an extra class, for example:
[Serializable]
public class AutofillValuesData
{
public Object[] Vendors { get; set; }
public Object[] Products { get; set; }
public Object[] Companies { get; set; }
}
In the class AutofillValues remove the three properties and leave just one:
public AutofillValuesData Data { get; set; }
Then the logic can be modified to fill the ComboBox controls from the filled data object. This way your data will not be hardwired to the UI and this would make the code more maintainable. You can use a helper like AutoMapper to remove the repetitive code (like mappig objA.Vendors to objB.Vendors).

DataContractSerializer not deserializing all variables

I'm trying to deserialize some xml without having the original class that was used to create the object in xml. The class is called ComOpcClientConfiguration.
It's succesfully setting the ServerUrl and ServerUrlHda values, but not rest of them...
So what I'm asking is: How can I make the rest of these values get set properly, and why aren't they working with my current code.
Here is my deserialization code:
conf is an XElement which represents the ComClientConfiguration xml
DataContractSerializer ser = new DataContractSerializer(typeof(ComClientConfiguration), new Type[] {typeof(ComClientConfiguration), typeof(ComOpcClientConfiguration) });
ComOpcClientConfiguration config = (ComOpcClientConfiguration)ser.ReadObject(conf.CreateReader());
I don't know why I have to have ComClientConfiguration and ComOpcClientConfiguration, there's probably a better way to do the known types hack I have. But for now it's what I have.
Here is the xml as it looks in the file.
<ComClientConfiguration xsi:type="ComOpcClientConfiguration" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ServerUrl>The url</ServerUrl>
<ServerName>a server name </ServerName>
<ServerNamespaceUrl>a namespace url</ServerNamespaceUrl>
<MaxReconnectWait>5000</MaxReconnectWait>
<MaxReconnectAttempts>0</MaxReconnectAttempts>
<FastBrowsing>true</FastBrowsing>
<ItemIdEqualToName>true</ItemIdEqualToName>
<ServerUrlHda>hda url</ServerUrlHda>
</ComClientConfiguration>
Here is the class I built to deserialize into:
[DataContract(Name = "ComClientConfiguration", Namespace = "http://opcfoundation.org/UA/SDK/COMInterop")]
public class ComClientConfiguration
{
public ComClientConfiguration() { }
//Prog-ID for DA-connection
[DataMember(Name = "ServerUrl"), OptionalField]
public string ServerUrl;//url
[DataMember(Name = "ServerName")]
public string ServerName;
[DataMember(Name = "ServerNamespaceUrl")]
public string ServerNamespaceUrl;//url
[DataMember(Name = "MaxReconnectWait")]
public int MaxReconnectWait;
[DataMember(Name = "MaxReconnectAttempts")]
public int MaxReconnectAttempts;
[DataMember(Name = "FastBrowsing")]
public bool FastBrowsing;
[DataMember(Name = "ItemIdEqualToName")]
public bool ItemIdEqualToName;
//ProgID for DA-connection
[DataMember, OptionalField]
public string ServerUrlHda;//url
}
I additionally had to make this class, it's the same but with a different name. Used for known types in the Serializer because I don't know exactly how the whole type naming stuff works.
[DataContract(Name = "ComOpcClientConfiguration", Namespace = "http://opcfoundation.org/UA/SDK/COMInterop")]
public class ComOpcClientConfiguration
{
public ComOpcClientConfiguration() { }
... Same innards as ComClientConfiguration
}
Data-contract-serializer is... Fussy. In particular, I wonder if simply element order is the problem here. However, it is also not necessarily the best tool for working with XML. XmlSerializer is probably a more robust here - it can handle a much better range of XML. DCS simply isn't intended with that as it's primary goal.
With simple XML you often don't even need any attributes etc. You can even use xsd.exe on your existing XML to generate the matching c# classes (in two steps; XML-to-xsd; xsd-to-c#).
To get all values, try hardcoding the order (otherwise maybe it tries the alphabetical order):
[DataMember(Name = "ServerUrl", Order = 0)]
..
[DataMember(Name = "ServerName", Order = 1)]
..
[DataMember(Name = "ServerNamespaceUrl", Order = 2)]
..
[DataMember(Name = "MaxReconnectWait", Order = 3)]
..

Categories

Resources