Polymorphic collection serialization in C#

Posted in software by Christopher R. Wirz on Wed Nov 04 2015



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.

Implementing IXmlSerializable allows for specific approaches for serialization to be defined - and is basically required when objects contain refernces to interfaces.

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.