Over the weekend, I attended Codestock. One of the sessions that I attended dealt with a subject I had wrestled with myself: eliminating string literals.
While the session focused on HtmlHelper extensions for MVC, I have used the same techniques for building general LINQ expressions, queries, and other useful aspects.
Imagine sending data down to the client and you want to be able to have the client sort or search through that data. Subsequently, it’s very helpful if we can map the user’s actions against our domain models. These domain models would, in turn, be utilized to build out LINQ expressions. MVC has some handy built-in methods to get a property name, as a string, from an Expression. In building reusable utility methods, though, we can take a similar approach without incurring a dependecy on System.Web.Mvc. Ultimately, we want to be able to go to/from strongly typed property references and loose string literals. However, we want to work entirely with strongly typed references in our code.
Looking at the Linq Expression namespace (System.Linq.Expressions), here’s the simplest case:
public static string GetPropertyName<T>(Expression<Func<T>> expression) { var body = (MemberExpression)expression.Body; return body.Member.Name; } public static string GetPropertyName<T>(Expression<Func<T, object>> expression) { if (expression.Body is MemberExpression) { return ((MemberExpression)expression.Body).Member.Name; } else { var op = ((UnaryExpression)expression.Body).Operand; return ((MemberExpression)op).Member.Name; } }
And these methods would be called in this manner:
SearchParameters obj = null; var name = StaticHelpers.GetPropertyName(() => obj.Id); name = StaticHelpers.GetPropertyName<SearchParameters>(x => x.Id);
The first method requires a handle to the object type (note it can be a null ref) and the second is declaring the type specification. You may wonder why we have to check if the expression, in the second case, is a MemberExpression or a UnaryExpression. This is due to specifying Func[T, object] as our delegate without a type specification. For some types, this will result in LINQ generate a “Convert” expression to convert the underlying type to object. Conceptually, it is similar to unboxing, but it’s relative to expression tress.
For my simple model, I would get a string of “Id.” From that point forward, I can pass that Id to, for example, a LINQ orderby expression, or, in the case of how I like to use it, pass it to the client for binding and passing it to a generic LINQ Expression query builder. I tough on those topics a bit next time as a expand on this idea more.
Using this method is OK for getting single-depth properties, but what about long object graphs where we want the fully-qualified path to a property? We can handle this pretty easily:
/// <summary> /// Returns the fully qualified path of the property name - helpful if one does not want to use System.Web.Mvc ExpressionHelper /// </summary> /// <typeparam name="T"></typeparam> /// <typeparam name="TResult"></typeparam> /// <param name="expression"></param> /// <returns></returns> public static string GetFullPropertyName<T, TResult>(Expression<Func<T, TResult>> expression) { return string.Join(".", GetMembersOnPath(expression.Body as MemberExpression) .Select(m => m.Member.Name) .Reverse()); } private static IEnumerable<MemberExpression> GetMembersOnPath(MemberExpression expression) { while (expression != null) { yield return expression; expression = expression.Expression as MemberExpression; } }
This helper uses a simple LINQ query and method to get all of the MemberExpressions on the path and then return the string Name for the path. It works because yield expression.Expression returns the parent Expression of an Expression – yes, that seems confusing, but it is somewhat of a recursive routine thanks to the yield keyword. Since we work backward, this is why the “Reverse” is needed.
To call the method, it’s called similarly to the other static methods:
var name = StaticHelpers.GetFullPropertyName<Payment>(x => x.Employee.UserName);
The resulting string would be “Employee.UserName”
Next time, I’ll show how to tie these techniques into useful LINQ/EF Expressions in such a way that string literals are avoided.