c# XML deserialization of 1 XML to different classes

0

I would like to manage multiple customer standards efficiently.

If I open (deserialize) an XML, I want to determine which classes are used during deserialisation. Choosing another class basically means looking at the XML from an other perspective (view).

What I have right now: I have a class Project which has some properties and methods. I am able to serialize instances of motor to XML, this works fine. Also deserialization works fine.

Now I create a new class ProjectCustomerA, which is derived from the base class Project. I overwrite some of the methods on ProjectCustomerA and might add some in the future.

Both class Project and ProjectCustomerA share the same XmlType ([Serializable, XmlType("Project")]).

Now when I deserialize an XML I get an error that both classes use the same XmlType and that this is not possible.

Below is the message I get (it was originally in Dutch and I translated):

System.InvalidOperationException HResult=0x80131509 ... Inner Exception 1: InvalidOperationException: The types C4M_Data.C4M_Project and C4M_Data_customer.C4M_Project_Customer both use the XML-typename, Project, from namespace . Use XML-attributes to define a unique XML-name and/or -namespace for the type.

My question is how to read (deserialize) the same XML and let me control what classes are instantiated in my application during this process?

My current idea is that different types (all the same baseclass if needed) should result in an XML with the same root element and namespace. The XML should always look the same. Then I need to control / force the XmlSerializer to deserialize to the type I want, regardless of the root element name and namespace. Is this possible?

c#
xml
deserialization
xmlserializer
asked on Stack Overflow Apr 23, 2018 by jps • edited Apr 24, 2018 by dbc

1 Answer

0

You cannot have multiple types in the type hierarchy have identical [XmlType] attributes. If you do, the XmlSerializer constructor will throw the exception you have seen, stating:

Use XML-attributes to define a unique XML-name and/or -namespace for the type.

The reason XmlSerializer requires unique element names and/or namespaces for all types in the hierarchy is that it is designed to be able to successfully serialize type information via the xsi:type mechanism - which becomes impossible if the XML names & namespaces are identical. You wish to make all the types in your root data model hierarchy be indistinguishable when serialized to XML which conflicts with this design intent of XmlSerializer.

Instead, when serializing, you can construct your XmlSerializer with the XmlSerializer(Type, XmlRootAttribute) constructor to specify a shared root element name and namespace to be used for all objects in your root model hierarchy. Then when deserializing you can construct an XmlSerializer using the root element name and namespace actually encountered in the file. The following extension methods do the job:

public static partial class XmlSerializationHelper
{
    public static T LoadFromXmlAsType<T>(this string xmlString)
    {
        return new StringReader(xmlString).LoadFromXmlAsType<T>();
    }

    public static T LoadFromXmlAsType<T>(this TextReader textReader)
    {
        using (var xmlReader = XmlReader.Create(textReader, new XmlReaderSettings { CloseInput = false }))
            return xmlReader.LoadFromXmlAsType<T>();
    }

    public static T LoadFromXmlAsType<T>(this XmlReader xmlReader)
    {
        while (xmlReader.NodeType != XmlNodeType.Element)
            if (!xmlReader.Read())
                throw new XmlException("No root element");
        var serializer = XmlSerializerFactory.Create(typeof(T), xmlReader.LocalName, xmlReader.NamespaceURI);
        return (T)serializer.Deserialize(xmlReader);
    }

    public static string SaveToXmlAsType<T>(this T obj, string localName, string namespaceURI)
    {
        var sb = new StringBuilder();
        using (var writer = new StringWriter(sb))
            obj.SaveToXmlAsType(writer, localName, namespaceURI);
        return sb.ToString();
    }

    public static void SaveToXmlAsType<T>(this T obj, TextWriter textWriter, string localName, string namespaceURI)
    {
        using (var xmlWriter = XmlWriter.Create(textWriter, new XmlWriterSettings { CloseOutput = false, Indent = true }))
            obj.SaveToXmlAsType(xmlWriter, localName, namespaceURI);
    }

