XmlSerializer offers a straight-forward way to serialize data objects using C#. However, in computer programming, SOLID (single responsibility, open-closed, Liskov substitution, interface segregation and dependency inversion) principles implies that one should code to interfaces, not concrete types. This poses an issue as Interfaces cannot be serialized. While interfaces cannot form serializable objects, a collection of objects may be defined by an interface which the objects implement. This provides a challenge in the serialization of these collections.
Take for example a generic serializer defined as follows:
public static partial class ExtensionMethods
{
/// <summary>
/// Serializes a generic object
/// </summary>
/// <typeparam name="T">The object type</typeparam>
/// <param name="obj">The object</param>
/// <returns>A string giving the serialized object</returns>
public static string Serialize<T>(this T obj)
{
string returnString = null;
try
{
if (obj == null) return returnString;
var xmlSerializer = new XmlSerializer(obj?.GetType() ?? typeof(T));
using (StringWriter sr = new StringWriter())
{
using (var textWriter = XmlWriter.Create(sr))
{
xmlSerializer.Serialize(textWriter, obj);
}
sr.Flush();
returnString = sr.ToString();
}
}
catch (Exception ex)
{
System.Console.WriteLine(ex.Message);
}
return returnString;
}
/// <summary>
/// Takes a string and returns an object
/// </summary>
/// <typeparam name="T">The object type</typeparam>
/// <param name="obj">The object</param>
/// <param name="str">The string to deserialize</param>
/// <returns>An object of type T</returns>
public static T Deserialize<T>(this T obj, string str)
{
T returnValue = default(T);
try
{
var xmlSerializer = new XmlSerializer(obj?.GetType() ?? typeof(T));
using (var xmlReader = new StringReader(str))
{
returnValue = (T)xmlSerializer.Deserialize(xmlReader);
}
}
catch (Exception ex)
{
System.Console.WriteLine(ex.Message);
}
return returnValue;
}
}
For this example, define a series of interfaces and implementations. An interface can implement another interface ("is a" relation) or contain a property or collection of another interface ("has a" relationship).
public interface IHasKey
{
int Key { get; set; }
}
// Because IId has a collection based on an interface,
// it must implement IXmlSerializable
public interface IId : IHasKey, IXmlSerializable
{
IHasKey[] Children { get; set; }
}
[Serializable]
public partial class IdBase : IHasKey
{
[XmlElement]
public int Key { get; set; }
}
[Serializable]
public partial class IdBaseXmlSerializable : IId
{
[XmlElement]
public int Key { get; set; }
[XmlElement]
public IHasKey[] Children { get; set; }
}
[Serializable]
public partial class IdDerived : IdBaseXmlSerializable
{
[XmlElement]
public int Salt { get; set; }
}
IdBaseXmlSerializable, the concrete class of IId, needs an implementation of IXmlSerializable.
partial class IdBaseXmlSerializable : IXmlSerializable
{
#region Implementation of IXmlSerializable
/// <summary>
/// Implementation of IXmlSerializable.GetSchema()
/// </summary>
/// <returns>Always return null</returns>
public virtual XmlSchema GetSchema()
{
return null;
}
/// <summary>
/// Implementation of IXmlSerializable.ReadXml
/// </summary>
/// <param name="writer">The XmlReader</param>
public virtual void ReadXml(XmlReader mainReader)
{
mainReader.MoveToContent(); // Always move to content
using (XmlReader reader = mainReader.ReadSubtree())
{
reader.MoveToContent(); // Always move to content
while (reader.Read())
{
// Example of reading Key element
if (reader.Name == nameof(Key) && reader.IsStartElement())
{
reader.Read(); // Advance to text
if (!string.IsNullOrEmpty(reader.Value))
{
Key = int.Parse(reader.Value.Trim());
}
}
// Example reading Children, which is a collection
if (reader.Name == nameof(Children) && reader.IsStartElement())
{
ReadChildren(reader);
}
}
}
}
/// <summary>
/// A method to read a collection of Children
/// </summary>
/// <param name="writer">The XmlReader</param>
private void ReadChildren(XmlReader reader)
{
List<IHasKey> children = new List<IHasKey>();
using (XmlReader subReader = reader.ReadSubtree())
{
subReader.MoveToContent(); // Always move to content
// Define temporary storage for content
List<Tuple<string, string>> contentStrings =
new List<Tuple<string, string>>();
// Go to the first element
subReader.Read();
// Start reading
string name = reader.Name;
string content = reader.ReadOuterXml(); // this advances the reader
// Load the content into the Tuple list
while (!string.IsNullOrEmpty(content))
{
contentStrings.Add(new Tuple<string, string>(name, content));
name = reader.Name;
content = reader.ReadOuterXml();
}
// Go through the content
foreach (Tuple<string, string> csTuple in contentStrings)
{
// Get the type by reflection
Type type = System.Reflection.Assembly.GetExecutingAssembly()
.GetTypes().FirstOrDefault(t => t.Name == csTuple.Item1);
if (type != null)
{
// Create a new instance of the type
var inst = Activator.CreateInstance(type);
if (inst != null && inst is IHasKey)
{
// Deserialize the type using XmlSerializer
var id = inst.Deserialize(csTuple.Item2);
// add the object
if (id != null) children.Add((IHasKey)id);
}
}
}
}
// Set the collection
Children = children.ToArray();
}
/// <summary>
/// Implementation of IXmlSerializable.WriteXml
/// </summary>
/// <param name="writer">The XmlWriter</param>
public virtual void WriteXml(XmlWriter writer)
{
// Example to write the Key property
writer.WriteElementString(nameof(Key), Key.ToString());
// Example to write the Children collection
if (Children != null && Children.Length > 0)
{
// Since Children is a collection, XmlSerializer won't make an element
writer.WriteStartElement(nameof(Children));
// Iterate through the collection
foreach (var id in Children)
{
// Get the serialization of the object
string serialized = id.Serialize();
if (!string.IsNullOrWhiteSpace(serialized))
{
// Write the node
using (TextReader t = new StringReader(serialized))
using (XmlReader r = XmlReader.Create(t))
{
r.ReadToFollowing(id.GetType().Name);
writer.WriteNode(r, true);
}
}
}
// Write the end of the colletion element
writer.WriteEndElement();
}
}
#endregion
}
IdDerived extends IdBaseXmlSerializable, so it will need to override the IXmlSerializable impementation for the additional fields.
public partial class IdDerived
{
#region Implementation of IXmlSerializable
public override void ReadXml(XmlReader reader)
{
reader.MoveToContent(); // note: does not advance XmlReader if at content
using (XmlReader subReader = reader.ReadSubtree())
{
subReader.MoveToContent();
string outerXml = subReader.ReadOuterXml();
// Run the base implementation first
using (XmlReader newMainReader =
XmlReader.Create(new StringReader(outerXml)))
{
base.ReadXml(newMainReader);
}
// Run the derived implementation
using (XmlReader mainReader = XmlReader.Create(new StringReader(outerXml)))
{
while (mainReader.Read())
{
if (mainReader.IsStartElement()
&& mainReader.Name == nameof(Salt))
{
// Perform logic for individual property types
mainReader.Read();
if (!string.IsNullOrEmpty(mainReader.Value))
{
Salt = int.Parse(mainReader.Value.Trim());
}
}
}
}
}
}
public override void WriteXml(XmlWriter writer)
{
base.WriteXml(writer);
writer.WriteElementString(nameof(Salt), Salt.ToString());
}
#endregion
}
Now we test the implementation.
class Program
{
static int Main(string[] args)
{
IdBaseXmlSerializable id = new IdBaseXmlSerializable
{
Key = 10,
Children = new IHasKey[] {
new IdBase
{
Key = 1,
},
new IdBaseXmlSerializable
{
Key = 3,
Children = new IHasKey[] {
new IdBase
{
Key = 31,
},
new IdDerived
{
Key = 32,
Salt = 37
},
}
},
new IdDerived
{
Key = 2,
Salt = 5
}
}
};
string serialized = id.Serialize();
File.WriteAllText("Id.xml", serialized.ToPrettyXml());
Console.WriteLine(serialized);
serialized = File.ReadAllText("Id.xml");
IId Deserialized = id.Deserialize(serialized);
// shows "10" is Deserialized
Console.WriteLine(Deserialized.Key.ToString());
Console.ReadKey();
return 0;
}
}
Opening Id.xml, the following contents are shown:
<?xml version="1.0" encoding="utf-16"?>
<IdBaseXmlSerializable>
<Key>10</Key>
<Children>
<IdBase xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Key>1</Key>
</IdBase>
<IdBaseXmlSerializable>
<Key>3</Key>
<Children>
<IdBase xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Key>31</Key>
</IdBase>
<IdDerived>
<Salt>37</Salt>
<Key>32</Key>
</IdDerived>
</Children>
</IdBaseXmlSerializable>
<IdDerived>
<Salt>5</Salt>
<Key>2</Key>
</IdDerived>
</Children>
</IdBaseXmlSerializable>
It is shown that the collections of polymorphic children are successfully serialized into XML. IdBase, which does not implement IXmlSerializable uses the default declarations of XmlSerializer. This does not have any effect on deserialization - which also succeeds.