C# deserialize xml while ignoring namespace - c#

I have to load and deserialize an Xml file into an object. I can read the xml, get to the point where the object is described and parse the xml only from that part which is great, but there is a namespace declared in the root of the xml.
I don't understand why but when reading the xml, even though I read it from a given node, the xmlns attribute gets added to it, resulting in my program not being able to deserialize that into an object, due to the unexpected member.
My code:
public static SomeClass GetObjectFromXml (string path)
{
XmlReader reader = XmlReader.Create(path);
string wantedNodeContents = string.Empty;
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "IWantThis")
{
wantedNodeContents = reader.ReadOuterXml();
break;
}
}
XmlSerializer xmlSerializer = new XmlSerializer(typeof(SomeClass));
System.IO.StringReader stringReader = new System.IO.StringReader(wantedNodeContents);
SomeClass loadedSomeClassXml = xmlSerializer.Deserialize(stringReader) as SomeClass;
return loadedSomeClassXml;
}
How could I get rid of the xmlns and deserialize the xml into an object?

You have a few issues here:
The default namespace attribute is added to the string returned by ReadOuterXml() because ReadOuterXml() is designed not to change the semantics of the returned XML. Apparently in your XML there is a default namespace applied to some parent node of <IWantThis> -- which, being a default namespace, recursively applies to <IWantThis> itself. To retain this namespace membership, ReadOuterXml() must emit a default namespace as it writes out the nested XML.
If you really want to completely ignore namespaces on XML, you need to create a custom XmlReader, e.g. as shown in
this answer to Can I make XmlSerializer ignore the namespace on deserialization? by Cheeso.
this answer to How do I create a XmlTextReader that ignores Namespaces and does not check characters by Alterant.
You need to construct an XmlSerializer for SomeClass whose expected root node is <IWantThis>. You can do this using the XmlSerializer(Type, XmlRootAttribute) constructor, however, if you do, you must statically cache and reuse the serializer to avoid a severe memory leak, as explained in Memory Leak using StreamReader and XmlSerializer.
You are creating a local copy wantedNodeContents of the element you want to deserialize, then re-parsing that local copy. There is no need to do this, you can use XmlReader.ReadSubtree() to deserialize just a portion of the XML.
Putting all these issues together, your GetObjectFromXml() could look like:
public static partial class XmlExtensions
{
public static T GetObjectFromXml<T>(string path, string localName, string namespaceURI, bool ignoreNamespaces = false)
{
using (var textReader = new StreamReader(path))
return GetObjectFromXml<T>(textReader, localName, namespaceURI);
}
public static T GetObjectFromXml<T>(TextReader textReader, string localName, string namespaceURI, bool ignoreNamespaces = false)
{
using (var xmlReader = ignoreNamespaces ? new NamespaceIgnorantXmlTextReader(textReader) : XmlReader.Create(textReader))
return GetObjectFromXml<T>(xmlReader, localName, namespaceURI);
}
public static T GetObjectFromXml<T>(XmlReader reader, string localName, string namespaceURI)
{
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.LocalName == "IWantThis" && reader.NamespaceURI == namespaceURI)
{
var serializer = XmlSerializerFactory.Create(typeof(T), localName, namespaceURI);
using (var subReader = reader.ReadSubtree())
return (T)serializer.Deserialize(subReader);
}
}
// Or throw an exception?
return default(T);
}
}
// This class copied from this answer https://stackoverflow.com/a/873281/3744182
// To https://stackoverflow.com/questions/870293/can-i-make-xmlserializer-ignore-the-namespace-on-deserialization
// By https://stackoverflow.com/users/48082/cheeso
// helper class to ignore namespaces when de-serializing
public class NamespaceIgnorantXmlTextReader : XmlTextReader
{
public NamespaceIgnorantXmlTextReader(System.IO.TextReader reader): base(reader) { }
public override string NamespaceURI { get { return ""; } }
}
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;
}
}
}
Demo fiddle here.

