Tuesday, 17 June 2008

Working with Objects in SSDS Part 1

Last time we talked about SQL Server Data Services and serializing objects, we discussed how easy it was to use the XmlSerializer to deserialize objects using the REST interface.  The problem was that when we serialized objects using the XmlSerializer, it left out the xsi type declarations that we needed.  I gave two possible solutions to this problem - one that used the XmlSerializer and 'fixed' the output after the fact, and the other built the XML that we needed using XLINQ and Reflection.

Today, I am going to talk about a third technique that I have been using lately that I like better.  It uses some of the previous techniques and leverages a few tricks with XmlSerializer to get what I want.  First, let's start with a POCO (plain ol' C# object) class that we would like to use with SSDS.

public class Foo
{
    public string Name { get; set; }
    public int Size { get; set; }
    public bool IsPublic { get; set; }
}

In it's correctly serialized form, it looks like this on the wire:

<Foo xmlns:s="http://schemas.microsoft.com/sitka/2008/03/"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:x="http://www.w3.org/2001/XMLSchema">
  <s:Id>someid</s:Id>
  <s:Version>1</s:Version>
  <Name xsi:type="x:string">My Foo</Name>
  <Size xsi:type="x:decimal">10</Size>
  <IsPublic xsi:type="x:boolean">false</IsPublic>
</Foo>

You'll notice that we have the additional system metadata attributes "Id" and "Version" in the markup.  We can account for the metadata attributes by doing something cheesy like deriving from a base class:

public abstract class Cheese
{
    public string Id { get; set; }
    public int Version { get; set; }
}

However this is very unnatural as our classes would all have to derive from our "Cheese" abstract base class (ABC).

public class Foo : Cheese
{
    public string Name { get; set; }
    public int Size { get; set; }
    public bool IsPublic { get; set; }
}

Developers familiar with remoting in .NET should be cringing right now as they remember the hassles associated with deriving from MarshalByRefObject.  In a world without multiple inheritance, this can be painful.  I want a model where I can use arbitrary POCO objects (redundant, yes I know) and not be forced to derive from anything or do what I would otherwise term unnatural acts.

What if instead, we derived a generic entity that could contain any other entity?

public class SsdsEntity<T> where T: class
{
    string _kind;

    public SsdsEntity() { }

    [XmlElement(Namespace = @"http://schemas.microsoft.com/sitka/2008/03/")]
    public string Id { get; set; }

    [XmlIgnore]
    public string Kind
    {
        get
        {
            if (String.IsNullOrEmpty(_kind))
            {
                _kind = typeof(T).Name;
            }
            return _kind;
        }
        set
        {
            _kind = value;
        }
    }

    [XmlElement(Namespace = @"http://schemas.microsoft.com/sitka/2008/03/")]
    public int Version { get; set; }

    [XmlIgnore]
    public T Entity { get; set; }
}

In this case, we have simply wrapped the POCO that we care about in a class that knows about the specifics of the SSDS wire format (or more accurately could serialize down to the wire format).

This SsdsEntity<T> is easy to use and provides access to the strongly typed object via the Entity property.

foomembers

Now, we just have to figure out how to serialize the SsdsEntity<Foo> object and we know that the metadata attributes are taken care of and our original POCO object that we care about is included.  I call it wrapping POCOs in a thin SSDS veneer.

The trick to this is to add a bucket of XElement objects on the SsdsEntity<T> class that will hold our public properties on our class T (i.e. 'Foo' class).  It looks something like this:

[XmlAnyElement]
public XElement[] Attributes
{
    get
    {
        //using XElement is much easier than XmlElement to build
        //take all properties on object instance and build XElement
        var props =  from prop in typeof(T).GetProperties()
                     let val = prop.GetValue(this.Entity, null)
                     where prop.GetSetMethod() != null
                     && allowableTypes.Contains(prop.PropertyType)
                     && val != null
                     select new XElement(prop.Name,
                         new XAttribute(Constants.xsi + "type",
                            XsdTypeResolver.Solve(prop.PropertyType)),
                         EncodeValue(val)
                         );

        return props.ToArray();
    }
    set
    {
        //wrap the XElement[] with the name of the type
        var xml = new XElement(typeof(T).Name, value);

        var xs = new XmlSerializer(typeof(T));

        //xml.CreateReader() cannot be used as it won't support base64 content
        XmlTextReader reader = new XmlTextReader(
            xml.ToString(),
            XmlNodeType.Document,
            null);

        this.Entity = (T)xs.Deserialize(reader);
    }
}

In the getter, we use Reflection and pull back a list of all the public properties on the T object and build an array of XElement.  This is the same technique I used in my first post on serialization.  The 'allowableTypes' object is a HashSet<Type> that we use to figure out which property types we can support in the service (DateTime, numeric, string, boolean, and byte[]).  When this property serializes, the XElements are simply added to the markup.

The EncodeValue method shown is a simple helper method that correctly encodes string values, boolean, dates, integers, and byte[] values for the attribute.  Finally, we are using a helper method that returns from a Dictionary<Type,string> the correct xsi type for the required attribute (as determined from the property type).

For deserialization, what happens is that the [XmlAnyElement] attribute causes all unmapped attributes (in this case, all non-system metadata attributes) to be collected in a collection of XElement.  When we deserialize, if we simply wrap an enclosing element around this XElement collection, it is exactly what we need for deserialization of T.  This is shown in the setter implementation.

It might look a little complicated, but now simple serialization will just work via the XmlSerializer.  Here is one such implementation:

public string Serialize(SsdsEntity<T> entity)
{
    //add a bunch of namespaces and override the default ones too
    XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces();
    namespaces.Add("s", Constants.ns.NamespaceName);
    namespaces.Add("x", Constants.x.NamespaceName);
    namespaces.Add("xsi", Constants.xsi.NamespaceName);

    var xs = new XmlSerializer(
        entity.GetType(),
        new XmlRootAttribute(typeof(T).Name)
        );

    XmlWriterSettings xws = new XmlWriterSettings();
    xws.Indent = true;
    xws.OmitXmlDeclaration = true;

    using (var ms = new MemoryStream())
    {
        using (XmlWriter writer = XmlWriter.Create(ms, xws))
        {
            xs.Serialize(writer, entity, namespaces);
            ms.Position = 0; //reset to beginning

            using (var sr = new StreamReader(ms))
            {
                return sr.ReadToEnd();
            }
        }
    }
}

Deserialization is even easier since we are starting with the XML representation and don't have to build a Stream in memory.

public SsdsEntity<T> Deserialize(XElement node)
{
    var xs = new XmlSerializer(
        typeof(SsdsEntity<T>),
        new XmlRootAttribute(typeof(T).Name)
        );

    //xml.CreateReader() cannot be used as it won't support base64 content
    XmlTextReader reader = new XmlTextReader(
        node.ToString(),
        XmlNodeType.Document,
        null);
    
    return (SsdsEntity<T>)xs.Deserialize(reader);
}

If you notice, I am using an XmlTextReader to pass to the XmlSerializer.  Unfortunately, the XmlReader from XLINQ does not support handling of base64 content, so this workaround is necessary.

At this point, we have a working serializer/deserializer that can handle arbitrary POCOs.  There are some limitations of course:

  • We are limited to the same datatypes that SSDS supports.  This also means nested objects and arrays are not directly supported.
  • We have lost a little of the 'flexible' in the Flexible Entity (the E in the ACE model).  We now have a rigid schema defined by SSDS metadata and T public properties and enforced on our objects.

In my next post, I will attempt to address some of those limitations and I will introduce a library that handles most of this for you.

Comments are closed.