Reputation: 211
I am trying to implement an abstraction for basic serialization in C# behind a custom attribute, called GenericSerializable
. Essentially, I want this attribute, when applied to a public property, to indicate to some serializer (whether it be XML, JSON, Protobuf, etc.) that that property should be serialized, and if it's absent then it should not be serialized. Currently, I can get the information of whether or not a specific property has that attribute, but I'm struggling to implement the XML serializer. Here is my test inheritance structure:
public abstract class SerializableObjectBase
{
protected int _typeIndicator;
[GenericSerializable]
public int TypeIndicator
{
get
{
return _typeIndicator;
}
}
public SerializableObjectBase()
{
_typeIndicator = 0;
}
}
public class SerializableObjectChildOne : SerializableObjectBase
{
private int _test;
public int Test
{
get
{
return _test;
}
set
{
_test = value;
}
}
public SerializableObjectChildOne() : base()
{
_test = 1234;
_typeIndicator = 1;
}
}
public class SerializableObjectChildTwo : SerializableObjectChildOne
{
private List<int> _list;
public List<int> List
{
get
{
return _list;
}
}
public SerializableObjectChildTwo() : base()
{
_list = new List<int>();
_typeIndicator = 2;
}
}
I want the XML for this example to look like:
<?xml version="1.0" encoding="utf-8"?>
<SerializableObjectChildTwo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<TypeIndicator>2</TypeIndicator>
</SerializableObjectChildTwo>
But instead it looks like this:
<?xml version="1.0" encoding="utf-8"?>
<SerializableObjectChildTwo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Test>1234</Test>
</SerializableObjectChildTwo>
Here is the serialization code:
using (FileStream fs = new FileStream(".\\output.xml", FileMode.Create))
{
// object to serialize
SerializableObjectChildTwo s = new SerializableObjectChildTwo();
XmlAttributeOverrides overrides = new XmlAttributeOverrides();
// check whether each property has the custom attribute
foreach (PropertyInfo property in typeof(SerializableObjectChildTwo).GetProperties())
{
XmlAttributes attrbs = new XmlAttributes();
// if it has the attribute, tell the overrides to serialize this property
if (property.CustomAttributes.Any((attr) => attr.AttributeType.Equals(typeof(GenericSerializable))))
{
Console.WriteLine("Adding " + property.Name + "");
attrbs.XmlElements.Add(new XmlElementAttribute(property.Name));
}
else
{
// otherwise, ignore the property
Console.WriteLine("Ignoring " + property.Name + "");
attrbs.XmlIgnore = true;
}
// add this property to the list of overrides
overrides.Add(typeof(SerializableObjectChildTwo), property.Name, attrbs);
}
// create the serializer
XmlSerializer xml = new XmlSerializer(typeof(SerializableObjectChildTwo), overrides);
// serialize it
using (TextWriter tw = new StreamWriter(fs))
{
xml.Serialize(tw, s);
}
}
Interestingly, if I add the GenericSerializable
attribute to the List
property in SerializableObjectChildTwo
, it behaves as expected. The issue is that for some reason, Test
is getting serialized despite the fact that I added attrbs.XmlIgnore = true
, and TypeIndicator
is not getting serialized despite the fact that I added it explicitly to the XmlAttributeOverrides
.
Am I using the overrides incorrectly? I don't need any fancy XML schemas or anything, I just want public properties to be serialized/not serialized based on the presence or absence of my custom property.
Thanks in advance.
Upvotes: 1
Views: 1048
Reputation: 116721
You have a few problems here:
When adding overrides for a property using XmlAttributeOverrides.Add (Type, String, XmlAttributes)
, the type
passed in must be the declaring type for the property, not the derived type being serialized.
E.g. to suppress Test
when serializing SerializableObjectChildTwo
the type
must be SerializableObjectChildOne
.
The property TypeIndicator
is not serialized because it does not have a public setter. As explained in Why are properties without a setter not serialized, in most cases a member must be publicly readable and writable to be serialized with XmlSerializer
.
That being said, a get-only collection property can be serialized by XmlSerializer
. This is explained, albeit unclearly, in Introducing XML Serialization:
XML serialization does not convert methods, indexers, private fields, or read-only properties (except read-only collections). To serialize all an object's fields and properties, both public and private, use the DataContractSerializer instead of XML serialization.
(Here read-only collection actually means read-only, pre-allocated collection property.)
This explains why the List
property is serialized despite being get-only.
You should cache the serializer to avoid a memory leak as explained in Memory Leak using StreamReader and XmlSerializer.
Putting all this together, you can construct a serializer for SerializableObjectChildTwo
using the following extension method:
public static class SerializableObjectBaseExtensions
{
static readonly Dictionary<Type, XmlSerializer> serializers = new Dictionary<Type, XmlSerializer>();
static readonly object padlock = new object();
public static XmlSerializer GetSerializer<TSerializable>(TSerializable obj) where TSerializable : SerializableObjectBase, new()
{
return GetSerializer(obj == null ? typeof(TSerializable) : obj.GetType());
}
public static XmlSerializer GetSerializer<TSerializable>() where TSerializable : SerializableObjectBase, new()
{
return GetSerializer(typeof(TSerializable));
}
static XmlSerializer GetSerializer(Type serializableType)
{
lock (padlock)
{
XmlSerializer serializer;
if (!serializers.TryGetValue(serializableType, out serializer))
serializer = serializers[serializableType] = CreateSerializer(serializableType);
return serializer;
}
}
static XmlSerializer CreateSerializer(Type serializableType)
{
XmlAttributeOverrides overrides = new XmlAttributeOverrides();
for (var declaringType = serializableType; declaringType != null && declaringType != typeof(object); declaringType = declaringType.BaseType)
{
// check whether each property has the custom attribute
foreach (PropertyInfo property in declaringType.GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance))
{
XmlAttributes attrbs = new XmlAttributes();
// if it has the attribute, tell the overrides to serialize this property
// property.IsDefined is faster than actually creating and returning the attribute
if (property.IsDefined(typeof(GenericSerializableAttribute), true))
{
Console.WriteLine("Adding " + property.Name + "");
attrbs.XmlElements.Add(new XmlElementAttribute(property.Name));
}
else
{
// otherwise, ignore the property
Console.WriteLine("Ignoring " + property.Name + "");
attrbs.XmlIgnore = true;
}
// add this property to the list of overrides
overrides.Add(declaringType, property.Name, attrbs);
}
}
// create the serializer
return new XmlSerializer(serializableType, overrides);
}
}
Working .Net fiddle here.
Upvotes: 1
Reputation: 211
I found a solution that works as expected.
This line:
overrides.Add(typeof(SerializableObjectChildTwo), property.Name, attrbs);
Should be:
overrides.Add(property.DeclaringType, property.Name, attrbs);
The difference being the type supplied as the first parameter. Thanks to @dbc for pointing me in the right direction.
Upvotes: 1