Поделиться через


DataContract Serialization with Generics & Read-only Properties

Recently I have been messing around with figuring out how to serialize some data back & forth between two applications.  The code in particular leveraged several interfaces, classes with readonly properties, & generics.  This caused a couple problems with serialization which I thought might be useful to share for others that run into a similar problem.

Take for instance the following code:

static class Program
{
    public static void Main()
    {
    
        Class1 m = new Class1(new MyClass<string>("Test"));
        string xml = m.Serialize();
        Class1 m2 = Class1.Deserialize(xml);

        Debug.Assert(m.Value.ToString() == m2.Value.ToString());
    }
}

[Serializable]
public class MyClass<T> where T : IComparable
{
    readonly T item;

    public T Item { get { return item; } }

    public MyClass(T obj)
    {
        item = obj;
    }
}

[Serializable]
public class Class1
{
    public Class1(object val)
    {
        value = val;
    }
    
    public string Serialize()
    {
        XmlSerializer serializer = new XmlSerializer(typeof(Class1));
        StringWriter writer = new StringWriter();
        serializer.Serialize(writer, this);
        writer.Close();
        return writer.ToString();
    }

    public static Class1 Deserialize(string xml)
    {
        XmlSerializer serializer = new XmlSerializer(typeof(Class1));
        StringReader sr = new StringReader(xml);
        Class1 item = (Class1)serializer.Deserialize(sr);
        return item;
    }
    
    readonly object value;

    public object Value { get { return value; } }
}

 

Parameterless Constructor

If you run this through & try to serialize & then deserialize you will run into a problem just serializing this. The first error you will hit is:

System.InvalidOperationException was unhandled
Message=There was an error generating the XML document.
Source=System.Xml
StackTrace:
at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id)
at System.Xml.Serialization.XmlSerializer.Serialize(TextWriter textWriter, Object o, XmlSerializerNamespaces namespaces)
at System.Xml.Serialization.XmlSerializer.Serialize(TextWriter textWriter, Object o)
at MyNamespace.Class1.Serialize()
at MyNamespace.Program.Main()
InnerException: System.InvalidOperationException
Message=MyNamespace.MyClass`1[System.String] cannot be serialized because it does not have a parameterless constructor.
Source=System.Xml

Xml Serialization requires you to have a parameterless constructor, so you can either add one, or switch to using WCF DataContract Serialization. This is as simple as changing the attribute on your Classes from [Serializable] to [DataContract] and then updating your Serialize & Deserialize methods. This will allow you to successfully serialize & deserialize the objects without an exception.

[DataContract]
public class MyClass<T> where T : IComparable
{
    readonly T item;

    public T Item { get { return item; } }

    public MyClass(T obj)
    {
        item = obj;
    }
}

[DataContract]
public class Class1
{
    public Class1(object val)
    {
        value = val;
    }

    public string Serialize()
    {
        StringBuilder xml = new StringBuilder();
        DataContractSerializer serializer = new DataContractSerializer(typeof(Class1));
        using (XmlWriter xw = XmlWriter.Create(xml))
        {
            serializer.WriteObject(xw, this);
            xw.Flush();
            return xml.ToString();
        }
    }

    public static Class1 Deserialize(string xml)
    {
        Class1 newItem;
        DataContractSerializer serializer = new DataContractSerializer(typeof(Class1));
        StringReader textReader = new StringReader(xml);
        using (XmlReader xr = XmlReader.Create(textReader))
        {
            newItem = (Class1)serializer.ReadObject(xr);
        }
        return newItem;
    }
    
    readonly object value;

    public object Value { get { return value; } }
}

 

Serializing Read-only Properties

Next you will his the Debug.Assert which was a check to ensure we are serializing the read-only property was correctly being populated & in this case it is not.  The reason being is that XmlSerialization will automatically attempt to serialize all public properties, but WCD DataSerialization does not automatically attempt to serialize any properties.  So you need to decorate any item you want serialized with a [DataMember] tag.  This is great because now for readonly properties you can just mark the underlying private data as the DataMember.

[DataMember]
readonly object value;

public object Value { get { return value; } }

 

Serializing Generics

The next problem you will hit is a SerializationException about not being able to Serialize the generic MyClassOfString, aka MyClass<string>

System.Runtime.Serialization.SerializationException was unhandled
Message=Type 'MyNamespace.MyClass`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]' with data contract name 'MyClassOfstring:https://schemas.datacontract.org/2004/07/MyNamespace' is not expected. Consider using a DataContractResolver or add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.
Source=System.Runtime.Serialization

To resolve this you will need to use the KnownType attribute & there are a couple options:

  1. Generically implement serialization
  2. Explicitly list the known types

Generic KnownType

Generic serialization is nice as the serialize since you don’t have to decorate your code  with the various possible classes used for your generic.  To implement it you add the KnownTypes attribute to your class with a string of the method name to call.

[DataContract]
[KnownType("GetTypes")]
public class MyClass<T> where T : IComparable
{
    readonly T item;

    public T Item { get { return item; } }

    public MyClass(T obj)
    {
        item = obj;
    }

    static Type[] GetTypes()
    {
        return new Type[] { typeof(MyClass<T>) };
    }
}

Explicit KnownType

If you do the above then you will wind up with an exception during deserialization now since Class1 does not how to deserialize MyClassOfString. As the deserializer you will need to explictly state what types you expect.  In this case you can do it with a single attribute on the top.

[DataContract]
    [KnownType(typeof(MyClass<string>))]
    public class Class1

 

Final Results

At this point the class will successfully serialize & deserialize fully. Due to the nature of my example, the step of using the GetTypes method is not required since I am doing both serialization & deserialization in Class1, but it is useful when that is not the case.

static class Program
{
    public static void Main()
    {
    
        Class1 m = new Class1(new MyClass<string>("Test"));
        string xml = m.Serialize();
        Class1 m2 = Class1.Deserialize(xml);

        Debug.Assert(m.Value.ToString() == m2.Value.ToString());
    }
}

[DataContract]
[KnownType("GetTypes")]
public class MyClass<T> where T : IComparable
{
    readonly T item;

    public T Item { get { return item; } }

    public MyClass(T obj)
    {
        item = obj;
    }

    static Type[] GetTypes()
    {
        return new Type[] { typeof(MyClass<T>) };
    }
}

[DataContract]
[KnownType(typeof(MyClass<string>))]
public class Class1
{
    public Class1(object val)
    {
        value = val;
    }

    public string Serialize()
    {
        StringBuilder xml = new StringBuilder();
        DataContractSerializer serializer = new DataContractSerializer(typeof(Class1));
        using (XmlWriter xw = XmlWriter.Create(xml))
        {
            serializer.WriteObject(xw, this);
            xw.Flush();
            return xml.ToString();
        }
    }

    public static Class1 Deserialize(string xml)
    {
        Class1 newItem;
        DataContractSerializer serializer = new DataContractSerializer(typeof(Class1));
        StringReader textReader = new StringReader(xml);
        using (XmlReader xr = XmlReader.Create(textReader))
        {
            newItem = (Class1)serializer.ReadObject(xr);
        }
        return newItem;
    }
    
    [DataMember]
    readonly object value;

    public object Value { get { return value; } }
}