XmlSerialization with XmlAnyElement to XmlNode and Order property - c#

I'm facing an issue with the .Net XmlSerializer, basically I need one or more elements with same name to be serialized (and deserialized) dynamically between other elements of fixed schema.
Example:
<?xml version="1.0" encoding="utf-8"?>
<A xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<asd>asd</asd>
<nnn>q</nnn>
<nnn>w</nnn>
<nnn>e</nnn>
<aaa>aaa</aaa>
</A>
My real <nnn> tag is a little bit more complicated, with dynamic tags inside (not only conditionals), but I'm currently threating this right.
I really need to use the "Order" parameter of XmlElement to control some rules.
I can't change the XML layout.
The example serializable class:
[XmlRoot]
[Serializable]
public class A
{
[XmlElement("asd", Order=1)]
public string asd { get; set; }
[XmlIgnore]
public string[] qwe { get; set; }
[XmlAnyElement("nnn", Order=2)]
public XmlNode[] nnn
{
get
{
if (qwe == null) return null;
var xml = new XmlDocument();
var nodes = new List<XmlNode>(qwe.Length);
foreach (var q in qwe)
{
var nnnTag = xml.CreateNode(XmlNodeType.Element, "nnn", null);
nnnTag.InnerText = q;
nodes.Add(nnnTag);
}
return nodes.ToArray();
}
set
{
if (value == null) return;
qwe = value.Select(tag => tag.InnerText).ToArray();
}
}
[XmlElement("aaa", Order=3)]
public string aaa { get; set; }
The problem is, when not using the "Order" parameter the serialization goes fine, but with the parameter the elements after the XmlAnyElement are understood as part of the array of nodes and so are not deserialized right.
My main program for the example is a Console Application with the following main:
static void Main(string[] args)
{
var a = new A
{
aaa = "aaa",
asd = "asd",
qwe = new[] {"q", "w", "e"}
};
var s = Serialize(a);
var ss = Deserialize<A>(s);
var s2 = Serialize(ss);
Console.WriteLine(s);
Console.WriteLine(s2);
Console.WriteLine("Equals: {0};", s == s2);
Console.ReadKey();
}
The wrong output is:
<?xml version="1.0" encoding="utf-8"?>
<A xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<asd>asd</asd>
<nnn>q</nnn>
<nnn>w</nnn>
<nnn>e</nnn>
<aaa>aaa</aaa>
</A>
<?xml version="1.0" encoding="utf-8"?>
<A xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<asd>asd</asd>
<nnn>q</nnn>
<nnn>w</nnn>
<nnn>e</nnn>
<nnn>aaa</nnn>
</A>
Equals: False;
For testing, here is the Serialization/Deserialization snippets I'm using:
public static string Serialize<T>(T a)
{
var s = new XmlSerializer(typeof(T));
using (var ms = new MemoryStream())
{
using (TextWriter sw = new StreamWriter(ms))
{
s.Serialize(sw, a);
ms.Seek(0, 0);
using (var sr = new StreamReader(ms))
{
return sr.ReadToEnd();
}
}
}
}
public static T Deserialize<T>(string a)
{
var s = new XmlSerializer(typeof(T));
var bytes = Encoding.ASCII.GetBytes(a);
using (var ms = new MemoryStream(bytes))
{
return (T) s.Deserialize(ms);
}
}
The full source code:
https://gist.github.com/inventti-gabriel/81054269f2e0a32d7e8d1dd44f30a97f
Thanks in advance.

I agree with #CharlesMager that this does appear to be a bug in XmlSerializer.
That being said, you can work around the bug by introducing an intermediate wrapper class or struct to contain the arbitrary nodes, then modifying your surrogate nnn property to return an array of these structs after marking the property with [XmlElement("nnn", Order = 2)]:
[XmlRoot]
[Serializable]
public class A
{
[XmlElement("asd", Order = 1)]
public string asd { get; set; }
[XmlIgnore]
public string[] qwe { get; set; }
[XmlElement("nnn", Order = 2)]
public XmlNodeWrapper [] nnn
{
get
{
if (qwe == null)
return null;
var xml = new XmlDocument();
var nodes = new List<XmlNode>(qwe.Length);
foreach (var q in qwe)
{
var nnnTag = xml.CreateNode(XmlNodeType.Element, "nnn", null);
nnnTag.InnerText = q;
nodes.Add(nnnTag);
}
return nodes.Select(n => (XmlNodeWrapper)n.ChildNodes).ToArray();
}
set
{
if (value == null)
return;
qwe = value.Select(tag => tag.InnerText()).ToArray();
}
}
[XmlElement("aaa", Order = 3)]
public string aaa { get; set; }
}
[XmlType(AnonymousType = true)]
public struct XmlNodeWrapper
{
public static implicit operator XmlNodeWrapper(XmlNodeList nodes)
{
return new XmlNodeWrapper { Nodes = nodes == null ? null : nodes.Cast<XmlNode>().ToArray() };
}
public static implicit operator XmlNode[](XmlNodeWrapper wrapper)
{
return wrapper.Nodes;
}
// Marking the Nodes property with both [XmlAnyElement] and [XmlText] indicates that the node array
// may contain mixed content (I.e. both XmlElement and XmlText objects).
// Hat tip: https://stackoverflow.com/questions/25995609/xmlserializer-node-containing-text-xml-text
[XmlAnyElement]
[XmlText]
public XmlNode[] Nodes { get; set; }
public string InnerText()
{
if (Nodes == null)
return null;
return String.Concat(Nodes.Select(n => n.InnerText));
}
}
Note that the Nodes property in XmlNodeWrapper is marked with both [XmlAnyElement] and [XmlText]. The requirement to do this is explained here.
Sample fiddle.

Try using XmlArrayAttribute in place of XmlAnyElement as the nodes are array

Related

C# Object Serialization to XML with ignoring null element [duplicate]

This question already has answers here:
How do I stop an empty tag from being emitted by XmlSerializer?
(4 answers)
Closed 4 years ago.
[XmlRoot("SAPInformationInterchangeXML")]
public class EWayBillResponseXML
{
[XmlElement(ElementName = "SAPBusinessNetworkCustomerID")]
public string SAPBusinessNetworkCustomerID { get; set; }
[XmlElement(ElementName = "INVOIC")]
public ResponseINVOIC Invoice { get; set; }
}
public class ResponseINVOIC
{
[XmlElement(ElementName = "HeaderInformation")]
public string HeaderInformation { get; set; }
[XmlElement(ElementName = "AuthorizationInformation")]
public string AuthorizationInformation { get; set; }
}
var encoding = Encoding.GetEncoding("ISO-8859-1");
XmlWriterSettings xmlWriterSettings = new XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = false,
Encoding = encoding
};
string requestHeaderInformation = null, requestAuthorizationInformation = null;
EWayBillResponseXML obj = new EWayBillResponseXML
{
SAPBusinessNetworkCustomerID = "1",
Invoice = new ResponseINVOIC
{
HeaderInformation = requestHeaderInformation,
AuthorizationInformation = requestAuthorizationInformation
}
};
System.Xml.Serialization.XmlSerializer x = new System.Xml.Serialization.XmlSerializer(obj.GetType());
using (var stream = new MemoryStream())
{
using (var xmlWriter = XmlWriter.Create(stream, xmlWriterSettings))
{
x.Serialize(xmlWriter, obj);
}
Console.WriteLine(encoding.GetString(stream.ToArray()));
}
I have created 2 objects named EWayBillResponseXML and ResponseINVOIC. I tried to serialize using above code snippet. It gives me serialized XML but it returns null object element too. I don't need null object in serialized XML. Can you Please help me out.
Currently getting output:
<?xml version="1.0" encoding="iso-8859-1"?>
<SAPInformationInterchangeXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SAPBusinessNetworkCustomerID>1</SAPBusinessNetworkCustomerID>
<INVOIC />
</SAPInformationInterchangeXML>
Expected output:
<?xml version="1.0" encoding="iso-8859-1"?>
<SAPInformationInterchangeXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SAPBusinessNetworkCustomerID>1</SAPBusinessNetworkCustomerID>
</SAPInformationInterchangeXML>
You can add the following method to your serializable class (EWayBillResponseXML):
public bool ShouldSerializeInvoice()
{
return Invoice != null;
}
You can read more about that here.
As #gmiley said, you should make method named like this ShouldSerialize___ where ___ is the name of property that should or not be serialized. That method needs to be in class where is said property
So, in your case, make ShouldSerializeResponseINVOIC method in your EWayBillResponseXML class
[XmlRoot("SAPInformationInterchangeXML")]
public class EWayBillResponseXML
{
[XmlElement(ElementName = "SAPBusinessNetworkCustomerID")]
public string SAPBusinessNetworkCustomerID { get; set; }
[XmlElement(ElementName = "INVOIC")]
public ResponseINVOIC Invoice { get; set; }
public book ShouldSerializeInvoice()
{
return Invoice != null;
}
}
Edit:
Dbc correctly said in comment that Invoice is not actually null but empty, so you should handle that.

