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?
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.
User contributions licensed under CC BY-SA 3.0