Data Contract type Detection in C#

Posted in software by Christopher R. Wirz on Thu Feb 18 2016



Serialization is important for any service oriented application that wishes to persist state in a form independent of architecture, programming language, or process. When adopting other people's code, however, different approaches are often taken to these data transfer objects - specifically when it comes to C# .Net. In this case, we still want one serialization library that can detect these paradigms and produce the same results.

Note: The reason for using XML serialization over JSON is polymorphism. There is more support for it, natively, in XML.

First, let's specify everyhing to be UTF8.


/// <summary>
///     A class only to override encoding with UTF8.
/// </summary>
public class Utf8StringWriter : StringWriter
{
	/// <summary>
	///     Specify the encoding type
	/// </summary>
	public override Encoding Encoding => Encoding.UTF8;
}

Now we create a series of extension methods to perform serialization.


public static partial class SerializationExtensionMethods
{

	/// <summary>
	///     Checks if the class type is a data contract
	/// </summary>
	/// <typeparam name="T">The type of object</typeparam>
	/// <param name="t">The object to check for data contract status</param>
	/// <returns>True if it has a DataContract attribute</returns>
	public static bool IsDataContract<T>(this T t) where T : class
	{
		try
		{
			Type ty = t as Type ?? typeof(T);
			foreach (var a in ty.CustomAttributes)
			{
				if (a.AttributeType.Name == "DataContractAttribute") { return true; }
			}
		} catch { }
		return false;
	}

	/// <summary>
	///		Checks if a field is a collection
	/// </summary>
	/// <param name="field">The field</param>
	/// <returns>True if the field is a collection</returns>
	public static bool IsACollection(this System.Reflection.FieldInfo field)
	{
		if (field == null) { return false; }
		return !typeof(string).Equals(field.FieldType) &&
			typeof(System.Collections.IEnumerable).IsAssignableFrom(field.FieldType);
	}

	/// <summary>
	///		Checks if a property is a collection
	/// </summary>
	/// <param name="property">The property</param>
	/// <returns>True if the property is a collection</returns>
	public static bool IsACollection(this System.Reflection.PropertyInfo property)
	{
		if (property == null) { return false; }
		return (!typeof(String).Equals(property.PropertyType) &&
			typeof(System.Collections.IEnumerable).IsAssignableFrom(property.PropertyType));
	}

	/// <summary>
	///		Checks if a member is a collection
	/// </summary>
	/// <param name="member">The member</param>
	/// <returns>True if the member is a collection</returns>
	public static bool IsACollection(this System.Reflection.MemberInfo member)
	{
		if (member == null) { return false; }
		if ((member.Name ?? "").EndsWith("[]", StringComparison.InvariantCultureIgnoreCase)) { return true; }
		if (member is System.Reflection.PropertyInfo)
		{
			return ((System.Reflection.PropertyInfo)member).IsACollection();
		}
		if (member is System.Reflection.FieldInfo)
		{
			return ((System.Reflection.FieldInfo)member).IsACollection();
		}
		return false;
	}

	/// <summary>
	///		Checks if a type has a parameterless constructor
	/// </summary>
	/// <param name="t">The type</param>
	/// <returns>True if it has a parameterless constructor</returns>
	public static bool HasParameterlessConstructor(this Type t) => t.GetConstructors().Any(c => c.GetParameters().Length == 0);

	/// <summary>
	///     Gets all the types known by the object
	/// </summary>
	/// <param name="obj">An objects</param>
	/// <param name="known">A collection of already known objects</param>
	/// <returns>A collection of types</returns>
	public static Type[] GetAllTypes(this object obj, System.Collections.Generic.List<Type> known = null)
	{
		known = known ?? new System.Collections.Generic.List<Type>();
		if (obj is null) { return known.ToArray(); }

		var type = obj.GetType();
		if (!type.HasParameterlessConstructor()) { return known.ToArray(); }
		if (type.IsValueType) { return known.ToArray(); }

		if (known.Contains(type)) { return known.ToArray(); }
		known.Add(type);

		var properties = type.GetProperties().Where(p => p.CanWrite && p.CanRead);
		foreach (var p in properties)
		{
			if (p.PropertyType.IsValueType) { continue; }
			if (p.PropertyType.Name.ToLowerInvariant().Equals("string")) { continue; }

			var o = p.GetValue(obj);
			if (o is null) { continue; }
			var t = o.GetType();
			if (t.IsACollection())
			{
				Array array = o as Array;
				if (array == null) { continue; }

				for (int i = 0; i < array.Length; i++)
				{
					object ob = array.GetValue(i);
					if (ob is null) { continue; }
					known.AddRange(ob.GetAllTypes(known));
				}
			}
			else
			{
				if (known.Contains(t)) { continue; }
				if (!t.HasParameterlessConstructor()) { continue; }
				known.Add(t);
				known.AddRange(o.GetAllTypes(known));
			}
		}
		return known.Distinct().ToArray();
	}

	/// <summary>
	///     Serializes an object based on a known type
	/// </summary>
	/// <param name="t">The type</param>
	/// <param name="obj">The object to serialize</param>
	/// <returns>A string representation of the object</returns>
	public static string Serialize(this Type t, object obj)
	{
		string returnString = null;
		if (obj == null) return returnString;
		try
		{
			// Look pretty and use UTF-8
			var settings = new XmlWriterSettings
			{
				Indent = true,
				NewLineOnAttributes = true,
				Encoding = Encoding.UTF8
			};

			if (t.IsDataContract())
			{
				DataContractSerializer ser =
				new DataContractSerializer(t);
				using (StringWriter sw = new Utf8StringWriter())
				{
					using (var textWriter = XmlWriter.Create(sw, settings))
					{
						ser.WriteObject(textWriter, obj);
					}
					sw.Flush();
					returnString = sw.ToString();
				}
			}
			else
			{
				var types = obj.GetAllTypes();
				var xmlSerializer = new XmlSerializer(obj?.GetType() ?? t, types);
				using (StringWriter sw = new Utf8StringWriter())
				{
					using (var textWriter = XmlWriter.Create(sw, settings))
					{
						xmlSerializer.Serialize(textWriter, obj);
					}
					sw.Flush();
					returnString = sw.ToString();
				}
			}
		}
		catch (Exception ex)
		{
			System.Console.WriteLine(ex.Message);
		}
		return returnString;
	}

