Sunday 20 June 2010

A Strongly Typed Expand Extension for WCF Data Services Clients

For anyone that's used WCF Data Services (formerly ADO.NET Data Services), I guess you'll have run into the fact that using the Expand function for eager loading tables referenced by foreign keys uses a string as its overhead parameter:
DataServiceQuery<Car> query = myDataServiceContext.Cars.Expand( "Wheels" );
I was always really confused with this. This is what it's like in Entity Framework 4.0 as well with the Include function. Using a string literal of course means that schema changes will not be picked up at compile time. The code will successfully be compiled if the "Wheels" table has changed its name for instance, and the problem only gets picked up in the testing phase.


A type safe approach

So here's the code that I use. It's an extension class, so you can carry on using your existing data context object, and is not overtly "code-intrusive". It provides the ability to use strongly typed expressions instead of magic strings. When your schema changes, a compile error will be raised displaying that the foreign table name is no longer correct. This will of course make an automated build fail, which is exactly what we want.
using DataServiceQueryExpressions;
...
DataServiceQuery<Car> query = myDataServiceContext.Cars.Expand( car => car.Wheels );
Note that you can also use the extension for generating cascading expand URIs, along with the ability to specify foreign key references across data collection boundaries, as shown below:
DataServiceQuery<Car> query = myDataServiceContext.Cars;
query = query.Expand( car => car.Engine );
query = query.Expand( car => car.Engine.Manfucturer );
query = query.Expand( car => car.Wheels );
query = query.Expand( car => car.Wheels.First().Tyre );

Source code

Copy and paste the following code into a new class (.cs) file. Use the namespace from your business logic layer, and start expanding safely.
// Rab Hallett 2009. Public Domain.
// Whilst I'd appreciate if you could keep my name at the top of this source code,
// I won't be mad at you if you rip it off and pass it off to your boss as your own.
using System;
using System.Text;
using System.Linq.Expressions;
using System.Data.Services.Client;

namespace DataServiceQueryExpressions
{
    /// <summary>
    /// Extension for the <code>DataServiceQuery</code> class that supplies an 
    /// override for the <code>Expand</code> function, allowing query expand 
    /// paths to be specified by strongly typed expressions as opposed to 
    /// magic strings.
    /// <example>
    ///     myObjectContext.MyNamedEntityCollection.Expand( x => x.ForeignKeyTable );
    /// </example>
    /// </summary>
    public static class ExpressionsExtension
    {
        /// <summary>
        /// Specifies the related objects to include in the query results.
        /// </summary>
        /// <returns>A new System.Data.Objects.ObjectQuery<T> with the defined 
        /// query path.</returns>
        public static DataServiceQuery<T> Expand<T>
            (this DataServiceQuery<T> parent, Expression<Func<T, object>> expression)
        {
            if ( expression.Body.NodeType != ExpressionType.MemberAccess )
            {
                throw new ArgumentException( "expected a member access node for the " +
                    "expression body." );
            }

            const char PathSeparator = '/';

            // Get the last element of the include path
            MemberExpression expressionBody = (MemberExpression)expression.Body;
            StringBuilder path = new StringBuilder();            
            
            do
            {
                // Build the next component of the include path
                if ( path.Length > 0 )
                {
                    path.Insert( 0, PathSeparator );
                }
                path.Insert( 0, expressionBody.Member.Name );

                // Get the next expression node; throw if not a call or method type
                Expression nextNode = expressionBody.Expression;
                if ( nextNode.NodeType != ExpressionType.Call && 
                     nextNode.NodeType != ExpressionType.MemberAccess &&
                     nextNode.NodeType != ExpressionType.Parameter  )
                {
                    throw new InvalidOperationException( 
                        "Unsupported node specified in expression: " + nextNode.NodeType );
                }

                // Skip any superfluous method calls (like First()): these are required
                // for including through collection (eg. MyARecords.First().MyBRecord)                
                while ( nextNode.NodeType == ExpressionType.Call )
                {
                    nextNode = ( (MethodCallExpression)nextNode ).Arguments[ 0 ];
                }
                
                // Get the next member expression, if any
                expressionBody = nextNode as MemberExpression;
            }
            while ( expressionBody != null );

            // Call the underlying framework function with the path string
            return parent.Expand( path.ToString() );
        }
    }
}

5 comments:

  1. This is great stuff. Thanks Rab.

    ReplyDelete
  2. I'm glad someone appreciated it... it was so niche I wondered if anyone would read this page.

    ReplyDelete
  3. Well done. Thanks for putting that together.

    ReplyDelete
  4. How can we unit test this extension method? I want to ensure that no-one in my team inadvertently breaks it while trying to amend it? Thank you.

    ReplyDelete