Search code examples
c#constructordeserializationxmlserializerdatacontract

Pass list of known types to object with its own XmlSerializer in (de-)serialization tree


I use C# 3 on microsoft .net 3.5 (VS2008). I have a problem with de-serialization. I use DataContract and DataMember in a hierarchy of classes that I want to be serializable.

However, I also have polymorphism in one container, so I need to pass a list of known types to the serializers. My collection is a serializable dictionary that I found on the net:

[Serializable]
[XmlRoot("dictionary")]
public class SerializableSortedDictionary<TKey, TVal>
    : SortedDictionary<TKey, TVal>, IXmlSerializable
{
    #region Constants
    private const string DictionaryNodeName = "Dictionary";
    private const string ItemNodeName = "Item";
    private const string KeyNodeName = "Key";
    private const string ValueNodeName = "Value";
    #endregion

    #region Constructors
    public SerializableSortedDictionary()
    {
    }

    public SerializableSortedDictionary(IDictionary<TKey, TVal> dictionary)
    : base(dictionary)
    {
    }

    public SerializableSortedDictionary(IComparer<TKey> comparer)
    : base(comparer)
    {
    }

    public SerializableSortedDictionary(IDictionary<TKey, TVal> dictionary, IComparer<TKey> comparer)
    : base(dictionary, comparer)
    {
    }

    #endregion

    #region IXmlSerializable Members

    void IXmlSerializable.WriteXml(System.Xml.XmlWriter writer)
    {
        //writer.WriteStartElement(DictionaryNodeName);
        foreach (KeyValuePair<TKey, TVal> kvp in this)
        {
            writer.WriteStartElement(ItemNodeName);
            writer.WriteStartElement(KeyNodeName);
            KeySerializer.Serialize(writer, kvp.Key);
            writer.WriteEndElement();
            writer.WriteStartElement(ValueNodeName);
            ValueSerializer.Serialize(writer, kvp.Value);
            writer.WriteEndElement();
            writer.WriteEndElement();
        }
        //writer.WriteEndElement();
    }

    void IXmlSerializable.ReadXml(System.Xml.XmlReader reader)
    {
        if (reader.IsEmptyElement)
        {
            return;
        }

        // Move past container
        if (!reader.Read())
        {
            throw new XmlException("Error in Deserialization of Dictionary");
        }

        //reader.ReadStartElement(DictionaryNodeName);
        while (reader.NodeType != XmlNodeType.EndElement)
        {
            reader.ReadStartElement(ItemNodeName);
            reader.ReadStartElement(KeyNodeName);
            TKey key = (TKey)KeySerializer.Deserialize(reader);
            reader.ReadEndElement();
            reader.ReadStartElement(ValueNodeName);
            TVal value = (TVal)ValueSerializer.Deserialize(reader);
            reader.ReadEndElement();
            reader.ReadEndElement();
            this.Add(key, value);
            reader.MoveToContent();
        }
        //reader.ReadEndElement();

        reader.ReadEndElement(); // Read End Element to close Read of containing node
    }

    System.Xml.Schema.XmlSchema IXmlSerializable.GetSchema()
    {
        return null;
    }

    // for serialization/deserialization pruporses
    public void SetKnownTypes(Type[] extraTypes)
    {
        this.extraTypes = extraTypes;
    }

    public Type[] extraTypes = null;

    #endregion

    #region Private Properties
    protected XmlSerializer ValueSerializer
    {
        get
        {
            if (valueSerializer == null)
            {
                if (extraTypes == null)
                    valueSerializer = new XmlSerializer(typeof(TVal));
                else
                    valueSerializer = new XmlSerializer(typeof(TVal), extraTypes);
            }
            return valueSerializer;
        }
    }

    private XmlSerializer KeySerializer
    {
        get
        {
            if (keySerializer == null)
            {
                if (extraTypes == null)
                    keySerializer = new XmlSerializer(typeof(TKey));
                else
                    keySerializer = new XmlSerializer(typeof(TKey), extraTypes);
            }
            return keySerializer;
        }
    }
    #endregion
    #region Private Members
    [NonSerialized]
    private XmlSerializer keySerializer = null;
    [NonSerialized]
    private XmlSerializer valueSerializer = null;
    #endregion
}

This is the one that holds a polymorphic object tree in its TVal. So you see I have modified the original code to add a list of known types, which works well for serialization, because I set this list in my superior classes constructors. (the classes that holds the dictionary instance).

