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.
Related
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
I have the following xml:
<Applications>
<AccessibleApplication></AccessibleApplication>
<AccessibleApplication></AccessibleApplication>
<EligibleApplication></EligibleApplication>
<EligibleApplication></EligibleApplication>
</Applications>
Is there a way to deserialize this into a C# object so that the AccessibleApplications and EligibleApplications are two separate arrays? I tried the following but get an exception because "Applications" is used more than once.
[XmlArray("Applications")]
[XmlArrayItem("AccessibleApplication")]
public List<Application> AccessibleApplications { get; set; }
[XmlArray("Applications")]
[XmlArrayItem("EligibleApplication")]
public List<Application> EligibleApplications { get; set; }
The exception is:
The XML element 'Applications' from namespace '' is already present in the current scope. Use XML attributes to specify another XML name or namespace for the element.
Is this possible to do?
Thanks!
Edit: I forgot to mention that I do not want to have an "Applications" class just for the sake of providing a container object for the two arrays. I have several situations like this and I don't want the clutter of these classes whose only purpose is to split up two arrays of the same type.
I was hoping to be able to deserialize the two arrays into an outer object using some sort of tag like [XmlArrayItem="Application/AccessibleApplication"] without creating an "Applications" class.
I've found a fairly neat way to do this, first make a class like this:
using System.Xml.Serialization;
[XmlRoot]
public class Applications
{
[XmlElement]
public string[] AccessibleApplication;
[XmlElement]
public string[] EligibleApplication;
}
Notice how the elements are individual arrays. Now using this class (I had my XML in a separate file hence the XmlDocument class).
var doc = new XmlDocument();
doc.Load("../../Apps.xml");
var serializer = new XmlSerializer(typeof(Applications));
Applications result;
using (TextReader reader = new StringReader(doc.InnerXml))
{
result = (Applications)serializer.Deserialize(reader);
}
Now to prove this works you can write this all in to a console app and do a foreach to print all the values in your arrays, like so:
foreach (var app in result.AccessibleApplication)
{
Console.WriteLine(app);
}
foreach (var app in result.EligibleApplication)
{
Console.WriteLine(app);
}
You can use XmlElement attribute to deserialize to different lists:
public class Applications
{
[XmlElement("AccessibleApplication")]
public List<Application> AccessibleApplications { get; set; }
[XmlElement("EligibleApplication")]
public List<Application> EligibleApplications { get; set; }
}
public class Application
{
[XmlText]
public string Value { get; set; }
}
So for a sample XML:
<Applications>
<AccessibleApplication>xyz</AccessibleApplication>
<AccessibleApplication>abc</AccessibleApplication>
<EligibleApplication>def</EligibleApplication>
<EligibleApplication>zzz</EligibleApplication>
</Applications>
The following snippet would output the below:
using (var reader = new StreamReader("XMLFile1.xml"))
{
var serializer = new XmlSerializer(typeof(Applications));
var applications = (Applications)serializer.Deserialize(reader);
Console.WriteLine("AccessibleApplications:");
foreach (var app in applications.AccessibleApplications)
{
Console.WriteLine(app.Value);
}
Console.WriteLine();
Console.WriteLine("EligibleApplications:");
foreach (var app in applications.EligibleApplications)
{
Console.WriteLine(app.Value);
}
}
Output:
AccessibleApplications:
xyz
abc
EligibleApplications:
def
zzz
You can use this class to create objects from strings, creating strings from objects and create byte [] from objects
StringToObject
var applicationObject = new XmlSerializerHelper<Applications>().StringToObject(xmlString);
ObjectToString
var xmlString = new XmlSerializerHelper<Applications>().ObjectToString(applicationObject);
ObjectToByteArray
var byteArray = new XmlSerializerHelper<Applications>().ObjectToByteArray(applicationObject);
The XmlSerializerHelper:
namespace StackOverflow
{
public class XmlSerializerHelper<T> where T : class
{
private readonly XmlSerializer _serializer;
public XmlSerializerHelper()
{
_serializer = new XmlSerializer(typeof(T));
}
public T ToObject(string xml)
{
return (T)_serializer.Deserialize(new StringReader(xml));
}
public string ToString(T obj, string encoding)
{
using (var memoryStream = new MemoryStream())
{
_serializer.Serialize(memoryStream, obj);
return Encoding.GetEncoding(encoding).GetString(memoryStream.ToArray());
}
}
public byte[] ToByteArray(T obj, Encoding encoding = null)
{
var settings = GetSettings(encoding);
using (var memoryStream = new MemoryStream())
{
using (var writer = XmlWriter.Create(memoryStream, settings))
{
_serializer.Serialize(writer, obj);
}
return memoryStream.ToArray();
}
}
private XmlWriterSettings GetSettings(Encoding encoding)
{
return new XmlWriterSettings
{
Encoding = encoding ?? Encoding.GetEncoding("ISO-8859-1"),
Indent = true,
IndentChars = "\t",
NewLineChars = Environment.NewLine,
ConformanceLevel = ConformanceLevel.Document
};
}
}
}
Your Class:
[XmlRoot]
public class Applications
{
[XmlElement("AccessibleApplication")]
public string[] AccessibleApplication { get; set; }
[XmlElement("EligibleApplication")]
public string[] EligibleApplication { get; set; }
}
Or
[XmlRoot]
public class Applications
{
[XmlElement("AccessibleApplication")]
public List<string> AccessibleApplication { get; set; }
[XmlElement("EligibleApplication")]
public List<string> EligibleApplication { get; set; }
}
Cheers.
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);
}
}
I am creating an app that fetches fuel prices from a web service. I want to deserialise the prices a IDictionary of Price objects with the fuel as the key (similar to this).
I had created a setter to do this but have since found out that the serialisation uses the Add method rather than the setter for lists. Is there a way of doing this using the serialisation API or will I have to write custom serialisation code?
The XML looks like this
<?xml version="1.0"?>
<prices>
<price fuel="Petrol">152.43</price>
<price fuel="Diesel">147.53</price>
</prices>
The code looks like this
[XmlRoot("prices")]
public class FuelPrices
{
private IDictionary<Fuel, Price> prices = new Dictionary<Fuel, Price>();
// This is used for serialising to XML
[XmlElement("price")]
public ICollection<Price> Prices
{
get
{
return prices.Values;
}
set
{
prices = new Dictionary<Fuel, Price>();
foreach (Price price in value)
{
prices[price.Fuel] = price;
}
}
}
// These properties are used to access the prices in the code
[XmlIgnore]
public Price PetrolPrice
{
get
{
Price petrolPrice;
prices.TryGetValue(Fuel.Petrol, out petrolPrice);
return petrolPrice;
}
}
[XmlIgnore]
public Price DieselPrice
{
get
{
Price dieselPrice;
prices.TryGetValue(Fuel.Diesel, out dieselPrice);
return dieselPrice;
}
}
}
You can write a wrapper around a dictionary along the lines of
sealed class DictionaryWrapper<K, T> : ICollection<T>
{
private readonly Func<T, K> m_keyProjection ;
private readonly IDictionary<K, T> m_dictionary ;
// expose the wrapped dictionary
public IDictionary<K, T> Dictionary { get { return m_dictionary ; }}
public void Add (T value)
{
m_dictionary[m_keyProjection (value)] = value ;
}
public IEnumerator<T> GetEnumerator ()
{
return m_dictionary.Values.GetEnumerator () ;
}
// the rest is left as excercise for the reader
}
and use it like this
private DictionaryWrapper<Fuel, Price> pricesWrapper =
new DictionaryWrapper<Fuel, Price> (
new Dictionary<Fuel, Price> (), price => price.Fuel) ;
[XmlElement("price")]
public ICollection<Price> Prices
{
get { return pricesWrapper ; } // NB: no setter is necessary
}
if you do not want to write custom serialization - you can do this:
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
namespace ConsoleApplication2
{
public class Program
{
[XmlType("price")]
public class Price
{
[XmlText]
public double price { get; set; }
[XmlAttribute("fuel")]
public string fuel { get; set; }
}
[XmlType("prices")]
public class PriceList : List<Price>
{
}
static void Main(string[] args)
{
//Serialize
var plist = new PriceList()
{
new Price {price = 153.9, fuel = "Diesel"},
new Price {price = 120.6, fuel = "Petrol"}
};
var serializer = new XmlSerializer(typeof(PriceList));
var sw = new StringWriter();
var ns = new XmlSerializerNamespaces();
ns.Add("", "");
serializer.Serialize(sw, plist, ns);
var result = sw.ToString();//result xml as we like
//Deserialize
var sr = new StringReader(result);
var templist = (PriceList)serializer.Deserialize(sr);
var myDictionary = templist.ToDictionary(item => item.fuel, item => item.price);
}
}
}
and if you need custom serialization - look at this post:Why isn't there an XML-serializable dictionary in .NET?
In the example code below, I get this error:
Element
TestSerializeDictionary123.Customer.CustomProperties
vom Typ
System.Collections.Generic.Dictionary`2[[System.String,
mscorlib, Version=2.0.0.0,
Culture=neutral,
PublicKeyToken=b77a5c561934e089],[System.Object,
mscorlib, Version=2.0.0.0,
Culture=neutral,
PublicKeyToken=b77a5c561934e089]] can
not be serialized because it
implements IDictionary.
When I take out the Dictionary property, it works fine.
How can I serialize this Customer object with the dictionary property? Or what replacement type for Dictionary can I use that would be serializable?
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
using System.IO;
using System.Xml;
using System.Text;
namespace TestSerializeDictionary123
{
public class Program
{
static void Main(string[] args)
{
List<Customer> customers = Customer.GetCustomers();
Console.WriteLine("--- Serializing ------------------");
foreach (var customer in customers)
{
Console.WriteLine("Serializing " + customer.GetFullName() + "...");
string xml = XmlHelpers.SerializeObject<Customer>(customer);
Console.WriteLine(xml);
Console.WriteLine("Deserializing ...");
Customer customer2 = XmlHelpers.DeserializeObject<Customer>(xml);
Console.WriteLine(customer2.GetFullName());
Console.WriteLine("---");
}
Console.ReadLine();
}
}
public static class StringHelpers
{
public static String UTF8ByteArrayToString(Byte[] characters)
{
UTF8Encoding encoding = new UTF8Encoding();
String constructedString = encoding.GetString(characters);
return (constructedString);
}
public static Byte[] StringToUTF8ByteArray(String pXmlString)
{
UTF8Encoding encoding = new UTF8Encoding();
Byte[] byteArray = encoding.GetBytes(pXmlString);
return byteArray;
}
}
public static class XmlHelpers
{
public static string SerializeObject<T>(object o)
{
MemoryStream ms = new MemoryStream();
XmlSerializer xs = new XmlSerializer(typeof(T));
XmlTextWriter xtw = new XmlTextWriter(ms, Encoding.UTF8);
xs.Serialize(xtw, o);
ms = (MemoryStream)xtw.BaseStream;
return StringHelpers.UTF8ByteArrayToString(ms.ToArray());
}
public static T DeserializeObject<T>(string xml)
{
XmlSerializer xs = new XmlSerializer(typeof(T));
MemoryStream ms = new MemoryStream(StringHelpers.StringToUTF8ByteArray(xml));
XmlTextWriter xtw = new XmlTextWriter(ms, Encoding.UTF8);
return (T)xs.Deserialize(ms);
}
}
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Street { get; set; }
public string Location { get; set; }
public string ZipCode { get; set; }
public Dictionary<string,object> CustomProperties { get; set; }
private int internalValue = 23;
public static List<Customer> GetCustomers()
{
List<Customer> customers = new List<Customer>();
customers.Add(new Customer { Id = 1, FirstName = "Jim", LastName = "Jones", ZipCode = "23434" });
customers.Add(new Customer { Id = 2, FirstName = "Joe", LastName = "Adams", ZipCode = "12312" });
customers.Add(new Customer { Id = 3, FirstName = "Jack", LastName = "Johnson", ZipCode = "23111" });
customers.Add(new Customer { Id = 4, FirstName = "Angie", LastName = "Reckar", ZipCode = "54343" });
customers.Add(new Customer { Id = 5, FirstName = "Henry", LastName = "Anderson", ZipCode = "16623" });
return customers;
}
public string GetFullName()
{
return FirstName + " " + LastName + "(" + internalValue + ")";
}
}
}
In our application we ended up using:
DataContractSerializer xs = new DataContractSerializer(typeof (T));
instead of:
XmlSerializer xs = new XmlSerializer(typeof (T));
which solved the problem as DatacontractSerializer supports Dictionary.
Another solution is ths XML Serializable Generic Dictionary workaround also works in the above example, and there is a long discussion at that link from people using it, might be useful for people working with this issue.
Here's a generic dictionary class that knows how to serialize itself:
public class XmlDictionary<T, V> : Dictionary<T, V>, IXmlSerializable {
[XmlType("Entry")]
public struct Entry {
public Entry(T key, V value) : this() { Key = key; Value = value; }
[XmlElement("Key")]
public T Key { get; set; }
[XmlElement("Value")]
public V Value { get; set; }
}
System.Xml.Schema.XmlSchema IXmlSerializable.GetSchema() {
return null;
}
void IXmlSerializable.ReadXml(System.Xml.XmlReader reader) {
this.Clear();
var serializer = new XmlSerializer(typeof(List<Entry>));
reader.Read(); // Why is this necessary?
var list = (List<Entry>)serializer.Deserialize(reader);
foreach (var entry in list) this.Add(entry.Key, entry.Value);
reader.ReadEndElement();
}
void IXmlSerializable.WriteXml(System.Xml.XmlWriter writer) {
var list = new List<Entry>(this.Count);
foreach (var entry in this) list.Add(new Entry(entry.Key, entry.Value));
XmlSerializer serializer = new XmlSerializer(list.GetType());
serializer.Serialize(writer, list);
}
}
You can't (short of doing it all yourself, which is horrible); the xml serializer isn't going to have a clue what to do with object, as it doesn't include type metadata in the wire format. One (hacky) option would be to stream these all as strings for the purposes of serialization, but then you have a lot of extra parsing (etc) code to write.
You can use Binary serialization instead. (Just make sure all your classes are marked as [Serializable]. Of course, it won't be in XML format, but you didn't list that as a requirement :)
I've just found this blog post by Rakesh Rajan which describes one possible solution:
Override XmlSerialization by making the type implement the System.Xml.Serialization.IXmlSerializable class. Define how you want the object to be serialized in XML in the WriteXml method, and define how you could recreate the object from an xml string in the ReadXml method.
But this wouldn't work as your Dictionary contains an object rather than a specific type.
What about to mark Customer class as DataContract and its properties as DataMembers. DataContract serializer will do the serialization for you.
Try Serializating through BinaryFormatter
private void Deserialize()
{
try
{
var f_fileStream = File.OpenRead(#"dictionarySerialized.xml");
var f_binaryFormatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
myDictionary = (Dictionary<string, myClass>)f_binaryFormatter.Deserialize(f_fileStream);
f_fileStream.Close();
}
catch (Exception ex)
{
;
}
}
private void Serialize()
{
try
{
var f_fileStream = new FileStream(#"dictionarySerialized.xml", FileMode.Create, FileAccess.Write);
var f_binaryFormatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
f_binaryFormatter.Serialize(f_fileStream, myDictionary);
f_fileStream.Close();
}
catch (Exception ex)
{
;
}
}