0

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.

4
  • learn.microsoft.com/en-us/dotnet/standard/serialization/… Commented May 22, 2018 at 15:59
  • I don't think DataContractSerializer has a mechanism to support polymorphism by changing the element name, it only supports the known type mechanism which uses the i:type attribute. You might need to switch to XmlSerializer and use one of the attributes shown in Using XmlSerializer to serialize derived classes. Commented May 22, 2018 at 16:07
  • @dbc I have changed the DataContractSerializer to XmlSerializer but the behavior is the same. I can't use the attributes on the base type, because I have the derived types in a separate assembly. I am passing the retrieved known types while initializing XmlSerializer. Commented May 22, 2018 at 16:28
  • @MikołajMularczyk - in that case, can you edit your question to give some more details about your current problem? Is it that you can't add the [XmlElement("B", Type = typeof(ChildB))] attribute(s) to the serialized list? A minimal reproducible example would be great. Commented May 22, 2018 at 18:33

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

That is a great answer, thank you. I ended up with manually editing the xml structure (creating a new node with the name from the type attribute and replacing the old one with it). I would love to stick to the i:type mechanism, but having a name of the derived class as an element name was a requirement.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.