    public static void SaveToXmlAsType<T>(this T obj, XmlWriter xmlWriter, string localName, string namespaceURI)
    {
        var serializer = XmlSerializerFactory.Create(obj.GetType(), localName, namespaceURI);
        serializer.Serialize(xmlWriter, obj);
    }
}

public static class XmlSerializerFactory
{
    // To avoid a memory leak the serializer must be cached.
    // https://stackoverflow.com/questions/23897145/memory-leak-using-streamreader-and-xmlserializer
    // This factory taken from 
    // https://stackoverflow.com/questions/34128757/wrap-properties-with-cdata-section-xml-serialization-c-sharp/34138648#34138648

    readonly static Dictionary<Tuple<Type, string, string>, XmlSerializer> cache;
    readonly static object padlock;

    static XmlSerializerFactory()
    {
        padlock = new object();
        cache = new Dictionary<Tuple<Type, string, string>, XmlSerializer>();
    }

    public static XmlSerializer Create(Type serializedType, string rootName, string rootNamespace)
    {
        if (serializedType == null)
            throw new ArgumentNullException();
        if (rootName == null && rootNamespace == null)
            return new XmlSerializer(serializedType);
        lock (padlock)
        {
            XmlSerializer serializer;
            var key = Tuple.Create(serializedType, rootName, rootNamespace);
            if (!cache.TryGetValue(key, out serializer))
                cache[key] = serializer = new XmlSerializer(serializedType, new XmlRootAttribute { ElementName = rootName, Namespace = rootNamespace });
            return serializer;
        }
    }
}

Then, if your type hierarchy looks something like this:

public class Project
{
    // Name for your root element.  Replace as desired.
    public const string RootElementName = "Project";
    // Namespace for your project.  Replace as required.
    public const string RootElementNamespaceURI = "https://stackoverflow.com/questions/49977144";

    public string BaseProperty { get; set; }
}

public class ProjectCustomerA : Project
{
    public string CustomerProperty { get; set; }

    public string ProjectCustomerAProperty { get; set; }
}

public class ProjectCustomerB : Project
{
    public string CustomerProperty { get; set; }

    public string ProjectCustomerBProperty { get; set; }
}

You can serialize an instance of ProjectCustomerA and deserialize it as an instance of ProjectCustomerB as follows:

var roota = new ProjectCustomerA
{
    BaseProperty = "base property value",
    CustomerProperty = "shared property value",
    ProjectCustomerAProperty = "project A value",
};
var xmla = roota.SaveToXmlAsType(Project.RootElementName, Project.RootElementNamespaceURI);

var rootb = xmla.LoadFromXmlAsType<ProjectCustomerB>();
var xmlb = rootb.SaveToXmlAsType(Project.RootElementName, Project.RootElementNamespaceURI);

// Assert that the shared BaseProperty was deserialized successfully.
Assert.IsTrue(roota.BaseProperty == rootb.BaseProperty);
// Assert that the same-named CustomerProperty was ported over properly.
Assert.IsTrue(roota.CustomerProperty == rootb.CustomerProperty);

Notes:

  • I chose to put the shared XML element name and namespace as constants in the base type Project.

  • When constructing an XmlSerializer with an override root element name or namespace, it must be cached to avoid a memory leak.

  • All this being said, making it be impossible to determine whether a given XML file contains an object of type ProjectCustomerA or ProjectCustomerB seems like a dangerously inflexible design going forward. I'd encourage you to rethink whether this design is appropriate. For instance, you could instead serialize them with their default, unique element names and namespaces, and still deserialize to any desired type using the methods LoadFromXmlAsType<T>() above, which generate an XmlSerializer using the actual name and namespace found in the file.

  • The methods LoadFromXmlAsType<T>() may not work if there is an xsi:type attribute on the root element. If you want to ignore (or process) the xsi:type attribute then further work may be required.

Sample working .Net fiddle.

answered on Stack Overflow Apr 24, 2018 by dbc • edited Apr 24, 2018 by dbc

User contributions licensed under CC BY-SA 3.0