CoClass Serialization in C#

Posted in software by Christopher R. Wirz on Thu Oct 08 2015



Abstract classes and Interfaces serve different purposes, but neither allows for instantiation. This is the value of a CoClass - which allows a developer to create a new instance of an Interface. Due to the prerequisites of XmlSerializer, an interface cannot be serialized unless logic is added to get the CoClass class type.

The abstract keyword enables developers to create classes and class members that are incomplete and must be implemented in a derived class. When inheriting from an abstract class, implementation of methods on the base class have to be marked with override. Because an abstract class is still a class, its methods can have implementation and the use of the keyword "abstract" is optional on methods if the derived class is not required to provide implementation.

An interface contains definitions for a group of related functionalities that a class or a struct can implement. There is no implementation in an interface.

A CoClass provides an interface - such that a developer must implement all methods and properties in derivation, but also provides a default implementation. The following is an example of a CoClass declaration:


[Serializable]
public partial class Id : IId
{
	[XmlElement]
	public byte[] UUID { get; set; }

	[XmlIgnore]
	public string UUIDString
	{
		get
		{
			return Encoding.UTF8.GetString(UUID);
		}
		set
		{
			UUID = Encoding.UTF8.GetBytes(value);
		}
	}
}

[Guid("41F65AE4-E78F-45D7-B798-4176057004A8")]
[CoClass(typeof(Id))]
[ComImport]
public interface IId
{
	byte[] UUID { get; set; }
	string UUIDString { get; set; }
}

The following code is used to test instantiation of the [CoClass] interface:


class Program
{
	static int Main(string[] args)
	{
		// create a new IId object
		IId iid = new IId
		{
			UUIDString = "thisisad-emon-stra-tive-uuidfortests",
		};
		string serialized = iid.Serialize();
		File.WriteAllText("IId.xml", serialized);

		serialized = File.ReadAllText("IId.xml");
		IId Deserialized = iid.Deserialize(serialized);

		// should show "thisisad-emon-stra-tive-uuidfortests"
		Console.WriteLine(Deserialized.UUIDString);

		Console.ReadKey();
		return 0;
	}
}

While the code compiles, in the initial run it fails at the line


string serialized = iid.Serialize();

as the value of serialized is null or empty. An interface cannot be serialized! Since this is a CoClass and not a traditional interface, create an extension method and return the type into the XmlSerializer constructor. The CoClass type can be found in the interface's CustomAttributeData.


public static partial class ExtensionMethods
{
	/// <summary>
	///     Get the CoClass type if the type object is not a class
	/// </summary>
	/// <param name="type">The type to get the CoClass of</param>
	/// <returns>The type of the type object's CoClass</returns>
	public static Type CoClassType(this Type type)
	{
		// interfaces can't be serialized, but classes still can
		if (type.IsClass) return type; 

		// Get the CoClass attribute data
		IEnumerable<System.Reflection.CustomAttributeData> data 
			= type.CustomAttributes.Where( 
				c => 
					c.AttributeType.Name == "CoClassAttribute" &&
					c.ConstructorArguments !=null &&
					c.ConstructorArguments.Any(a => a.Value is Type)
			);

		foreach (System.Reflection.CustomAttributeData t in data)
		{
			// Return the CoClass type
			return (Type)t.ConstructorArguments.First(a => a.Value is Type).Value;
		}

		// If a CoClass type can't be returned, return the original type
		return type;
	}
	
	/// <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
		{
			var xmlSerializer = new XmlSerializer(typeof(T).CoClassType());

			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</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(typeof(T).CoClassType());
			using (var xmlReader = new StringReader(str))
			{
				returnValue = (T)xmlSerializer.Deserialize(xmlReader);
			}
		}
		catch (Exception ex)
		{
			System.Console.WriteLine(ex.Message);
		}
		return returnValue;
	}
}

With the Serialize and Deserialize methods updated to include the CoClassType extension method, running the test again successful. We now have the conveniance of a CoClass Interface with the Serializable nature of a concrete class type. The following is the IId.xml file from the example (indented for readability):


<?xml version="1.0" encoding="utf-16"?>
<Id xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
	<UUID>dGhpc2lzYWQtZW1vbi1zdHJhLXRpdmUtdXVpZGZvcnRlc3Rz</UUID>
</Id>

Notice that the [XmlIgnore] attribute prevented UUIDString from being serialized. We have done this because UUID is the backing property for UUIDString - so there is no need to serialize both. By default, a byte array is base64 encoded when serialized.