This list of known types happens to be discovered at runtime, using this function:

    static public class TypeDiscoverer
    {
        public enum EFilter { All, OnlyConcreteTypes }
        public enum EAssemblyRange { AllAppDomain, OnlyAssemblyOfRequestedType }

        public static List<Type> FindAllDerivedTypes<T>(EFilter typesFilter, EAssemblyRange assembRange)
        {
            HashSet< Type > founds = new HashSet<Type>();

            Assembly[] searchDomain =
                assembRange == EAssemblyRange.OnlyAssemblyOfRequestedType ?
                new Assembly[1] { Assembly.GetAssembly(typeof(T)) }
                : AppDomain.CurrentDomain.GetAssemblies();

            foreach (Assembly a in searchDomain)
            {
                founds = new HashSet<Type>(founds.Concat(FindAllDerivedTypes<T>(a, typesFilter)));
            }
            return founds.ToList();
        }

        public static List<Type> FindAllDerivedTypes<T>(Assembly assembly, EFilter typesFilter)
        {
            var derivedType = typeof(T);

            List<Type> result = assembly
                                .GetTypes()
                                .Where(t =>
                                       t != derivedType &&
                                       derivedType.IsAssignableFrom(t)
                                      ).ToList();

            if (typesFilter == EFilter.OnlyConcreteTypes)
                result = result.Where(x => !x.IsAbstract).ToList();
            return result;
        }
    }

This dynamic system allows me to discover the known types by just knowing the base class. Which is something I always wondered why do the framework does not provide this feature... but well..

So my issue is that, my serializable dictionary, is an utility class, I can not specialize it to hardcode the list of known types, even less so because it is discovered at run time. Deserialization works on uninitialized object, and therefore I can not provide the list of known types to the dictionary de-serializer.

Of course, for the moment, I will workaround that problem by discovering the list of known types using my FindAllDerivedTypes functions on TVal directly in the dictionary.

But as it is less scalable than an exeternally-provided type list, I'd like to know if anyone can provide me with a real fix.

thanks a lot.