	/// <summary>
	///     Serializes an object to a string
	/// </summary>
	/// <typeparam name="T">The object type</typeparam>
	/// <param name="obj">The object to serialize</param>
	/// <returns>The string representation</returns>
	public static string Serialize<T>(this object obj)
	{
		if (obj == null) { return string.Empty; }
		return typeof(T).Serialize(obj);
	}

	/// <summary>
	///     Serializes an object to a string
	/// </summary>
	/// <param name="obj">The object to serialize</param>
	/// <returns>The string representation</returns>
	public static string Serialize(this object obj)
	{
		if (obj == null) { return string.Empty; }
		return obj.GetType().Serialize(obj);
	}

	/// <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) where T : class
	{
		return typeof(T).Serialize(obj);
	}




	/// <summary>
	///     Turns a string into an object of a type
	/// </summary>
	/// <param name="ty">The type</param>
	/// <param name="str">The string</param>
	/// <returns>An object of a given type</returns>
	public static object Deserialize(this Type ty, string str)
	{
		if (string.IsNullOrWhiteSpace(str)) { return null; }
		object returnValue = null;
		try
		{
			if (ty.IsDataContract())
			{
				DataContractSerializer ser = new DataContractSerializer(ty);
				using (var xmlReader = XmlReader.Create(new StringReader(str)))
				{
					returnValue = ser.ReadObject(xmlReader);
				}
			}
			else
			{
				var xmlSerializer = new XmlSerializer(ty);
				using (var xmlReader = new StringReader(str))
				{
					returnValue = xmlSerializer.Deserialize(xmlReader);
				}
			}
		}
		catch (Exception ex)
		{
			System.Console.WriteLine(ex.Message);
		}
		return returnValue;
	}

	/// <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 string str) where T : class
	{
		if (string.IsNullOrWhiteSpace(str)) { return null; }
		return typeof(T).Deserialize(str) as T;
	}

	/// <summary>
	///     Turns a string into an object
	///     (least specific case)
	/// </summary>
	/// <param name="str">The string</param>
	/// <returns>The returned object</returns>
	public static object Deserialize(this string str)
	{
		try
		{
			if (string.IsNullOrWhiteSpace(str)) { return null; }
			string name = null;
			var ele = XElement.Parse(str);
			var names = ele.Attributes().Where(a => a.Name.LocalName == "type").ToArray();
			if (names.Count() > 0) { name = names.First().Value; }
			else { name = ele?.Name?.LocalName; }
			if (name == null) { return null; }

			var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes().Where(t => t.Name == name));
			foreach (Type t in types){
				try
				{
					var o = t.Deserialize(str);
					if (o != null) { return o; }
				} catch { }
			}
		}
		catch (Exception ex){ Console.WriteLine(ex.Message); }

		return null;
	}
}

Now that we have serialization methods, we can write some code to test.


// Note that KnownType2 is not added as an attribute
[DataContract]
[KnownType(typeof(KnownType1))]
[XmlInclude(typeof(KnownType1))]
public class BaseType
{
	[DataMember]
	public string Id { get; set; }
}

[DataContract]
public class KnownType1 : BaseType
{
	[DataMember]
	public string K1Id { get; set; }
}

[DataContract]
public class KnownType2 : BaseType
{
	[DataMember]
	public string K2Id { get; set; }
}

static void Main(string[] args)
{
	object k1 = new KnownType1()
	{
		Id = "Id1",
		K1Id = "K1Id",
	};

	object k2 = new KnownType2()
	{
		Id = "Id2",
		K2Id = "K2Id",
	};

	var s1 = k1.Serialize<BaseType>();
	var b1 = s1.Deserialize();
	Console.WriteLine(s1 + Environment.NewLine);
	Console.WriteLine(b1.Serialize() + Environment.NewLine);


	var s2 = k2.Serialize();
	var b2 = s2.Deserialize();
	Console.WriteLine(s2 + Environment.NewLine);
	Console.WriteLine(b2.Serialize() + Environment.NewLine);

	Console.ReadKey();
}

Let's see the results.


<?xml version="1.0" encoding="utf-8"?>
<BaseType xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
  i:type="KnownType1" xmlns="http://schemas.datacontract.org/2004/07/TestProject">
  <Id>Id1</Id>
  <K1Id>K1Id</K1Id>
</BaseType>

<?xml version="1.0" encoding="utf-8"?>
<KnownType1 xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/TestProject">
  <Id>Id1</Id>
  <K1Id>K1Id</K1Id>
</KnownType1>

<?xml version="1.0" encoding="utf-8"?>
<KnownType2 xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/TestProject">
  <Id>Id2</Id>
  <K2Id>K2Id</K2Id>
</KnownType2>

<?xml version="1.0" encoding="utf-8"?>
<KnownType2 xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/TestProject">
  <Id>Id2</Id>
  <K2Id>K2Id</K2Id>
</KnownType2>

All types have been successfully serialized and de-serialized, with polymorphic relationships preserved.