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.