Question

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.

Était-ce utile?

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

Autres conseils

First of all XmlSerializer ignores [Serializable], [NonSerialized], [DataContract] and [DataMember] attributes - it is controlled either by IXmlSerializable interface (which completely changes behavior of object serialization) or by default it serializes all public members/properties of an object, and you can give hints to XmlSerializer via attributes like [XmlRoot], [XmlAttribute], [XmlIgnore] [XmlArray], [XmlElement], [XmlArrayItem], [XmlInclude], [XmlText], etc.

The functionality you're after is already included in those attributes. Lets assume you have SerializableSortedDictionary<string, Car> where Car is class with subclasses Volvo and Audi.

[XmlInclude(typeof(Audi))]
[XmlInclude(typeof(Volvo))]
public class Car {
  private string m_Name = "Car";

  public virtual string Name {
    get { return m_Name; }
    set { m_Name = value; }
  }
}

public class Audi : Car {
  private string m_Name = "Audi";

  public override string Name {
    get { return m_Name; }
    set { m_Name = value; }
  }
}

public class Volvo : Car {
  private string m_Name = "Volvo";

  public override string Name {
    get { return m_Name; }
    set { m_Name = value; }
  }
}

All you need is to decorate the base class with all possible sub classes via the XmlInclude

var dic = new SerializableSortedDictionary<string, Car>();
dic.Add("0", new Car());
dic.Add("1", new Audi());
dic.Add("2", new Volvo());

var serializer = new XmlSerializer(typeof(SerializableSortedDictionary<string, Car>));
var builder = new StringBuilder();
using(var writer = new StringWriter(builder)) {
  serializer.Serialize(writer, dic);
}

Deserialization works as well. You may notice xsi:type attribute in resulting xml - that how xml serializer persist information about types. It essentially impossible to "guess" type from serialized object without specifying it. It won't work for generic types, that weren't specified via XmlInclude - that's a security feature (if an attacker can make you make instance of any object he like when you're parsing xml feed you may run into serious troubles).

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top