This project is read-only.

Simplex Xml Serializer Compiler

In this exercise, we will build a mini compiler that takes a Type annotated with some System.Xml.Serialization attributes and generates a lightweight serializer on the fly.
What we want to do...
For the sake of simplicity we will support properties tagged with XmlAttributeAttribute and XmlElementAttribute, in which cases the property can be an array as well. Let's this with an example:

public class Foo
{
    [XmlAttribute]
    public string Name {get;set;}

    [XmlElement]
    public Bar MyBar {get;set;}
}

    public class Bar
    {
        [XmlElement]
        public Foo[] Foes {get;set;}
    }


Given a little object tree, we'll expect the serializer to produce a nice xml output:

Foo foo = new Foo();
foo.Name = "Hello";
foo.MyBar = new Bar();
foo.MyBar.Foes = new Foo[]{ new Foo(), new Foo(), null };
foo.MyBar.Foes[0].Name = "foo1";
foo.MyBar.Foes[1].Name = "foo2";

gives
<Foo Name="Hello">
  <Bar>
    <Foes>
      <Foo Name="foo1" />
      <Foo Name="foo2" />
    </Foes>
  </Bar>
</Foo>

Designing the serializer

Given the type Foo, we expect the compiler to generate a method for each type Foo and Bar that works directly on a XmlWriter:

void WriteFoo(XmlWriter writer, Foo target)
{
    writer.WriteStartElement("Foo");
    writer.WriteAttributeString("Name", target.Name);
    if (target.Bar != null)
        WriteBar(writer, target.Bar);
    writer.WriteEndElement();
}

void WriteBar(XmlWriter writer, Bar bar)
{
    writer.WriteStartElement("Bar");
    if (bar.Foes!=null)
    {
        writer.WriteStartElements("Foes");
        foreach(Foo foo in bar.Foes)
        {  
            if (foo!=null)
                WriteFoo(writer, foo); 
        }
        writer.WriteEndElement();
    }
    writer.WriteEndElement();
}

Building the compiler

The compilation process is a 2-pass algorithm:
  • collect the list of types that might be serialized by walking the property types. For each type, define new {{WriteXXX} method,
  • bake the body for each WriteXXX method

The full source of this sample is in the source distribution. Let's focus on interresting parts, i.e. generating the method bodies.
  • Write a property as argument:
    • fetch the property value,
    • if the property is not a string, convert the value to string using XmlConvert,
    • invoke XmlWriter.WriteAttributeString
PropertyInfo property = ...;
XmlAttributeAttribute xmlAttribute = Attribute.GetCustomAttribute(property, typeof(XmlAttributeAttribute)) 
    as XmlAttributeAttribute; // is this an attribute?
if (xmlAttribute!=null)
{
    // var value = target.Foo;
    Expression pvalue= Expr.Param(value).GetProperty(property);
    if (property.PropertyType != typeof(string))
    {
        // WriteAttributeString works with string
        // string value = XmlConvert.ToString(target.Foo);
        pvalue = Expr.InvokeMethod(
            typeof(XmlConvert), "ToString", BindingFlags.Public | BindingFlags.Static, pvalue);
     }

     // writer.WriteAttributeString("Foo", XmlConvert.ToString(target.Foo);
     body.Statements.Add(
         Expr.Param(writer).InvokeMethod(
             "WriteAttributeString", Expr.Prim(property.Name), pvalue)
         );
}
  • Write an element:
    • if the element is not null, call the corresponding WriteXXX method:
// types is a dictionary containing the WriteXXX methods
MethodFactoryBase method = this.types[property.PropertyType];

// WriteFoo(writer, value.Foo); 
MethodInvokeExpression invoke = new MethodInvokeExpression(
    null, // static method
    (MethodInfo)method.Method,
    method.Parameters.GetParameterTypes());
invoke.Arguments.Add(Expr.Param(writer));
invoke.Arguments.Add(Expr.Param(value).GetProperty(property));

// if (value.Foo != null) WriteFoo(writer, value.Foo);
body.Statements.Add(
    Stm.If(
        Expr.Inequality(Expr.Param(value).GetProperty(property), Expr.Null),
        Stm.Expr(invoke)
        ) // If
    ); // Add

  • Write an array of elements:
    • check that the array is not null,
    • create a foreach loop that invokes the corresponding WriteXXX method. Even better, Toad supports adding a condition on the foreach (i.e. foreach ... where) so we can filter out null elements in the array:
//  foreach(Foo foo in target.Foes where foo != null)
ForEachStatement fe = Stm.ForEach(
    property.PropertyType.GetElementType(),
    "item",
    Expr.Param(value).GetProperty(property));
// where x != null                               
fe.WhereExpression = Expr.Inequality(Expr.Var(fe.Variable), Expr.Null);

// WriteFoo(writer, foo);    
MethodFactoryBase method = this.types[property.PropertyType.GetElementType()];
MethodInvokeExpression invoke = new MethodInvokeExpression(...);
invoke.Arguments.Add(Expr.Param(writer));
invoke.Arguments.Add(Expr.Var(fe.Variable));

// foreach(...) { WriteFoo(writer, foo); }
fe.Body = Stm.Expr(invoke);

// if (value.Foes != null) { foreach... }
body.Statements.Add(
    Stm.If(
        Expr.Inequality(Expr.Param(value).GetProperty(property), Expr.Null),
        fe
        )
    );


That's it. Glue the pieces together and let Toad bake the IL for you :)

Last edited May 20, 2007 at 6:00 PM by pelikhan, version 9

Comments

No comments yet.