XDocument provides you a bit of more flexibility at time of deserialize any XML. I had a similiar problem and it was resolve using the next snippet code:
///Type T must have a default constructor
private T XMLToObject (string pathXML)
{
T myObjectParsedFromXML= default(T);
LoadOptions loadOpt = LoadOptions.SetLineInfo;
XDocument xmlDocument = XDocument.Load(pathXML , loadOpt);
string namespaceXML = xmlDocument.Root.Name.Namespace.NamespaceName;
XmlSerializer serializer = new XmlSerializer(typeof(T), defaultNamespace: namespaceXML);
XmlReader XMLreader = xmlDocument.CreateReader();
myObjectParsedFromXML= (T)serializer.Deserialize(XMLreader);
return myObjectParsedFromXML;
}
In addition, XmlSerializer provides you a set of events for register any issue or error during serialization process:
XmlSerializer serializer = new XmlSerializer(typeof(T), defaultNamespace: namespaceXML);
serializer.UnknownAttribute += new XmlAttributeEventHandler((sender, args) =>
{
//Your code for manage the errors during serialization
});
serializer.UnknownElement += new XmlElementEventHandler((sender, args) =>
{
//Your code for manage the errors during serialization
});

Related

Deserialize Comments using Custom IXmlSerializer

I am attempting to serialize my Description property to an Xml comment. So, to do this I have implemented IXmlSerializable and the following WriteXml produces very nice XML.
[Serializable]
public sealed class Setting<T> : SettingBase, IXmlSerializable
{
public Setting() { }
public Setting(T value, string description)
{
Value = value;
Description = description;
}
public Setting(string command, T value, string description)
: this(value, description)
{
Command = command;
}
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
}
public void WriteXml(XmlWriter writer)
{
var properties = GetType().GetProperties();
foreach (var propertyInfo in properties)
{
if (propertyInfo.IsDefined(typeof(XmlCommentAttribute), false))
writer.WriteComment(Description);
else if (!propertyInfo.CustomAttributes.Any((attr) => attr.AttributeType.Equals(typeof(XmlIgnoreAttribute))))
writer.WriteElementString(propertyInfo.Name, propertyInfo.GetValue(this, null)?.ToString());
}
}
[XmlComment, Browsable(false)]
public string Description { get; set; }
[XmlElement, Browsable(false)]
public string Command { get; set; }
[XmlElement, Browsable(false)]
public T Value { get; set; }
[XmlIgnore]
public override object ValueUntyped { get { return Value; } }
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class XmlCommentAttribute : Attribute {}
However, I have had many attempts at implementing the ReadXml but i cannot seem to be able to deserialize the Description comment.
How can I implement ReadXml to deserailize my class?
When implementing IXmlSerializable you need to adhere to the rules stated in this answer to Proper way to implement IXmlSerializable? by Marc Gravell as well as the documentation:
For IXmlSerializable.WriteXml(XmlWriter):
The WriteXml implementation you provide should write out the XML representation of the object. The framework writes a wrapper element and positions the XML writer after its start. Your implementation may write its contents, including child elements. The framework then closes the wrapper element.
For IXmlSerializable.ReadXml(XmlReader):
The ReadXml method must reconstitute your object using the information that was written by the WriteXml method.
When this method is called, the reader is positioned on the start tag that wraps the information for your type. That is, directly on the start tag that indicates the beginning of a serialized object. When this method returns, it must have read the entire element from beginning to end, including all of its contents. Unlike the WriteXml method, the framework does not handle the wrapper element automatically. Your implementation must do so. Failing to observe these positioning rules may cause code to generate unexpected runtime exceptions or corrupt data.
It turns out to be very tricky to write a ReadXml() that correctly handles edge cases such as out-of-order or unexpected elements, missing or extra whitespace, empty elements, and so on. As such it makes sense to adopt some sort of parsing framework to iterate through the XML tree correctly, such as this one from Why does XmlSerializer throws an Exception and raise a ValidationEvent when a schema validation error occurs inside IXmlSerializable.ReadXml(), and extend it to handle comment nodes:
public static class XmlSerializationExtensions
{
// Adapted from this answer https://stackoverflow.com/a/60498500/3744182
// To https://stackoverflow.com/questions/60449088/why-does-xmlserializer-throws-an-exception-and-raise-a-validationevent-when-a-sc
// by handling comments.
public static void ReadIXmlSerializable(XmlReader reader, Func<XmlReader, bool> handleXmlAttribute, Func<XmlReader, bool> handleXmlElement, Func<XmlReader, bool> handleXmlText, Func<XmlReader, bool> handleXmlComment)
{
//https://learn.microsoft.com/en-us/dotnet/api/system.xml.serialization.ixmlserializable.readxml?view=netframework-4.8#remarks
//When this method is called, the reader is positioned on the start tag that wraps the information for your type.
//That is, directly on the start tag that indicates the beginning of a serialized object.
//When this method returns, it must have read the entire element from beginning to end, including all of its contents.
//Unlike the WriteXml method, the framework does not handle the wrapper element automatically. Your implementation must do so.
//Failing to observe these positioning rules may cause code to generate unexpected runtime exceptions or corrupt data.
reader.MoveToContent();
if (reader.NodeType != XmlNodeType.Element)
throw new XmlException(string.Format("Invalid NodeType {0}", reader.NodeType));
if (reader.HasAttributes)
{
for (int i = 0; i < reader.AttributeCount; i++)
{
reader.MoveToAttribute(i);
handleXmlAttribute(reader);
}
reader.MoveToElement(); // Moves the reader back to the element node.
}
if (reader.IsEmptyElement)
{
reader.Read();
return;
}
reader.ReadStartElement(); // Advance to the first sub element of the wrapper element.
while (reader.NodeType != XmlNodeType.EndElement)
{
if (reader.NodeType == XmlNodeType.Element)
{
using (var subReader = reader.ReadSubtree())
{
subReader.MoveToContent();
handleXmlElement(subReader);
}
// ReadSubtree() leaves the reader positioned ON the end of the element, so read that also.
reader.Read();
}
else if (reader.NodeType == XmlNodeType.Text || reader.NodeType == XmlNodeType.CDATA)
{
var type = reader.NodeType;
handleXmlText(reader);
// Ensure that the reader was not advanced.
if (reader.NodeType != type)
throw new XmlException(string.Format("handleXmlText incorrectly advanced the reader to a new node {0}", reader.NodeType));
reader.Read();
}
else if (reader.NodeType == XmlNodeType.Comment)
{
var type = reader.NodeType;
handleXmlComment(reader);
// Ensure that the reader was not advanced.
if (reader.NodeType != type)
throw new XmlException(string.Format("handleXmlComment incorrectly advanced the reader to a new node {0}", reader.NodeType));
reader.Read();
}
else // Whitespace, etc.
{
// Skip() leaves the reader positioned AFTER the end of the node.
reader.Skip();
}
}
// Move past the end of the wrapper element
reader.ReadEndElement();
}
public static void ReadIXmlSerializable(XmlReader reader, Func<XmlReader, bool> handleXmlAttribute, Func<XmlReader, bool> handleXmlElement, Func<XmlReader, bool> handleXmlText)
{
ReadIXmlSerializable(reader, handleXmlAttribute, handleXmlElement, handleXmlText, r => true);
}
public static void WriteIXmlSerializable(XmlWriter writer, Action<XmlWriter> writeAttributes, Action<XmlWriter> writeNodes)
{
//https://learn.microsoft.com/en-us/dotnet/api/system.xml.serialization.ixmlserializable.writexml?view=netframework-4.8#remarks
//The WriteXml implementation you provide should write out the XML representation of the object.
//The framework writes a wrapper element and positions the XML writer after its start. Your implementation may write its contents, including child elements.
//The framework then closes the wrapper element.
writeAttributes(writer);
writeNodes(writer);
}
}
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 modify your class to use it as follows:
[Serializable]
public sealed class Setting<T> : SettingBase, IXmlSerializable
{
public Setting() { }
public Setting(T value, string description)
{
Value = value;
Description = description;
}
public Setting(string command, T value, string description)
: this(value, description)
{
Command = command;
}
public XmlSchema GetSchema() { return null;}
public void ReadXml(XmlReader reader)
{
XmlSerializationExtensions.ReadIXmlSerializable(reader, r => true,
r =>
{
switch (r.LocalName)
{
case "Command":
Command = r.ReadElementContentAsString();
break;
case "Value":
var serializer = XmlSerializerFactory.Create(typeof(T), "Value", reader.NamespaceURI);
Value = (T)serializer.Deserialize(r);
break;
}
return true;
},
r => true, r => { Description += r.Value; return true; });
}
public void WriteXml(XmlWriter writer)
{
XmlSerializationExtensions.WriteIXmlSerializable(writer, w => { },
w =>
{
if (Description != null)
w.WriteComment(Description);
if (Command != null)
w.WriteElementString("Command", Command);
if (Value != null)
{
var serializer = XmlSerializerFactory.Create(typeof(T), "Value", null);
serializer.Serialize(w, Value);
}
});
}
public string Description { get; set; }
public string Command { get; set; }
public T Value { get; set; }
public override object ValueUntyped { get { return Value; } }
}
// ABSTRACT BASE CLASS NOT INCLUDED IN QUESTION, THIS IS JUST A GUESS
[Serializable]
public abstract class SettingBase
{
public abstract object ValueUntyped { get; }
}
And you will be able to round-trip it to XML.
Notes:
Since your class is sealed I replaced use of reflection with direct access to properties to serialize.
In your version you serialize your T Value to XML by writing its ToString() value:
writer.WriteElementString(propertyInfo.Name, propertyInfo.GetValue(this, null)?.ToString());
Unless the value is, itself, a string, this is likely to produce a wrong result:
Numeric, DateTime, TimeSpan and similar primitives will be localized. XML primitives should always be formatted in a culturally invariant manner.
Complex objects such as string [] that do not override ToString() will be formatted in a completely incorrect manner.
To avoid these problems my version serializes the value to XML by constructing an appropriate XmlSerializer. This guarantees correctness but may be slower than your version. If performance matters here you could check for known types (such as string) and format them to XML manually, using e.g. the utility class XmlConvert.
XmlReader.ReadSubtree() is used to ensure that the XmlReader is not mispositioned by HandleXmlElement(XmlReader reader).
Demo fiddle here.

