Thursday, January 19, 2012

Using expression trees to denote models

Let’s have an XML list of people:

<?xml version="1.0" encoding="utf-8" ?>
<people>
    <person GivenName="John" Surname="Smith" Age="30" />
    <person GivenName="Matthew" Surname="Kowalski" Age="35" />
</people>

and an object model of list items:

public class Person
{
    public string GivenName { get; set; }
    public string Surname { get; set; }
    public int    Age { get; set; }
 
    public override string ToString()
    {
        return string.Format( "{0} {1}, {2}", GivenName, Surname, Age );
    }
}

What we can see here is the mismatch between two domains – a strongly typed OO domain and loosely typed text domain. The mismatch can be resolved using different approaches, for example we could build a set of tags over class members to help the XmlSerializer to deal with plain text data. But trying to be as naive as possible we could provide a helper method which, for given XmlNode, would return a typed value of an attribute.

public static class XmlNodeExtensions
 {
     public static T GetNodeAttribute<T>( this XmlNode Node, string AttributeName )
     {
         if ( Node != null )
             return (T)Convert.ChangeType( Node.Attributes[AttributeName].Value, typeof( T ) );
         else
             return default( T );
     }
 }

The conversion of untyped text data to typed class instances require the knowledge of member signatures:

XmlDocument people = new XmlDocument();
people.Load( "people.xml" );
 
foreach ( XmlNode personNode in people.SelectNodes( "//people/person" ) )
{
    Person person = new Person();
 
    person.GivenName = personNode.GetNodeAttribute<string>( "GivenName" );
    person.Surname   = personNode.GetNodeAttribute<string>( "Surname" );
    person.Age       = personNode.GetNodeAttribute<int>( "Age" );
 
    Console.WriteLine( person );
}

Although such naive approach is common, it has obvious disadvantages. Basically we duplicate the model metadata description on both sides of assignments. The mismatch is still there waiting to bite when models change – if names/types change, we’d have to apply manual changes on one/both sides of above assignments. In particular, refering to members using literal names could possibly bite badly as there’s no chance to catch possible mismatch during the compilation.

The intention of this entry is to document another common approach, a one step further than what is presented above. Note once again that each assignment of a form:

person.GivenName = personNode.GetNodeAttribute<string>( "GivenName" );

contains the same information twice – the name and type of of a class member matches the name of XML attribute, however we have to repeat this information on the right side of the assignment.

Let’s then provide a new extension:

public static class XmlNodeExtensions
 {
     public static T GetNodeAttribute<T>( this XmlNode Node, string AttributeName )
     {
         ... unchanged ...
     }
 
     public static TProperty GetNodeAttribute<TProperty>(
          this XmlNode Node,
          Expression<Func<TProperty>> Property )
     {
         if ( Property != null && Property.Body != null )
             if ( Property.Body.NodeType == ExpressionType.MemberAccess )
             {
                 MemberExpression memberExpression = 
                    (MemberExpression)Property.Body;
 
                 if ( !string.IsNullOrEmpty( memberExpression.Member.Name ) )
                     return Node.GetNodeAttribute<TProperty>( 
                                memberExpression.Member.Name );
             }
 
         return default(TProperty);
     }
 }

This time we require an expression denoting model member, rather than a separate information of its name and type. This allows us to rewrite the client code to:

XmlDocument people = new XmlDocument();
 people.Load( "people.xml" );
 
 foreach ( XmlNode personNode in people.SelectNodes( "//people/person" ) )
 {
     Person person = new Person();
 
     person.GivenName = personNode.GetNodeAttribute( () => person.Surname );
     person.Surname   = personNode.GetNodeAttribute( () => person.GivenName );
     person.Age       = personNode.GetNodeAttribute( () => person.Age );
 
     Console.WriteLine( person );
 }

The possibilities of further enhancements are endless and even such simple one is a significant one as possible mismatch between models can now be caught by the compiler.

1 comment:

Stacey said...

Wonderful, I’ve been looking for similar information for a while. Thank you. Regards - BlackBerry Application Development