Search code examples
c#inheritanceserializationdatacontractserializerdatacontract

Serialize a list of objects inheriting from class A to xml, so the names of the elements in the xml are B,C,D


Using DataContractSerializer I want to serialize a list of object that inherit from class A. Those objects are in different assembly and let's say they are of class B, C and D. I have added the B,C and D to the known types of the data contract serializer. I was able to serialize the list, but the outcome of the serialization looks like this:

<SerializedListObjects>
   <A i:type="B">
   <A i:type="C">
</SerializedListObjects>

What I want is:

<SerializedListObjects>
   <B>
   <C>
</SerializedListObjects>

Probably there can be some attribute in B and C with the information that those inherit from A.

This is my base class:

  [Serializable]
  [DataContract(Name = "A")]
  public abstract class A
  {
  }

And this is the example of a derived class definition.

  [Serializable]
  [DataContract(Name = "B")]
  public class B : A
  {
  }

Since the derived classes are in a different assembly, I can't put any attribute in their base class or the serialized class that would contain a derived class name (for example [XmlElement("B", Type = typeof(ChildB))]) - I don't have the access to derived classes there.

Is it possible?

While I am currently using DataContractSerializer, I am willing to switch to another XML serializer such as XmlSerializer if necessary.


Solution

  • Firstly DataContractSerializer does not have a mechanism to support collection item polymorphism by changing collection element name(s). It only supports the known type mechanism which uses the i:type attribute - which you indicate is not acceptable.

    Since you are willing to switch to XmlSerializer, you could use the attribute XmlArrayItemAttribute.Type to specify element names for polymorphic types in lists:

    public class AListObject
    {
        [XmlArrayItem(typeof(B))]
        [XmlArrayItem(typeof(C))]
        public List<A> SerializedListObjects { get; set; }
    }
    

    However, you also indicate that the polymorphic subtypes cannot be declared statically at compile type because they exist in some other assembly.

    As a result, you will need to use the XmlAttributeOverrides mechanism to specify all possible derived types for all List<A> properties in runtime, and manually construct an XmlSerializer using those overrides.

    Here is a prototype solution. First, let's assume you have a root object that refers to an object containing a List<A> like so:

    public class RootObject
    {
        public AListObject AList { get; set; }
    }
    
    public class AListObject
    {
        public List<A> SerializedListObjects { get; set; }
    }
    

    (The root object could be the object with the List<A> property, but doesn't need to be.) Let's also assume you know all such objects like AListObject that may contain List<A> properties.

    With those assumptions, the following serializer factory can be used to generate an XmlSerializer for any root object that may refer to any instances of the known types containing a List<A> property:

    public interface IXmlSerializerFactory
    {
        XmlSerializer CreateSerializer(Type rootType);
    }
    
    public static class AListSerializerFactory
    {
        static readonly XmlArrayItemTypeOverrideSerializerFactory instance;
    
        static AListSerializerFactory()
        {
            // Include here a list of all types that have a List<A> property.
            // You could use reflection to find all such public types in your assemblies.
            var declaringTypeList = new []
            {
                typeof(AListObject),
            };
    
            // Include here a list of all base types with a corresponding mapping 
            // to find all derived types in runtime.   Here you could use reflection
            // to find all such types in your assemblies, as shown in 
            // https://stackoverflow.com/questions/857705/get-all-derived-types-of-a-type
            var derivedTypesList = new Dictionary<Type,  Func<IEnumerable<Type>>>
            {
                { typeof(A), () => new [] { typeof(B), typeof(C) } },
            };
            instance = new XmlArrayItemTypeOverrideSerializerFactory(declaringTypeList, derivedTypesList);
        }
    
        public static IXmlSerializerFactory Instance { get { return instance; } }
    }
    
    public class XmlArrayItemTypeOverrideSerializerFactory : IXmlSerializerFactory
    {
        // To avoid a memory & resource leak, the serializers must be cached as explained in
        // https://stackoverflow.com/questions/23897145/memory-leak-using-streamreader-and-xmlserializer
    
        readonly object padlock = new object();
        readonly Dictionary<Type, XmlSerializer> serializers = new Dictionary<Type, XmlSerializer>();
        readonly XmlAttributeOverrides overrides;
    
        public XmlArrayItemTypeOverrideSerializerFactory(IEnumerable<Type> declaringTypeList, IEnumerable<KeyValuePair<Type, Func<IEnumerable<Type>>>> derivedTypesList)
        {
            var completed = new HashSet<Type>();
            overrides = declaringTypeList
                .SelectMany(d => derivedTypesList.Select(p => new { declaringType = d, itemType = p.Key, derivedTypes = p.Value() }))
                .Aggregate(new XmlAttributeOverrides(), (a, d) => a.AddXmlArrayItemTypes(d.declaringType, d.itemType, d.derivedTypes, completed));
        }
    
        public XmlSerializer CreateSerializer(Type rootType)
        {
            lock (padlock)
            {
                XmlSerializer serializer;
                if (!serializers.TryGetValue(rootType, out serializer))
                    serializers[rootType] = serializer = new XmlSerializer(rootType, overrides);
                return serializer;
            }
        }
    }
    
    public static partial class XmlAttributeOverridesExtensions
    {
        public static XmlAttributeOverrides AddXmlArrayItemTypes(this XmlAttributeOverrides overrides, Type declaringType, Type itemType, IEnumerable<Type> derivedTypes)
        {
            return overrides.AddXmlArrayItemTypes(declaringType, itemType, derivedTypes, new HashSet<Type>());
        }
    
        public static XmlAttributeOverrides AddXmlArrayItemTypes(this XmlAttributeOverrides overrides, Type declaringType, Type itemType, IEnumerable<Type> derivedTypes, HashSet<Type> completedTypes)
        {
            if (overrides == null || declaringType == null || itemType == null || derivedTypes == null || completedTypes == null)
                throw new ArgumentNullException();
            XmlAttributes attributes = null;
            for (; declaringType != null && declaringType != typeof(object); declaringType = declaringType.BaseType)
            {
                // Avoid duplicate overrides.
                if (!completedTypes.Add(declaringType))
                    break;
                foreach (var property in declaringType.GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance))
                {
                    // Skip the property if already ignored
                    if (property.IsDefined(typeof(XmlIgnoreAttribute), false))
                        continue;
    
                    // See if it is a list property, and if so, get its type.
                    var propertyItemType = property.PropertyType.GetListType();
                    if (propertyItemType == null)
                        continue;
    
                    // OK, its a List<itemType>.  Add all the necessary XmlElementAttribute declarations.
                    if (propertyItemType == itemType)
                    {
                        if (attributes == null)
                        {
                            attributes = new XmlAttributes();
                            foreach (var derivedType in derivedTypes)
                                // Here we are assuming all the derived types have unique XML type names.
                                attributes.XmlArrayItems.Add(new XmlArrayItemAttribute { Type = derivedType });
                            if (itemType.IsConcreteType())
                                attributes.XmlArrayItems.Add(new XmlArrayItemAttribute { Type = itemType });
                        }
                        overrides.Add(declaringType, property.Name, attributes);
                    }
                }
            }
            return overrides;
        }
    }
    
    public static class TypeExtensions
    {
        public static bool IsConcreteType(this Type type)
        {
            return !type.IsAbstract && !type.IsInterface;
        }
    
        public static Type GetListType(this Type type)
        {
            while (type != null)
            {
                if (type.IsGenericType)
                {
                    var genType = type.GetGenericTypeDefinition();
                    if (genType == typeof(List<>))
                        return type.GetGenericArguments()[0];
                }
                type = type.BaseType;
            }
            return null;
        }
    }
    

    Then, you can serialize and deserialize instances of RootObject to and from XML as follows:

    var root = new RootObject
    {
        AList = new AListObject
        {
            SerializedListObjects = new List<A> { new B(), new C() },
        },
    };
    
    var serializer = AListSerializerFactory.Instance.CreateSerializer(root.GetType());
    
    var xml = root.GetXml(serializer);
    var root2 = xml.LoadFromXml<RootObject>(serializer);
    

    Using the extension methods:

    public static class XmlSerializationHelper
    {
        public static T LoadFromXml<T>(this string xmlString, XmlSerializer serial = null)
        {
            serial = serial ?? new XmlSerializer(typeof(T));
            using (var reader = new StringReader(xmlString))
            {
                return (T)serial.Deserialize(reader);
            }
        }
    
        public static string GetXml<T>(this T obj, XmlSerializer serializer = null)
        {
            using (var textWriter = new StringWriter())
            {
                var settings = new XmlWriterSettings() { Indent = true }; // For cosmetic purposes.
                using (var xmlWriter = XmlWriter.Create(textWriter, settings))
                    (serializer ?? new XmlSerializer(obj.GetType())).Serialize(xmlWriter, obj);
                return textWriter.ToString();
            }
        }
    }
    

    And the result is:

    <RootObject xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <AList>
        <SerializedListObjects>
          <B />
          <C />
        </SerializedListObjects>
      </AList>
    </RootObject>
    

    Notes:

    • As explained in Memory Leak using StreamReader and XmlSerializer, you must statically cache any XmlSerializer constructed with XmlAttributeOverrides to avoid a severe memory leak. The documentation suggests using a Hashtable, however XmlAttributeOverrides does not override Equals() or GetHashCode(), and does not provide enough access to its internal data for applications developers to write their own. Thus it's necessary to hand-craft some sort of static caching scheme whenever XmlAttributeOverrides is used.

    • Given the complexity of finding and overriding the XmlArrayItem attributes of all List<A> properties, you might consider sticking with the existing i:type mechanism. It's simple, works well, is supported by both DataContractSerializer and XmlSerializer, and is standard.

    • I wrote the class XmlArrayItemTypeOverrideSerializerFactory in a generic way, which adds to the apparent complexity.

    Working sample .Net fiddle here.