xml creation in C#

I want final xml as follows...
<Programs>
<Program><id>4</id><isRead>true</isRead><isWrite>false</isWrite></Program>
<Program><id>8</id><isRead>true</isRead><isWrite>true</isWrite></Program>
</programs>
now following is the code written
XmlDocument xmlDoc = new XmlDocument();
XmlNode rootNode = xmlDoc.CreateNode(XmlNodeType.Element,"programs",null);
xmlDoc.AppendChild(rootNode);
foreach (dynamic item in access)
{
XmlNode myXmlNode = JsonConvert.DeserializeXmlNode(item.ToString(), "program");
rootNode.AppendChild(myXmlNode); //error
}
where in myXmlNode.InnerXml, I am already getting following
<Program><id>4</id><isRead>true</isRead><isWrite>false</isWrite></Program>
And so, running the loop for all children to add in which is parent. But I am getting error for line //error as marked above. Error is:
The specified node cannot be inserted as the valid child of this node,
because the specified node is the wrong type.
You are trying to insert different type of xml node. You could use ImportNode to apply it.
foreach (dynamic item in access)
{
XmlNode myXmlNode = JsonConvert.DeserializeXmlNode(item.ToString(), "program");
rootNode.AppendChild(rootNode.OwnerDocument.ImportNode(myXmlNode.FirstChild,true));
}
I would recommend you create a model for what you are looking to create in XML, then serialize a list of the model to XML.
The reason being that you will end up with something far more maintainable.
Your model would be something like this:
public class Program
{
public int Id { get; set; }
public bool IsRead { get; set; }
public bool IsWrite { get; set; }
}
And you can serialize it by following this article:
https://support.microsoft.com/en-gb/help/815813/how-to-serialize-an-object-to-xml-by-using-visual-c
I've used the XmlSerializer library myself a few times.
public class Program
{
public int Id { get; set; }
public bool IsRead { get; set; }
public bool IsWrite { get; set; }
}
public class XmlTemplate
{
public List<Program> Programs { get; set; }
}
class MainProgram
{
static void Main(string[] args)
{
var xmlTemplate = new XmlTemplate();
xmlTemplate.Programs = new List<Program>();
xmlTemplate.Programs.Add(new Program() { Id = 123, IsRead = true, IsWrite = false });
xmlTemplate.Programs.Add(new Program() { Id = 456, IsRead = false, IsWrite = true });
var serializer = new XmlSerializer(typeof(XmlTemplate));
var stream = new StringWriter();
serializer.Serialize(stream, xmlTemplate);
Console.WriteLine(stream.ToString());
Console.ReadKey();
}
}
This outputs the XML in the following format:
<XmlTemplate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Programs>
<Program>
<Id>123</Id>
<IsRead>true</IsRead>
<IsWrite>false</IsWrite>
</Program>
<Program>
<Id>456</Id>
<IsRead>false</IsRead>
<IsWrite>true</IsWrite>
</Program>
</Programs>
</XmlTemplate>

