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?

dbc
  • 104,963
  • 20
  • 228
  • 340
jps
  • 1
  • 5
  • I think you confused the classes names or didn't detail all of them (Project, Motor), did you? – user3114639 Apr 23 '18 at 09:11
  • Did the file that you attempted to de-serialize come from your code or from another source? If it is a different source compare the C# code with other source to find differences. Often happens that you can serialize to xml but cannot deserialize. To fix you have to add properties definition above the class/properties in the classes to fix issue. The error indicates you have two properties/classes with the same name. Probably an overload in c#. – jdweng Apr 23 '18 at 09:11
  • Correct, I confused class names. Motor should be Project too. I changed it in my original question. – jps Apr 23 '18 at 09:23
  • Yes, the XML comes from my code, same solution. I store the project and all of its contents ass XML in a SQL-database – jps Apr 23 '18 at 09:24
  • @jdweng: And you are right that both classes could have the same XmlType name. I want this to be it like this, because if I change it, the XML node names change too. – jps Apr 23 '18 at 09:32
  • What you should do is have both classes inherit the same base class. Then the Element name will be the same but the type attribute will be different. The serializer automatically adds a type attribute if the class inherits a base class. – jdweng Apr 23 '18 at 10:51
  • @jdweng: Thnx. I made my baseclass abstract, to prevent it from being instantiated at all. Then I created a class Project_A and project_B, which both inherit the same baseclass. Writing the XML is no problem, but reading the XML throws the same error. Any suggestions? – jps Apr 23 '18 at 12:59
  • Need to add the datatype to definition : [XmlRoot(ElementName="Test",DataType="Test")] – jdweng Apr 23 '18 at 14:15
  • Not sure I understand the question. Are you saying that you want to serialize two different types using the same root element name and namespace? If you do that, you won't be able to determine from the XML which type had been serialized. Can you confirm that's really what you want? Or do you want a way to force `XmlSerializer` to deserialize an XML document to a specified type regardless of the root element name and namespace? – dbc Apr 23 '18 at 16:47
  • @dbc Yes, to different types (both same baseclass if needed like recommended by jdweng) should result in an XML with the same root element and namespace. The XML should always look the same. And yes, this means that I need to control / force the XmlSerializer to deserialize to the type I want, regardless of the root element name and namespace. – jps Apr 24 '18 at 09:43
  • @jdweng I did some testing and as you state the type attribute is added to the XML when using different classes with same baseclass. This means a change to my XML and is not what I am looking for. My goal is not having to change the format of my XML in any way. Sorry. – jps Apr 24 '18 at 14:01
  • @jps - does your XML have an [`xsi:type`](https://msdn.microsoft.com/en-us/library/ca1ks327.aspx) attribute on the root element? – dbc Apr 24 '18 at 14:11
  • What does your schema require? – jdweng Apr 24 '18 at 14:25
  • @dbc No, it has not, I would like to have it as generic as possible. – jps Apr 24 '18 at 14:26
  • @jdweng - Currently I do not use a scheme. I built a class (project) containing everything I need (e.g. several lists with objets) and had it serialized to an XML using the default serializer from Microsoft. This made it very easy to serialize a complex object and resulted in an XML format I like. I try to stick to this exact XML format. – jps Apr 24 '18 at 14:31
  • I think what I want is not easy and is tricky. What I will try to do is serialize and deserialize to and from one baseclass, e.g. projectA, which inherits from one abstract baseclass projectMama. I can create other classes which inherit from projectMama, e.g. projectB. Then just cast my projectA to projectB and all should work. The comment of jdweng brought me to this. I will let know if this works. In addition I will investigate the solution from dbc too, I just saw it. For now thank you all! – jps Apr 24 '18 at 15:34
  • Based on the feedback I got from you I decided to (de)serialize always from 1 class (project_A). This results in a stable XML file. I convert / cast from project_A to project_B using JSON. I think that is the most save and simple way. Thanks for the excellent help and adivce! – jps Apr 30 '18 at 14:04

1 Answers1

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.

dbc
  • 104,963
  • 20
  • 228
  • 340