Solution

  • You can use custom XmlReader (or pass the types in some static / thread-local-storage variable). IdeOne example

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.IO;
    using System.Xml;
    using System.Xml.Serialization;
    
    namespace DynaXmlSer {
    
        public class KnownTypesXmlReader: XmlTextReader {
            public KnownTypesXmlReader(Stream ios): base(ios) {}
            public Type[] ExtraTypes = null;
        }
    
        public partial class SerializableSortedDictionary<TKey, TVal>
            : SortedDictionary<TKey, TVal>, IXmlSerializable
        {
            public void SetKnownTypes(Type[] extraTypes) {
                this.extraTypes = extraTypes;
                valueSerializer = null;
                keySerializer = null;
            }
            void IXmlSerializable.ReadXml(System.Xml.XmlReader reader) {
                if (reader.IsEmptyElement)
                    return;
                if (!reader.Read())
                    throw new XmlException("Error in Deserialization of Dictionary");
    
                //HERE IS THE TRICK
                if (reader is KnownTypesXmlReader)
                    SetKnownTypes(((KnownTypesXmlReader)reader).ExtraTypes);
    
                //reader.ReadStartElement(DictionaryNodeName);
                while (reader.NodeType != XmlNodeType.EndElement)
                {
                    reader.ReadStartElement(ItemNodeName);
                    reader.ReadStartElement(KeyNodeName);
                    TKey key = (TKey)KeySerializer.Deserialize(reader);
                    reader.ReadEndElement();
                    reader.ReadStartElement(ValueNodeName);
                    TVal value = (TVal)ValueSerializer.Deserialize(reader);
                    reader.ReadEndElement();
                    reader.ReadEndElement();
                    this.Add(key, value);
                    reader.MoveToContent();
                }
                //reader.ReadEndElement();
    
                reader.ReadEndElement(); // Read End Element to close Read of containing node
            }
    
        }
    
        public class BasicElement {
            private string name;
            public string Name {
                get { return name; }
                set { name = value; } }
        }
        public class ElementOne: BasicElement {
            private string one;
            public string One {
                get { return one; }
                set { one = value; }
            }
        }
        public class ElementTwo: BasicElement {
            private string two;
            public string Two {
                get { return two; }
                set { two = value; }
            }
        }
    
        public class Program {
            static void Main(string[] args) {
                Type[] extraTypes = new Type[] { typeof(ElementOne), typeof(ElementTwo) };
                SerializableSortedDictionary<string, BasicElement> dict = new SerializableSortedDictionary<string,BasicElement>();
                dict.SetKnownTypes(extraTypes);
                dict["foo"] = new ElementOne() { Name = "foo", One = "FOO" };
                dict["bar"] = new ElementTwo() { Name = "bar", Two = "BAR" };
    
                XmlSerializer ser = new XmlSerializer(typeof(SerializableSortedDictionary<string, BasicElement>));
    
                MemoryStream mem = new MemoryStream();
                ser.Serialize(mem, dict);
                Console.WriteLine(Encoding.UTF8.GetString(mem.ToArray()));
                mem.Position = 0;
    
                using(XmlReader rd = new KnownTypesXmlReader(mem) { ExtraTypes = extraTypes })
                    dict = (SerializableSortedDictionary<string, BasicElement>)ser.Deserialize(rd);
    
                foreach(KeyValuePair<string, BasicElement> e in dict) {
                    Console.Write("Key = {0}, Name = {1}", e.Key, e.Value.Name);
                    if(e.Value is ElementOne) Console.Write(", One = {0}", ((ElementOne)e.Value).One);
                    else if(e.Value is ElementTwo) Console.Write(", Two = {0}", ((ElementTwo)e.Value).Two);
                    Console.WriteLine(", Type = {0}", e.Value.GetType().Name);
                }
            }
        }
    
        [Serializable]
        [XmlRoot("dictionary")]
        public partial class SerializableSortedDictionary<TKey, TVal>
            : SortedDictionary<TKey, TVal>, IXmlSerializable
        {
            #region Constants
            private const string DictionaryNodeName = "Dictionary";
            private const string ItemNodeName = "Item";
            private const string KeyNodeName = "Key";
            private const string ValueNodeName = "Value";
            #endregion
    
            #region Constructors
            public SerializableSortedDictionary()
            {
            }
    
            public SerializableSortedDictionary(IDictionary<TKey, TVal> dictionary)
            : base(dictionary)
            {
            }
    
            public SerializableSortedDictionary(IComparer<TKey> comparer)
            : base(comparer)
            {
            }
    
            public SerializableSortedDictionary(IDictionary<TKey, TVal> dictionary, IComparer<TKey> comparer)
            : base(dictionary, comparer)
            {
            }
    
            #endregion
    
            #region IXmlSerializable Members
    
            void IXmlSerializable.WriteXml(System.Xml.XmlWriter writer)
            {
                //writer.WriteStartElement(DictionaryNodeName);
                foreach (KeyValuePair<TKey, TVal> kvp in this)
                {
                    writer.WriteStartElement(ItemNodeName);
                    writer.WriteStartElement(KeyNodeName);
                    KeySerializer.Serialize(writer, kvp.Key);
                    writer.WriteEndElement();
                    writer.WriteStartElement(ValueNodeName);
                    ValueSerializer.Serialize(writer, kvp.Value);
                    writer.WriteEndElement();
                    writer.WriteEndElement();
                }
                //writer.WriteEndElement();
            }
    
            System.Xml.Schema.XmlSchema IXmlSerializable.GetSchema()
            {
                return null;
            }
    
    
            public Type[] extraTypes = null;
    
            #endregion
    
            #region Private Properties
            protected XmlSerializer ValueSerializer
            {
                get
                {
                    if (valueSerializer == null)
                    {
                        if (extraTypes == null)
                            valueSerializer = new XmlSerializer(typeof(TVal));
                        else
                            valueSerializer = new XmlSerializer(typeof(TVal), extraTypes);
                    }
                    return valueSerializer;
                }
            }
    
            private XmlSerializer KeySerializer
            {
                get
                {
                    if (keySerializer == null)
                    {
                        if (extraTypes == null)
                            keySerializer = new XmlSerializer(typeof(TKey));
                        else
                            keySerializer = new XmlSerializer(typeof(TKey), extraTypes);
                    }
                    return keySerializer;
                }
            }
            #endregion
            #region Private Members
            [NonSerialized]
            private XmlSerializer keySerializer = null;
            [NonSerialized]
            private XmlSerializer valueSerializer = null;
            #endregion
        }
    
    }
    

    Output:

    Key = bar, Name = bar, Two = BAR, Type = ElementTwo
    Key = foo, Name = foo, One = FOO, Type = ElementOne
    

    You can comment out the passing of the types and deserialization fails: using(XmlReader rd = new KnownTypesXmlReader(mem) /* { ExtraTypes = extraTypes } */)

    ADDED COMMENTS:

    I was searching for the solution in this order:

    1. Use what is already there ([XmlInclude]) if you can. (Your runtime constraint disallows this.)
    2. Customize some class involved - the problem was in void IXmlSerializable.ReadXml(System.Xml.XmlReader reader) thus XmlReader was perfect candidate. I'd suggest using iterface for final solution (e.g. interface KnownTypes { Type[] GetKnownTypes(object me, string hint, params Type[] involved); })
    3. If above fails (if you were not in control of the deserializer) use static variable (or thread-local-storage for use with multiple threads, or static synchronized(weak)dictionary) for additional configuration. (Not perfect and it seems that you can use option 2.)