Custom XML Serialization for the .NET Compact Framework

.NET provides a whole slew of utilities for serializing objects into an XML form. But as I wrote in my previous post, .NET Compact Framework has serious problems with this serialization. The good news is that you can leverage all of the existing Attributes and tricks that you think should work (if it weren’t so buggy) and use them in your own serialization scheme.

Get Started

For example I want to know if I should skip a given member? There are a number of different things I can check. Is a Reference type null? Is there and XmlIgnore attribute? Is there a PropertyNameSpecified value set to false? All of those questions can easily be answered using reflection.


/// <summary>
/// Should the current property be skipped based on rules 
/// such as the existence of a propertySpecified value set to false?
/// </summary>
/// <param name="member">The MemberInfo to check</param>
/// <param name="o">The object that contained this member</param>
/// <returns>true if this member should be skipped</returns>
public bool SkipMember(MemberInfo member, object o)
{
    object val = null;
    if (member.MemberType == MemberTypes.Field)
    {
        val = ((FieldInfo)member).GetValue(o);
    }
    else if (member.MemberType == MemberTypes.Property)
    {
        val = ((PropertyInfo)member).GetValue(o, null);
    }

    if (null == val)
        return true;

    string propertyToTest = member.Name + "Specified";

    PropertyInfo specifiedProperty = o.GetType().GetProperty(propertyToTest);
    if ((null != specifiedProperty && !(bool)specifiedProperty.GetValue(o, null)))
        return true;

    FieldInfo specifiedField = o.GetType().GetField(propertyToTest, FIELD_BINDING_FLAGS);
    if ((null != specifiedField && !(bool)specifiedField.GetValue(o)))
        return true;

    return member.IsDefined(typeof(XmlIgnoreAttribute), false);
}

I can use a similar “fall-through” strategy to determine the name of the element to write using the XmlElement attribute for example. Now that I know I can answer some basic questions about an Object using the built-in mechanisms that .NET uses for serialization I can get down to serious serialization.

We’re all Object-Oriented programmers these days right? Right!? So to start I decided that the best way to handle this problem was to decompose it into a bunch of simpler problems.

ITagWriter

There are two things that we can write in XML. Either an XML Element or an XML Attribute. So, I created an interface ITagWriter with two concrete implementations to correspond to these two XML types: AttributeTagWriter and ElementTagWriter. These classes allow me to write the structure of the XML Document.


/// <summary>
/// Interface to implement to write different Xml tags
/// Either Elements or Attributes.
/// </summary>
internal interface ITagWriter
{
    /// <summary>
    /// Write the opening Xml tag with the given name
    /// </summary>
    /// <param name="doc">The XML Document to write the tage to.</param>
    /// <param name="tagName">The name of the tag</param>
    void WriteStart(XmlWriter doc, string tagName);

    /// <summary>
    /// Write the appropriate end tag
    /// </summary>
    /// <param name="doc">The XML Document to write the tage to.</param>
    void WriteEnd(XmlWriter doc);
}

IValueWriter

With the ability to write the structure, I then need to be able to write out the values of the various objects and their properties. Just like with the ITagWriter interface, I decided to create an IValueWriter for the various kinds of values that I would need to write. The types I came up with were ObjectWriter, CollectionValueWriter, EnumValueWriter, SimpleValueWriter, and XmlElementValueWriter.


/// <summary>
/// Interface to implement to write different kinds of values.
/// </summary>
internal interface IValueWriter
{
    /// <summary>
    /// Write the Entry value to the XmlDocument
    /// </summary>
    /// <param name="doc">The XML Document to write the tage to.</param>
    /// <param name="entry">The meta-information and value to write.</param>
    void Write(XmlWriter doc, CustomSerializationEntry entry);
}

You’ll notice the CustomSerializationEntry class is the parameter for the IValueWriter.Write() method. This class contains all of the metadata and the value about the various properties of an Object. This alows us an easy way to ask questions about a given property. Is it a Collection? Is it an Enum? Is there a sort order? Basically the idea is to encapsulate all of the things that are interesting from a serialization point of view.

To help manage the interaction I also created a basic TypeLookup class. The job of this class is to determine what type of ITagWriter and IValueWriter to use for a given CustomSerializationEntry instance. This allows us to centralize that decision making in a single class. The centralized knowledge keeps the individual writer implementations much simpler. They just need to ask for the correct writer and then call the methods defined in the interface. They don’t need to care what type they are writing. All hail power of encapsulation and abstraction!

Start Serializing

I bootstrap the serialization by creating an ObjectWriter to handle the outermost object. From there, the ObjectWriter takes over, constructing CustomSerializationEntry objects for each of the serialized object’s properties. The type of the property determines the type of IValueWriter that is used to write the property value.


/// <summary>
/// Serialize an object using the given 
/// <paramref name="xmlRoot">xmlRoot</paramref> as the root element name.
/// </summary>
/// <param name="o"></param>
/// <param name="xmlRoot"></param>
/// <returns></returns>
public string Serialize(object o, string xmlRoot)
{
    StringBuilder sb = new StringBuilder();
    using (XmlTextWriter writer = new XmlTextWriter(new StringWriter(sb)))
    {
        writer.Formatting = Formatting.Indented;

        XmlWriter xmlDoc = XmlWriter.Create(writer);
        WriteRootElement(xmlDoc, o, xmlRoot);
    }

    return sb.ToString();
}

