Getting rid of the magic strings in a WCF Data Service Client

One of the common problems that you might find when using the generated DataServiceContext for consuming an existing WCF data service is that you have magic strings everywhere for handling links (expanding, adding, deleting, etc). The problem with all those magic strings is that they make your code to compile correctly, but you might run into some error at runtime because you used a link that does not exist or it was just renamed.

To give an example, this is how you expand the items associated to an order using the traditional way with magic strings.

context.Orders.Expand(“Items”)

If that property is later renamed to “OrderItems” on the service side, and the proxy is updated, that code will still compile but you will receive an error at runtime.

Fortunately, you can get rid of those “magic” strings and making the compiler your friend by leveraging Expression trees. The expression trees will make your code easier to maintain, and what’s more important, the compiler will verify the expressions correctness at compilation time.

Continuing what Stuart Leeks did for link expansions with expressions, I added a few more methods for managing links. 

namespace System.Data.Services.Client
{
    public static class DataServiceExtensions
    {
        public static void SetLink<TSource, TPropType>(this DataServiceContext context, TSource source, 
            Expression<Func<TSource, TPropType>> propertySelector, object target)
        {
            string expandString = BuildString(propertySelector);
            context.SetLink(source, expandString, target);
        }
 
        public static void AddLink<TSource, TPropType>(this DataServiceContext context, 
            TSource source, Expression<Func<TSource, TPropType>> propertySelector, object target)
        {
            string expandString = BuildString(propertySelector);
            context.AddLink(source, expandString, target);
        }
 
        public static void DeleteLink<TSource, TPropType>(this DataServiceContext context, 
            TSource source, Expression<Func<TSource, TPropType>> propertySelector, object target)
        {
            string expandString = BuildString(propertySelector);
            context.DeleteLink(source, expandString, target);
        }
 
        public static void LoadProperty<TSource, TPropType>(this DataServiceContext context, 
            TSource source, Expression<Func<TSource, TPropType>> propertySelector)
        {
            string expandString = BuildString(propertySelector);
            context.LoadProperty(source, expandString);
        }
 
        public static DataServiceQuery<TSource> Expand<TSource, TPropType>(this DataServiceQuery<TSource> source, 
            Expression<Func<TSource, TPropType>> propertySelector)
        {
            string expandString = BuildString(propertySelector);
            return source.Expand(expandString);
        }
 
        private static string BuildString(Expression propertySelector)
        {
            switch (propertySelector.NodeType)
            {
                case ExpressionType.Lambda:
                    LambdaExpression lambdaExpression = (LambdaExpression)propertySelector;
                    return BuildString(lambdaExpression.Body);
 
                case ExpressionType.Quote:
                    UnaryExpression unaryExpression = (UnaryExpression)propertySelector;
                    return BuildString(unaryExpression.Operand);
 
                case ExpressionType.MemberAccess:
                    MemberInfo propertyInfo = ((MemberExpression)propertySelector).Member;
                    return propertyInfo.Name;
 
                case ExpressionType.Call:
                    MethodCallExpression methodCallExpression = (MethodCallExpression)propertySelector;
                    if (IsSubExpand(methodCallExpression.Method)) // check that it's a SubExpand call
                    {
                        // argument 0 is the expression to which the SubExpand is applied (this could be member access or another SubExpand)
                        // argument 1 is the expression to apply to get the expanded property
                        // Pass both to BuildString to get the full expression
                        return BuildString(methodCallExpression.Arguments[0]) + "/" +
                               BuildString(methodCallExpression.Arguments[1]);
                    }
                    // else drop out and throw
                    break;
            }
            throw new InvalidOperationException("Expression must be a member expression or an SubExpand call: " + propertySelector.ToString());
 
        }
 
        private static readonly MethodInfo[] SubExpandMethods;
        static DataServiceExtensions()
        {
            Type type = typeof(DataServiceExtensions);
            SubExpandMethods = type.GetMethods().Where(mi => mi.Name == "SubExpand").ToArray();
        }
        private static bool IsSubExpand(MethodInfo methodInfo)
        {
            if (methodInfo.IsGenericMethod)
            {
                if (!methodInfo.IsGenericMethodDefinition)
                {
                    methodInfo = methodInfo.GetGenericMethodDefinition();
                }
            }
            return SubExpandMethods.Contains(methodInfo);
        }
 
        public static TPropType SubExpand<TSource, TPropType>(this Collection<TSource> source, Expression<Func<TSource, TPropType>> propertySelector)
            where TSource : class
            where TPropType : class
        {
            throw new InvalidOperationException("This method is only intended for use with DataServiceQueryExtensions.Expand"); 
        }
 
        public static TPropType SubExpand<TSource, TPropType>(this TSource source, Expression<Func<TSource, TPropType>> propertySelector)
            where TSource : class
            where TPropType : class
        {
            throw new InvalidOperationException("This method is only intended for use with DataServiceQueryExtensions.Expand"); 
        }
    }
}

With all these extensions in place you can do the following things,

1. Expanding a collection

context.Orders.Expand(o => o.Items)

2. Expanding a collection and subcollection

context.Orders.Expand(o => o.Items.SubExpand(i => i.Product))

3. Adding a link

context.AddLink(order, o => o.Items, item)

4. Removing a link

context.DeleteLink(order, o => o.Items, item)

5. Loading an association

context.LoadProperty(order, o => o.Items)

No Comments