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.
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.