Deserialize XML with XmlSerializer where XmlElement names differ but have same content

I would like to deserialize an XML File to a class with several subclasses. The XML looks like this:
<?xml version="1.0" encoding="utf-8"?>
<Objects>
<Group index="1">
<de>
<GroupName>ANTRIEB</GroupName>
</de>
<en>
<GroupName>missing translation!</GroupName>
</en>
<Level>2</Level>
</Group>
<Group index="2">
<de>
<GroupName>BREMSEN</GroupName>
</de>
<Level>3</Level>
</Group>
</Objects>
Deserializing the XML to classes would be no problem, if there wouldn't be those language tags. Sure, I could create a property for every language tag possible. But the list of languages possible should be dynamic (e.g. read from an config file).
This is the reason why i would like to deserialize those language tags and their content into a Dictionary which uses the language as key and a model for the content.
My models look like this:
[XmlRoot("Objects")]
public class DeactivationsXml
{
[XmlElement("Group")]
public DeactivationsGroup[] Groups { get; set; }
}
[Serializable()]
public class DeactivationsGroup
{
[XmlIgnore]
public Dictionary<string, GroupName> GroupNames { get; set; } = new Dictionary<string, GroupName>();
public int Level { get; set; }
[XmlAttribute]
public byte index { get; set; }
}
public class GroupName
{
[XmlElement("GroupName")]
public string Name { get; set; }
}
I searched for a long time to address this problem, but couldn't find a solution. I'm pretty sure, that it's not possible to solve this Problem just with attributes.
Does some hybrid aproach exist in order to combine the Deserialization of an XML File in combination with manual deserialization of all XmlElements which could not be automatically deserialized?
A good and extensible solution for my problem would be great, because the XML structure is complex (same Problem several times with different content etc.).
I can't change the structure of the XML, so please don't point this out.
Approaches
IXmlSerializable
I tried to implement the IXmlSerializable Interface on the DeactivationsGroup class in order to search with a list of given languages for XmlElements with those names and deserialize the content of those XmlElements.
But this approach didn't work out, because you have to map all properties manually.
IExtensibleDataObject
The Interface is only supported by a DataContractSerializer. In the worst case i could use this interface to deserialize after Deserializing, if no other solution is found..
OnDeserialization
This Attribute is not supported by XmlSerializer, but would provide the functionality i possibly need.
XmlAnyElement
I guess this is the best option at this point. Does some callback exist after deserialization finished in order to automate this?
Executable Code
Here's the whole code so far.
public void Parse()
{
string xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
" <Objects>" +
" <Group index=\"1\">" +
" <de>" +
" <GroupName>ANTRIEB</GroupName>" +
" </de>" +
" <en>" +
" <GroupName>missing translation!</GroupName>" +
" </en>" +
" <Level>2</Level>" +
" </Group>" +
" <Group index=\"2\">" +
" <de>" +
" <GroupName>BREMSEN</GroupName>" +
" </de>" +
" <Level>3</Level>" +
" </Group>" +
" </Objects>";
XmlSerializer serializer = new XmlSerializer(typeof(DeactivationsXml));
using (TextReader fileStream = new StringReader(xml))
{
var result = (DeactivationsXml)serializer.Deserialize(fileStream);
}
}
[XmlRoot("Objects")]
public class DeactivationsXml
{
[XmlElement("Group")]
public DeactivationsGroup[] Groups { get; set; }
}
[Serializable()]
public class DeactivationsGroup
{
[XmlIgnore]
public Dictionary<string, GroupName> GroupNames { get; set; } = new Dictionary<string, GroupName>();
public int Level { get; set; }
[XmlAttribute]
public byte index { get; set; }
}
public class GroupName
{
[XmlElement("GroupName")]
public string Name { get; set; }
}
You can adopt the approach from this answer and add a surrogate XmlElement [] property, marked with [XmlAnyElement], that performs a nested (de)serialization on the key/value pairs of the Dictionary<string, GroupName> property, binding the dictionary keys to the element names.
Note that, while the documentation for XmlAnyElementAttribute states
Specifies that the member (a field that returns an array of XmlElement or XmlNode objects) contains objects that represent any XML element that has no corresponding member in the object being serialized or deserialized.
In fact the attribute can be applied to a property as well. Thus a (de)serialization callback is not required since the nested serialization can be performed inside the getter and setter for the surrogate property itself. It can also be applied to members returning an array of XElement objects instead of XmlElement if you prefer the new LINQ-to-XML API.
In this approach, your DeactivationsGroup would look like:
[Serializable()]
public class DeactivationsGroup
{
public DeactivationsGroup() { this.GroupNames = new Dictionary<string, GroupName>(); }
[XmlIgnore]
public Dictionary<string, GroupName> GroupNames { get; set; }
public int Level { get; set; }
[XmlAttribute]
public byte index { get; set; }
[XmlAnyElement]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
public XElement[] XmlGroupNames
{
get
{
return GroupNames.SerializeToXElements(null);
}
set
{
if (value == null || value.Length < 1)
return;
foreach (var pair in value.DeserializeFromXElements<GroupName>())
{
GroupNames.Add(pair.Key, pair.Value);
}
}
}
}
Making use of the following extension methods and classes:
public static class XmlKeyValueListHelper
{
const string RootLocalName = "Root";
public static XElement [] SerializeToXElements<T>(this IEnumerable<KeyValuePair<string, T>> dictionary, XNamespace ns)
{
if (dictionary == null)
return null;
ns = ns ?? "";
var serializer = XmlSerializerFactory.Create(typeof(T), RootLocalName, ns.NamespaceName);
var array = dictionary
.Select(p => new { p.Key, Value = p.Value.SerializeToXElement(serializer, true) })
// Fix name and remove redundant xmlns= attributes. XmlWriter will add them back if needed.
.Select(p => new XElement(ns + p.Key, p.Value.Attributes().Where(a => !a.IsNamespaceDeclaration), p.Value.Elements()))
.ToArray();
return array;
}
public static IEnumerable<KeyValuePair<string, T>> DeserializeFromXElements<T>(this IEnumerable<XElement> elements)
{
if (elements == null)
yield break;
XmlSerializer serializer = null;
XNamespace ns = null;
foreach (var element in elements)
{
if (serializer == null || element.Name.Namespace != ns)
{
ns = element.Name.Namespace;
serializer = XmlSerializerFactory.Create(typeof(T), RootLocalName, ns.NamespaceName);
}
var elementToDeserialize = new XElement(ns + RootLocalName, element.Attributes(), element.Elements());
yield return new KeyValuePair<string, T>(element.Name.LocalName, elementToDeserialize.Deserialize<T>(serializer));
}
}
public static XmlSerializerNamespaces NoStandardXmlNamespaces()
{
var ns = new XmlSerializerNamespaces();
ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
return ns;
}
public static XElement SerializeToXElement<T>(this T obj)
{
return obj.SerializeToXElement(null, NoStandardXmlNamespaces());
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializerNamespaces ns)
{
return obj.SerializeToXElement(null, ns);
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, bool omitStandardNamespaces)
{
return obj.SerializeToXElement(serializer, (omitStandardNamespaces ? NoStandardXmlNamespaces() : null));
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
{
var doc = new XDocument();
using (var writer = doc.CreateWriter())
(serializer ?? new XmlSerializer(obj.GetType())).Serialize(writer, obj, ns);
var element = doc.Root;
if (element != null)
element.Remove();
return element;
}
public static T Deserialize<T>(this XContainer element, XmlSerializer serializer)
{
using (var reader = element.CreateReader())
{
object result = (serializer ?? new XmlSerializer(typeof(T))).Deserialize(reader);
return (T)result;
}
}
}
public static class XmlSerializerFactory
{
// To avoid a memory leak the serializer must be cached.
// https://stackoverflow.com/questions/23897145/memory-leak-using-streamreader-and-xmlserializer
// This factory taken from
// https://stackoverflow.com/questions/34128757/wrap-properties-with-cdata-section-xml-serialization-c-sharp/34138648#34138648
readonly static Dictionary<Tuple<Type, string, string>, XmlSerializer> cache;
readonly static object padlock;
static XmlSerializerFactory()
{
padlock = new object();
cache = new Dictionary<Tuple<Type, string, string>, XmlSerializer>();
}
public static XmlSerializer Create(Type serializedType, string rootName, string rootNamespace)
{
if (serializedType == null)
throw new ArgumentNullException();
if (rootName == null && rootNamespace == null)
return new XmlSerializer(serializedType);
lock (padlock)
{
XmlSerializer serializer;
var key = Tuple.Create(serializedType, rootName, rootNamespace);
if (!cache.TryGetValue(key, out serializer))
cache[key] = serializer = new XmlSerializer(serializedType, new XmlRootAttribute { ElementName = rootName, Namespace = rootNamespace });
return serializer;
}
}
}
Sample fiddle. And another demonstrating a case with XML namespaces and attributes.
Try following 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);
DeactivationsGroup.GroupNames = doc.Descendants("Group").Select(x => new {
languages = x.Elements().Where(y => y.Element("GroupName") != null).Select(y => new DeactivationsGroup() {
name = (string)y.Element("GroupName"),
level = (int)x.Element("Level"),
index = byte.Parse((string)x.Attribute("index")),
language = y.Name.LocalName
})
}).SelectMany(y => y.languages)
.GroupBy(x => x.name, y => y)
.ToDictionary(x => x.Key, y => y.FirstOrDefault());
}
public class DeactivationsGroup
{
public static Dictionary<string, DeactivationsGroup> GroupNames { get; set; }
public string name { get; set; }
public int level { get; set; }
public byte index { get; set; }
public string language { get; set; }
}
}
}
This is easily done as follows.
Use your set of classes.
Set event handler on the serializer:
var serializer = new XmlSerializer(typeof(DeactivationsXml));
serializer.UnknownElement += Serializer_UnknownElement;
The code in this handler is very simple:
private void Serializer_UnknownElement(object sender, XmlElementEventArgs e)
{
var group = (DeactivationsGroup)e.ObjectBeingDeserialized;
group.GroupNames.Add(e.Element.Name, new GroupName { Name = e.Element.InnerText });
}
Fiddle.