private static void WriteRootElement(XmlWriter doc, object o, string rootElement)
{
    doc.WriteStartDocument();

    ObjectWriter writer = new ObjectWriter(new TypeLookup());
    writer.Write(doc, o, rootElement);

    doc.WriteEndDocument();
}

The ObjectWriter itself creates a CustomSerializationEntry for all the properties that should be written. It then loops over the properties. Notice how it uses the TypeLookup (lookup) to ask for the proper value writer for each of the properties.


// ...
        public void Write(XmlWriter doc, object o, string elementName)
        {
            doc.WriteStartElement(elementName);
            IEnumerable<CustomSerializationEntry> entries = GetMemberInfo(o);
            foreach (CustomSerializationEntry currentEntry in entries)
            {
                lookup.GetValueWriter(currentEntry).Write(doc, currentEntry);
            }
            doc.WriteEndElement();
        }
// ...

Conclusion

OK, so I left out a lot of details! But If I gave you all of the answers it wouldn’t be any fun now would it. I hope you can see how decomposing the problem of serialization turns it a series of relatively simple problems that you can answer. So, if I can do this in about 500 lines of code, how come Microsoft can’t implement a decent XML Serializer for the .NET Compact Framework.

9 Responses to “Custom XML Serialization for the .NET Compact Framework”

  1. .NET Compact Framework Serialization Bugs writes:

    [...] follow-up post on Writing a Custom XML Serializer has been [...]

  2. Andrew Arnott writes:

    So what are the bugs in NetCF’s serializer that you ran into? And have you tried CF 2.0 SP2 to see if the problems were fixed?

  3. Geoff Lane writes:

    Andrew,
    See the linked post inline to here to read about the problems I ran into with XmlEnum and PropertySpecified. Luckily both of these bugs have been fixed in the .NET CF 2.0 SP2 release.

  4. Coder12 writes:

    What is the point of using XML limited serializer if you can get the real fast binary one
    http://www.freewebs.com/compactFormatter/links.html
    http://www.codeproject.com/KB/cs/CF_serializer.aspx

  5. Geoff Lane writes:

    Coder12,
    The point is that the entire world does not run .NET and if you are going to be sending data to a remote service, you’ll need to interoperate with said service and its existing contract.

  6. Coder12 writes:

    Geoff wrote:
    Coder12,
    The point is that the entire world does not run .NET and if you are going to be sending data to a remote service, you’ll need to interoperate with said service and its existing contract.

    You are wrong Geoff,
    The XML serializer is as useless as Binary one on different platform because there is NO STANDARDS on the serialization/deserialization. Only SOAP – which is very inefficient and limited. Using XML serializer on another platform only allows for adjusting your MANUALLY built serializer because the stream (text) can be read. But you can do the same with a binary one. The only difference is that the stream can not be viewed by a primitive text editor. That is a common misconception.

  7. Geoff Lane writes:

    Coder12,
    I was serializing to XML that was being consumed by a Java service. This is a real application where the Java service existed prior to the .NETCF client. And it works. To claim that it useless for interoperation seems to fly in the face of the fact that this is a real application. XML is a standard supported by many languages. Making your objects serialize to a specific schema allows it to be consumed by any language that could understand that schema. The XML serialization gives you control over the format that comes out, not because it’s ASCII based, but because it’s a configurable through declarative Attributes. So I can make my objects output XML that matches that existing schema.

    I think you are considering serializing only as a means of freezing and reconstituting objects. In that case, sure that can’t work across languages. But serializing or marshaling is just representing object data and structure in a different format. In this case an XML format is one that can be used by other tools much more easily than a binary one.

  8. Felipe Roos writes:

    Hi Geoff,

    I’ve trying this serialization code with TcpListener and TcpClient to interprocess communication in a same pocket pc.
    Both client and server sockets seems to be ok, but when debugging they seems to be in deadlock. A small code excerpt from client and server is below:
    //server:
    TcpListener listener = new TcpListener(IPAddress.Loopback, port);
    listener.Start();
    TcpClient myClient = listener.AcceptTcpClient();
    XmlSerializer stringSerial = new XmlSerializer(typeof(string));
    string methodName = (string)stringSerial.Deserialize(netStream);
    XmlSerializer boolSerial = new XmlSerializer(typeof(bool));
    result = true;
    boolSerial.Serialize(netStream, result);
    listener.Stop();
    //client
    TcpClient client = new TcpClient(IPAddress.Loopback.ToString(), port);
    NetworkStream netStream = client.GetStream();
    XmlSerializer stringSerial = new XmlSerializer(typeof(string));
    string metName = “SomeString”;
    stringSerial.Serialize(netStream, metName);
    bool resultOk = (bool)boolSerial.Deserialize(netStream);

    What happens is, Serialize from client happens and then it ways for Deserialize. However, server nevers identify that the Serialize was made, and waits forever on the Deserialize.
    Is this the kind of problem you refer when working with serialization on .Net Framework?

    Regards,

  9. Dere writes:

    Felipe,

    I was trying to do the same thing and discovered that after you call the Serialize function, you must call Close on the stream. For example, your code would be:

    XmlSerializer stringSerial = new XmlSerializer(typeof(string));
    string metName = “SomeString”;
    stringSerial.Serialize(netStream, metName);
    netStream.Close();

    That will allow the server to finish the read. It appears to be waiting for the stream to finish before finalizing the deserialization on the server’s end.

Leave a Reply