How to serialize properties with DefaultValueAttribute using XmlSerializer?

I am using XmlSerializer to serialize C# objects to XML. I have DefaultValueAttribute on some of the properties of the classes I am trying to serialize, when I try to serialize them it seems that XmlSerializer does not include value in xml if it equals default value.
Look at this example:
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace Test
{
public class Person
{
[System.Xml.Serialization.XmlAttribute()]
[System.ComponentModel.DefaultValue("John")]
public string Name { get; set; }
}
public static class Test
{
public static void Main()
{
var serializer = new XmlSerializer(typeof(Person));
var person = new Person { Name = "John" };
using (var sw = new StringWriter())
{
using (var writer = XmlWriter.Create(sw))
{
serializer.Serialize(writer, person);
var xml = sw.ToString();
}
}
}
}
}
It will produce the following xml (notice name attribute is not available):
<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" />
I cannot modify the source code of the classes so I CANNOT remove DefaultValueAttribute. Is there a way to make XmlSerializer serialize this properties without changing the source code?
You can do it by passing in an XmlAttributeOverrides instance to the XmlSerializer when you create it as below. You can either change the default value to something else using this, or set it to null to effectively remove it.
XmlAttributeOverrides attributeOverrides = new XmlAttributeOverrides();
var attributes = new XmlAttributes()
{
XmlDefaultValue = null,
XmlAttribute = new XmlAttributeAttribute()
};
attributeOverrides.Add(typeof(Person), "Name", attributes);
var serializer = new XmlSerializer(typeof(Person), attributeOverrides);
var person = new Person { Name = "John" };
using (var sw = new StringWriter())
{
using (var writer = XmlWriter.Create(sw))
{
serializer.Serialize(writer, person);
var xml = sw.ToString();
}
}
Update: the above means you have to provide other unrelated attributes again on each property you are changing. This is a bit of a chore if you have a lot of properties and just want to remove default for all of them. The class below can be used to preserve other custom attributes while only removing one type of attribute. It can be further extended if required to only do it for certain properties etc.
public class XmlAttributeOverrideGenerator<T>
{
private static XmlAttributeOverrides _overrides;
private static Type[] _ignoreAttributes = new Type[] { typeof(DefaultValueAttribute) };
static XmlAttributeOverrideGenerator()
{
_overrides = Generate();
}
public static XmlAttributeOverrides Get()
{
return _overrides;
}
private static XmlAttributeOverrides Generate()
{
var xmlAttributeOverrides = new XmlAttributeOverrides();
Type targetType = typeof(T);
foreach (var property in targetType.GetProperties())
{
XmlAttributes propertyAttributes = new XmlAttributes(new CustomAttribProvider(property, _ignoreAttributes));
xmlAttributeOverrides.Add(targetType, property.Name, propertyAttributes);
}
return xmlAttributeOverrides;
}
public class CustomAttribProvider : ICustomAttributeProvider
{
private PropertyInfo _prop = null;
private Type[] _ignoreTypes = null;
public CustomAttribProvider(PropertyInfo property, params Type[] ignoreTypes)
{
_ignoreTypes = ignoreTypes;
_prop = property;
}
public object[] GetCustomAttributes(bool inherit)
{
var attribs = _prop.GetCustomAttributes(inherit);
if (_ignoreTypes == null) return attribs;
return attribs.Where(attrib => IsAllowedType(attrib)).ToArray();
}
private bool IsAllowedType(object attribute)
{
if (_ignoreTypes == null) return true;
foreach (Type type in _ignoreTypes)
if (attribute.GetType() == type)
return false;
return true;
}
public object[] GetCustomAttributes(Type attributeType, bool inherit)
{
throw new NotImplementedException();
}
public bool IsDefined(Type attributeType, bool inherit)
{
throw new NotImplementedException();
}
}
}
Usage:
XmlAttributeOverrides attributeOverrides = XmlAttributeOverrideGenerator<Person>.Get();
var serializer = new XmlSerializer(typeof(Person), attributeOverrides);
I dont have enough reputation to comment on steve16351's answer. but I feel I improved upon his code a little bit.
My use case involved an XML schema file generated via XSD.exe provided by Microsoft, and a class was then generated from the schema file. So the class had all the DefaultValue tags on it from the schema. There was also many nested types created. So to make my code succinct, I modified the 'Generate' method to get the overrides for the top-level class, and all the ones beneath it. Then everything is returned in a single XmlAttributesOverrides object.
static XmlAttributeOverrideGenerator()
{
_overrides = Generate(typeof(T), new XmlAttributeOverrides());
}
private static XmlAttributeOverrides Generate(Type targetType, XmlAttributeOverrides Overrides)
{
foreach (var property in targetType.GetProperties())
{
if (property.CustomAttributes.Any( CA => CA.AttributeType == typeof(DefaultValueAttribute)))
{
XmlAttributes propertyAttributes = new XmlAttributes(new CustomAttribProvider(property, _ignoreAttributes));
Overrides.Add(targetType, property.Name, propertyAttributes);
}
else
{
Type propType = property.PropertyType;
if (propType.IsClass && !propType.IsValueType && !propType.IsEnum && !propType.IsPrimitive && !propType.IsSealed) // Check if this is a custom class
{
//If this is a nested class or other class type, generate its overrides as well
Generate(propType, Overrides);
}
}
}
return Overrides;
}