Need Parse XML to List<Object>, name field are separated from data

I am implementing a windows service and i need to consume a WebService with REST, I want to parse this to List but i dont know how.
My problem is that the names of the fields are separated from the data.
The structure I get is this:
<?xml version="1.0" encoding="utf-8" ?>
<xml>
<result>OK</result>
<headers>
<header>lastname</header>
<header>firstname</header>
<header>Age</header>
</headers>
<data>
<datum>
<item>Kelly</item>
<item>Grace</item>
<item>33</item>
</datum>
</data>
</xml>
You can use XmlSerializer to deserialize that XML into c# classes that reflect its structure. For instance:
[XmlRoot("xml")] // Indicates that the root element is named "xml"
public class XmlResponse
{
[XmlElement("result")] // Indicates that this element is named "result"
public string Result { get; set; }
[XmlArray("headers")] // Indicates two-level list with outer element named "headers" and inner elements named "header"
[XmlArrayItem("header")]
public List<string> Headers { get; set; }
[XmlArray("data")] // Indicates two-level list with outer element named "data" and inner elements named "datum"
[XmlArrayItem("datum")]
public List<XmlResponseDatum> Data { get; set; }
}
public class XmlResponseDatum
{
[XmlElement("item")] // Indicates a one-level list with repeated elements named "item".
public List<string> Items { get; set; }
}
Which you could deserialize like:
public static T LoadFromXML<T>(string xmlString)
{
using (StringReader reader = new StringReader(xmlString))
{
object result = new XmlSerializer(typeof(T)).Deserialize(reader);
if (result is T)
{
return (T)result;
}
}
return default(T);
}
public static string GetXml<T>(T obj)
{
using (var textWriter = new StringWriter())
{
var settings = new XmlWriterSettings() { Indent = true, IndentChars = " " }; // For cosmetic purposes.
using (var xmlWriter = XmlWriter.Create(textWriter, settings))
new XmlSerializer(obj.GetType()).Serialize(xmlWriter, obj);
return textWriter.ToString();
}
}
public static void Test()
{
string xml = #"<?xml version=""1.0"" encoding=""utf-8"" ?>
<xml>
<result>OK</result>
<headers>
<header>lastname</header>
<header>firstname</header>
<header>Age</header>
</headers>
<data>
<datum>
<item>Kelly</item>
<item>Grace</item>
<item>33</item>
</datum>
</data>
</xml>";
var response = LoadFromXML<XmlResponse>(xml);
Debug.WriteLine(GetXml(response));
As an alternative, here is some XDocument and using objects that do not need to be Xml decorated.
I advocate this approach because it is easier to tweak the XDocument/Xpath statements in case the structure of the Xml changes.....as opposed to the XmlAttributes.
This also allows the same objects to be used, even if there are different xml streams hydrating it. You just write a different XDocument "shredder" for each Xml Input.
[DebuggerDisplay("MyNotUsedStringKey = {MyNotUsedStringKey}")]
public class ImportParent
{
public ImportParent()
{
this.MyNotUsedStringKey = Guid.NewGuid().ToString("N");
this.ImportChildren = new ImportChildCollection();
}
public ImportChildCollection ImportChildren { get; set; }
public string MyNotUsedStringKey { get; set; }
}
public class ImportChildCollection : List<ImportChild>
{
public ImportChildCollection() { }
public ImportChildCollection(IEnumerable<ImportChild> src)
{
if (null != src)
{
foreach (ImportChild item in src)
{
item.Ordinal = this.Count + 1;
base.Add(item);
}
}
//AddRange(src);
}
}
[DebuggerDisplay("MyStringKey = {MyStringKey}, MyStringValue='{MyStringValue}', Ordinal='{Ordinal}'")]
public class ImportChild
{
public ImportChild()
{
}
public int Ordinal { get; set; }
public string MyStringKey { get; set; }
public string MyStringValue { get; set; }
}
string xmlString = #"<?xml version=""1.0"" encoding=""utf-8"" ?>
<xml>
<result>OK</result>
<headers>
<header>lastname</header>
<header>firstname</header>
<header>Age</header>
</headers>
<data>
<datum>
<item>Kelly</item>
<item>Grace</item>
<item>33</item>
</datum>
</data>
</xml> ";
XDocument xDoc = XDocument.Parse(xmlString);
//XNamespace ns = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003");
string ns = string.Empty;
List<ImportParent> parentKeys = new List<ImportParent>
(
from list in xDoc.Descendants(ns + "xml")
from item1 in list.Elements(ns + "headers")
where item1 != null
select new ImportParent
{
//note that the cast is simpler to write than the null check in your code
//http://msdn.microsoft.com/en-us/library/bb387049.aspx
ImportChildren = new ImportChildCollection
(
from detail in item1.Descendants("header")
select new ImportChild
{
MyStringKey = detail == null ? string.Empty : detail.Value
}
)
}
);
List<ImportParent> parentValues = new List<ImportParent>
(
from list in xDoc.Descendants(ns + "xml")
from item1 in list.Elements(ns + "data")
from item2 in item1.Elements(ns + "datum")
where item1 != null && item2 != null
select new ImportParent
{
//note that the cast is simpler to write than the null check in your code
//http://msdn.microsoft.com/en-us/library/bb387049.aspx
ImportChildren = new ImportChildCollection
(
from detail in item1.Descendants("item")
select new ImportChild
{
MyStringValue = detail == null ? string.Empty : detail.Value
}
)
}
);
/*Match up the Keys to the Values using "Ordinal" matches*/
foreach (ImportParent parent in parentKeys)
{
foreach (ImportChild child in parent.ImportChildren)
{
ImportChild foundMatch = parentValues.SelectMany(x => x.ImportChildren).Where(c => c.Ordinal == child.Ordinal).FirstOrDefault();
if (null != foundMatch)
{
child.MyStringValue = foundMatch.MyStringValue;
}
}
}
foreach (ImportParent parent in parentKeys)
{
foreach (ImportChild child in parent.ImportChildren)
{
Console.WriteLine("Key={0}, Value={1}", child.MyStringKey, child.MyStringValue);
}
}

Set values of a class based on XML strucutre

I want to process certain elements of an XML string into an object based on the properties in the object matching up to the names of elements in the XML.
An example structure of the XML is as follows:
<Bar>
<Body>
<Header>
<A>Value</A>
<B>true</B>
</Header>
<Data>
<D>Value</D>
</Data>
<Data>
<D>Value2</D>
<Data>
</Body>
</Bar>
There can be MANY <Data> elements in the XML, however <Header> only exists once. The class I have set up is as so:
public class Foo
{
public string A { get; set; }
public bool B { get; set; }
public List<FooData> { get; set; }
public void ProcessXml(string xml)
{
XDocument xDoc = XDocument.Load(new StringReader(xml));
var propVals = (from ele in xDoc.Descendants()
join prop in this.GetType().GetProperties() on ele.Name.LocalName equals prop.Name
select new
{
prop = prop,
val = new Func<object>(() =>
{
object objValue = null;
prop.PropertyType.TryParse(ele.Value, ref objValue);
return objValue;
}).Invoke()
});
propVals.ToList().ForEach(x => x.prop.SetValue(this, x.val, null));
}
}
public class FooData
{
public string D { get; set; }
}
I came up with the method ProcessXml which starts to set things up, however at the minute I am only ever setting Header values (A, B), any ideas on how I can add many FooData items into the List from within the same method easily?
public static class TypeExtensions
{
public static void TryParse(this Type t, object valIn, ref object valOut)
{
//Do some parsing logic
try{
out = TypeDescriptor.GetConverter(t).ConvertFromInvariantString(valIn);
return true;
} catch(Exception) { return false; }
}
}
I went along a similar line as what I did with the header stuff as there isn't an easy way to combine this into one line.
var dataElements = (from dataDescendants in (from ele2 in xDoc.Descendants()
Where ele2.Name.LocalName == "Data"
select ele2.Descendants())
from dataDescendant in dataDescendants
join prop in typeof(FooItem).GetProperties() on prop.Name equals dataDescendant.Name.LocalName
select Element = dataDescendant, Property = prop, dataDescendants
group by dataDescendants into g = group
select g).ToList();
dataElements.ForEach(dataElement =>
{
FooItem fi = new FooItem();
dataElement.ToList.ForEach(x =>
{
object objVal = null;
x.Property.PropertyType.TryParse(x.Element.Value, objVal);
x.Property.SetValue(fi, objVal, null);
}
DataItems.Add(fi);
}
If your XML is really that simple, you can use Serialization to get this dynamic/self loading behavior, by decorating your model classes with the appropriate XML Serialization attributes though you will need at least 1 class for each level of indentation in the document:
void Main()
{
var xml = #"<Bar>
<Body>
<Header>
<A>Value</A>
<B>true</B>
</Header>
<Data>
<D>Value</D>
</Data>
<Data>
<D>Value2</D>
</Data>
</Body>
</Bar>";
var serializer = new XmlSerializer(typeof(Bar));
serializer.Deserialize( new MemoryStream( Encoding.ASCII.GetBytes( xml ) ) ).Dump();
}
public class Bar
{
public Body Body { get; set; }
}
public class Body
{
public Header Header { get; set; }
[XmlElement]
public Data[] Data { get; set; }
}
public class Header
{
public string A { get; set; }
public string B { get; set; }
}
public class Data
{
public string D { get; set; }
}
(Edited: I missed that there's only 1 Header element)

Categories