XML Serialization Issues or Having Fun with TimeSpan and XmlIgnore
Recently I tackled a seemingly simple task: XML serialization of a generic class used with a TimeSpan
data type. Basically I had a situation similar to the following two classes:
// the generic class used
public class GenericClass<T> where T: new()
{
public virtual T Value { get; set; }
public GenericClass()
{
Value = new T();
}
}
// the class for XML serialization
public class Configuration
{
public GenericClass<TimeSpan> Time { get; set; }
public Configuration()
{
Time = new GenericClass<TimeSpan>();
}
}
The only remaining thing to do should be calling the XmlSerializer.Serialize
method:
Configuration config = new Configuration();
XmlSerializer serializer = new XmlSerializer(typeof(Configuration));
using (FileStream stream = new FileStream("configuration.xml", FileMode.Create))
{
serializer.Serialize(stream, config);
}
Though, the result wasn't what I expected:
<?xml version="1.0"?>
<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Time>
<Value />
</Time>
</Configuration>
Where did the value go? After some exploration it turned out that XmlSerializer
doesn't serialize the TimeSpan
structure at all. Unfortunately the suggested standard workaround by hiding the property with XmlIgnore
attribute and adding an additional property of the desired type can't be used in the above scenario because it has to be applied to the generic class only when it is used with TimeSpan
data type. The problem also can't be solved by deriving a class from TimeSpan
and fixing its XML serialization because it is a structure which doesn't support inheritance. Well, it looked like I'll just have to derive a new class from GenericClass
for the case of TimeSpan
:
public class TimeSpanClass : GenericClass<TimeSpan>
{
[XmlIgnore]
public override TimeSpan Value
{
get
{
return base.Value;
}
set
{
base.Value = value;
}
}
[XmlElement(ElementName="Value")]
public string StringValue
{
get
{
return Value.ToString(@"hh\:mm\:ss");
}
set
{
Value = TimeSpan.ParseExact(value,
@"hh\:mm\:ss", CultureInfo.InvariantCulture);
}
}
}
This should work, right? Wrong. The following exception is thrown when initializing XmlSerializer
for the modified Configuration
class now containing TimeSpanClass
instead of GenericClass<TimeSpan>
:
The XML element 'Value' from namespace '' is already present in the current scope. Use XML attributes to specify another XML name or namespace for the element.
How come? Well the XmlIgnore
attribute doesn't have any effect when used in a derived class on an overridden property. If you remove the XmlElement
attribute from the StringValue
property, you'll see that both properties will get serialized:
<?xml version="1.0"?>
<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Time>
<Value />
<StringValue>00:00:00</StringValue>
</Time>
</Configuration>
So, is there really no way to get this working? There is, but only if you do your own serialization by implementing IXmlSerializable
interface on the TimeSpanClass
:
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
public void ReadXml(System.Xml.XmlReader reader)
{
reader.ReadStartElement();
Value = TimeSpan.ParseExact(
reader.ReadElementContentAsString("Value", String.Empty),
@"hh\:mm\:ss",
CultureInfo.InvariantCulture);
reader.ReadEndElement();
}
public void WriteXml(System.Xml.XmlWriter writer)
{
writer.WriteElementString("Value", Value.ToString(@"hh\:mm\:ss"));
}
This will finally produce the desired serialization:
<?xml version="1.0"?>
<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Time>
<Value>00:00:00</Value>
</Time>
</Configuration>
Writing your own XML serialization logic pretty much defeats the purpose of having XmlSerializer
class available in the first place. It's not that difficult in this simple case but it can get quite tedious if you have to write it for a more complex class. It's definitely not something I'm looking forward to writing, but if it's the most elegant way to get the job done... Since the problem is already present in .NET framework for almost a decade, it's a pretty safe bet it won't get fixed any time soon if ever.