Using class name as method parameter and return type

I have an extension method for deserialization of an XDocument.
I use CarConfiguration as variable in this method, but I have another class configurations:
public static CarConfiguration Deserialize(this XDocument xdoc)
{
XmlSerializer serializer = new XmlSerializer(typeof(CarConfiguration));
using (StringReader reader = new StringReader(xdoc.ToString()))
{
CarConfiguration cfg = (CarConfiguration) serializer.Deserialize(reader);
return cfg;
}
}
class CarConfiguration
{
//car
}
class BikeConfiguration
{
//bike
}
So, there are 2 questions here:
Can I use class name as parameter for this method? Something like this:
static CarConfiguration Deserialize(this XDocument xdoc, class classname) {
var serializer = new XmlSerializer(typeof(classname));
Can I make this method generic for all required types(CarConfiguration, BikeConfiguration etc.)? I mean for example dependency of return type on input class:
static <classname> Deserialize(this XDocument xdoc, class classname) {
The key word in your question is generic, so yes you can do this by utilising C# generics. For example:
public static T Deserialize<T>(this XDocument xdoc)
{
XmlSerializer serializer = new XmlSerializer(typeof(T));
using (StringReader reader = new StringReader(xdoc.ToString()))
{
T cfg = (T) serializer.Deserialize(reader);
return cfg;
}
}
And now you call it like this:
CarConfiguration x = xmlDocument.Deserialize<CarConfiguration>();

.NET get class name from String

I need to use this method :
public T DeserializeFromXmlString<T>(string xmlString)
{
var serializer = new XmlSerializer(typeof(T));
using (TextReader reader = new StringReader(xmlString))
{
return (T)serializer.Deserialize(reader);
}
}
And to use it, I need to know the generic type T. Imagine that I have a class "Apple". I will use it like this:
var myapple= DeserializeFromXmlString<Apple>(someXmlString);
But I need to be able to remove this <Apple> and replacing it by something else considering that I have the string "Apple".The goal is to convert a string to be able to use it in this method as a generic type T.
Re-design your API to support non-generic cases:
public object DeserializeFromXmlString(Type targetType, string xmlString)
{
var serializer = new XmlSerializer(targetType);
using (TextReader reader = new StringReader(xmlString))
{
return serializer.Deserialize(reader);
}
}
public T DeserializeFromXmlString<T>(string xmlString)
{
return (T)DeserializeFromXmlString(typeof(T), xmlString);
}
Load type from string and use non-generic API:
var targetType = Type.GetType("YourTypeName", true);
var deserializedObj = DeserializeFromXmlString(targetType, yourXmlString);

Better IXmlSerializable format?

I have an interface IInput that is preventing the XmlSerializer from serializing the class natively (because it doesnt like interfaces). I found a hack/workaround that attempts to just create the underlying implementation when deserializing and then casts that back to the interface. The deserializer knows the underlying implementation because its encoded as a attribute AssemblyQualifiedName
In order to take advantage of this technique I have to implement IXmlSerializable, but only 1 property really needs help (IInput Input), I wish for all the other ones to act as if they were normal. Here is my class, it works as expected
but seems like a very messy way of getting types of which the normal XMLserializer can serialize to conform to an IXmlSerialiable interface.
Is there some sort of "Serialize all properties natively except x"? If not what is one way I can make this more readable and/or less copy and pasted
public class JobInput : IJobInput, IXmlSerializable
{
public int AgencyId { get; set; }
public Guid ExternalId { get; set; }
public string Requester { get; set; }
public IInput Input { get; set; }
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
reader.MoveToContent();
reader.ReadStartElement();
if (!reader.IsEmptyElement)
{
reader.ReadStartElement("AgencyId");
var xmlSerializer = new XmlSerializer(AgencyId.GetType());
AgencyId = ((int)xmlSerializer.Deserialize(reader));
reader.ReadEndElement();
reader.ReadStartElement("ExternalId");
xmlSerializer = new XmlSerializer(ExternalId.GetType());
ExternalId = ((Guid)xmlSerializer.Deserialize(reader));
reader.ReadEndElement();
reader.ReadStartElement("Requester");
xmlSerializer = new XmlSerializer(typeof(string));
Requester = ((string)xmlSerializer.Deserialize(reader));
reader.ReadEndElement();
var type = Type.GetType(reader.GetAttribute("AssemblyQualifiedName"), true);
reader.ReadStartElement("IInput");
xmlSerializer = new XmlSerializer(type);
Input = ((IInput)xmlSerializer.Deserialize(reader));
reader.ReadEndElement();
reader.ReadEndElement();
}
}
public void WriteXml(XmlWriter writer)
{
writer.WriteStartElement("AgencyId");
var xmlSerializer = new XmlSerializer(AgencyId.GetType());
xmlSerializer.Serialize(writer, AgencyId);
writer.WriteEndElement();
writer.WriteStartElement("ExternalId");
xmlSerializer = new XmlSerializer(ExternalId.GetType());
xmlSerializer.Serialize(writer, ExternalId);
writer.WriteEndElement();
writer.WriteStartElement("Requester");
xmlSerializer = new XmlSerializer(Requester.GetType());
xmlSerializer.Serialize(writer, Requester);
writer.WriteEndElement();
writer.WriteStartElement("IInput");
writer.WriteAttributeString("AssemblyQualifiedName", Input.GetType().AssemblyQualifiedName);
xmlSerializer = new XmlSerializer(Input.GetType());
xmlSerializer.Serialize(writer, Input);
writer.WriteEndElement();
}
}
Is it possible to have a generic function that can just detect the type for all the concrete types and serialize/deserialize appropriately. I would like
something like
public void WriteXml(XmlWriter writer) {
GenericSerialize("AgencyId", AgencyId, writer);
GenericSerialize("ExternalId", ExternalId, writer);
GenericSerialize("Requester", Requester, writer);
writer.WriteStartElement("IInput");
writer.WriteAttributeString("AssemblyQualifiedName", Input.GetType().AssemblyQualifiedName);
xmlSerializer = new XmlSerializer(Input.GetType());
xmlSerializer.Serialize(writer, Input);
writer.WriteEndElement();
}
You can use [XmlAnyElement] to add an XElement []-valued property to your class that handles serialization and deserialization of properties that cannot be automatically serialized, like so:
[XmlRoot(Namespace = JobInput.XmlNamespace)]
[XmlType(Namespace = JobInput.XmlNamespace)]
public class JobInput
{
const string XmlNamespace = "";
public int AgencyId { get; set; }
public Guid ExternalId { get; set; }
public string Requester { get; set; }
[XmlIgnore]
public IInput Input { get; set; }
[XmlAnyElement]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
public XElement[] XmlCustomElements
{
get
{
var list = new List<XElement>();
if (Input != null)
list.Add(Input.SerializePolymorphicToXElement(XName.Get("Input", XmlNamespace)));
// Add others as needed.
return list.ToArray();
}
set
{
if (value == null)
return;
this.Input = value.DeserializePolymorphicEntry<IInput>(XName.Get("Input", XmlNamespace));
// Add others as needed.
}
}
}
Your standard properties will now get automatically serialized and your custom properties can be semi-automatically serialized through nested calls to XmlSerializer using the appropriate type. The following extension methods are required:
public static class XObjectExtensions
{
public static XmlSerializerNamespaces NoStandardXmlNamespaces()
{
var ns = new XmlSerializerNamespaces();
ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
return ns;
}
public static object Deserialize(this XContainer element, Type type, XmlSerializer serializer)
{
using (var reader = element.CreateReader())
{
return (serializer ?? new XmlSerializer(type)).Deserialize(reader);
}
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializerNamespaces ns, XmlSerializer serializer)
{
var doc = new XDocument();
using (var writer = doc.CreateWriter())
(serializer ?? new XmlSerializer(obj.GetType())).Serialize(writer, obj, ns);
var element = doc.Root;
if (element != null)
element.Remove();
return element;
}
const string TypeAttributeName = "AssemblyQualifiedName";
public static T DeserializePolymorphicEntry<T>(this XElement[] arrayValue, XName name)
{
var element = arrayValue.Where(e => e.Name == name).FirstOrDefault();
return element.DeserializePolymorphic<T>(name);
}
public static T DeserializePolymorphic<T>(this XElement value, XName name)
{
if (value == null)
return default(T);
var typeName = (string)value.Attribute(TypeAttributeName);
if (typeName == null)
throw new InvalidOperationException(string.Format("Missing AssemblyQualifiedName for \"{0}\"", value.ToString()));
var type = Type.GetType(typeName, true); // Throw on error
return (T)value.Deserialize(type, XmlSerializerFactory.Create(type, name));
}
public static XElement SerializePolymorphicToXElement<T>(this T obj, XName name)
{
if (obj == null)
return null;
var element = obj.SerializeToXElement(XObjectExtensions.NoStandardXmlNamespaces(), XmlSerializerFactory.Create(obj.GetType(), name));
// Remove namespace attributes (they will be added back by the XmlWriter if needed)
foreach (var attr in element.Attributes().Where(a => a.IsNamespaceDeclaration).ToList())
attr.Remove();
element.Add(new XAttribute("AssemblyQualifiedName", obj.GetType().AssemblyQualifiedName));
return element;
}
}
public static class XmlSerializerFactory
{
static readonly object padlock;
static readonly Dictionary<Tuple<Type, XName>, XmlSerializer> serializers;
// An explicit static constructor enables fairly lazy initialization.
static XmlSerializerFactory()
{
padlock = new object();
serializers = new Dictionary<Tuple<Type, XName>, XmlSerializer>();
}
/// <summary>
/// Return a cached XmlSerializer for the given type and root name.
/// </summary>
/// <param name="type"></param>
/// <param name="name"></param>
/// <returns>a cached XmlSerializer</returns>
public static XmlSerializer Create(Type type, XName name)
{
// According to https://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer%28v=vs.110%29.aspx
// XmlSerializers created with new XmlSerializer(Type, XmlRootAttribute) must be cached in a hash table
// to avoid a severe memory leak & performance hit.
if (type == null)
throw new ArgumentNullException();
if (name == null)
return new XmlSerializer(type);
lock (padlock)
{
XmlSerializer serializer;
var key = Tuple.Create(type, name);
if (!serializers.TryGetValue(key, out serializer))
serializers[key] = serializer = new XmlSerializer(type, new XmlRootAttribute { ElementName = name.LocalName, Namespace = name.NamespaceName });
return serializer;
}
}
}
Doing it this way makes your class look simpler and reduces the possibility of bugs from mistakes in implementing IXmlSerializable - but it does require a little bit of reusable infrastructure.
Prototype fiddle.
Seems what I was after was possible
public void GenericWriter<T>(ref XmlWriter writer, string propertyName, T property)
{
writer.WriteStartElement(propertyName);
var xmlSerializer = new XmlSerializer(typeof(T));
xmlSerializer.Serialize(writer, property);
writer.WriteEndElement();
}
so same for reader. I ended up using ref just in case.

Categories