Strongly-typed reflection with Member.Of
For quite some time I have had a reflection helper class that worked much like the one described by Daniel Cazzulino in this post on Statically-typed reflection with LINQ. The static method on the class helped provide a strongly-typed reflection experience and to avoid the use of the dreaded magic strings.
The class has been working like intended, but I have always had a slight desire to revisit it and change the API that it provides. However I have never gotten around to it, not until Jeff Handley published his post on Member.Of – Avoiding hard-coded strings for property names. His post does a great job of explaining the problems of magic strings and also to illustrate how a Member.Of method can be used, so I will skip that.
There was three things that I immediately liked about Jeff’s implementation; the name Member.Of, the simple API that was provided by using a single method with a couple of overloads and the fact that he was using implicit cast operators to be able to cast the result to either a MemberInfo object or a string. It was these three things that made me decide that I was going to revisit my own reflection helper and create my own flavor of Jeff’s Member.Of implementation.
Implementing the Member.Of functionality
Because my own helper class already had a method called GetTargetMemberInfo, which is used to retrieve a MemberInfo instance from an expression, the transformation into Member.Of mostly involved renaming the class and cleaning up the available public methods.
using System;using System.Linq.Expressions;using System.Reflection;/// <summary>/// Contains helper methods for providing a strongly-typed reflection experience./// </summary>public static class Member{ /// <summary> /// Retrieves the member that is being invoked on an instance of an object. /// </summary> /// <param name="expression">A <see cref="Expression{TDelegate}"/> instance that the member should be retrieved from.</param> /// <returns>An <see cref="OfResult"/> instance for the retrieved member.</returns> /// <remarks>This method will be called for expressions that targets members that has no return value.</remarks> public static OfResult Of(Expression<Action> expression) { return GetResult(expression); } /// <summary> /// Retrieves the member that is being invoked on an instance of an object. /// </summary> /// <param name="expression">A <see cref="Expression{TDelegate}"/> instance that the member should be retrieved from.</param> /// <returns>An <see cref="OfResult"/> instance for the retrieved member.</returns> /// <remarks>This method will be called for expressions that targets members that has a return value.</remarks> public static OfResult Of(Expression<Func<object>> expression) { return GetResult(expression); } /// <summary> /// Retrieves member that is being identified using a strongly typed expression. /// </summary> /// <typeparam name="T">The <see cref="Type"/> used in the expression.</typeparam> /// <param name="expression">The strongly typed <see cref="Expression{TDelegate}"/> instance that the member should be retrieved for.</param> /// <returns>An <see cref="OfResult"/> instance for the retrieved member.</returns> /// <remarks>This method will be called for strongly-typed expressions with members that has no return value.</remarks> public static OfResult Of<T>(Expression<Action<T>> expression) { return GetResult(expression); } /// <summary> /// Retrieves member that is being identified using a strongly typed expression. /// </summary> /// <typeparam name="T">The <see cref="Type"/> used in the expression.</typeparam> /// <param name="expression">The strongly typed <see cref="Expression{TDelegate}"/> instance that the member should be retrieved for.</param> /// <returns>An <see cref="OfResult"/> instance for the retrieved member.</returns> /// <remarks>This method will be called for strongly-typed expressions with members that has a return value.</remarks> public static OfResult Of<T>(Expression<Func<T, object>> expression) { return GetResult(expression); } /// <summary> /// Gets the <see cref="MemberInfo"/> instance for <see cref="MemberExpression"/> expression. /// </summary> /// <param name="expression">The <see cref="Expression"/> that the <see cref="MemberInfo"/> instance should be retrieved for.</param> /// <returns>A <see cref="MemberInfo"/> that was retrieved from the provided <see cref="MemberExpression"/> object.</returns> private static MemberInfo GetMemberAccessReturnValue(MemberExpression expression) { return expression.Member.Name.Equals("instance") ? expression.Type : expression.Member; } /// <summary> /// Creates a <see cref="OfResult"/> instance for the member that was identified by the provided <see cref="Expression"/>. /// </summary> /// <param name="expression">The <see cref="Expression"/> that the member should be identified for.</param> /// <returns>A <see cref="OfResult"/> instance for the member that was identified in the expression provided by the <paramref name="expression"/> parameter.</returns> private static OfResult GetResult(Expression expression) { var member = GetTargetMemberInfo(expression); return new OfResult(member); } /// <summary> /// Retrieves the member that an expression is defined for. /// </summary> /// <param name="expression">The expression to retrieve the member from.</param> /// <returns>A <see cref="MemberInfo"/> instance if the member could be found; otherwise <see langword="null"/>.</returns> private static MemberInfo GetTargetMemberInfo(Expression expression) { switch (expression.NodeType) { case ExpressionType.Convert: return GetTargetMemberInfo(((UnaryExpression)expression).Operand); case ExpressionType.Lambda: return GetTargetMemberInfo(((LambdaExpression)expression).Body); case ExpressionType.Call: return ((MethodCallExpression)expression).Method; case ExpressionType.MemberAccess: return GetMemberAccessReturnValue((MemberExpression)expression); case ExpressionType.New: return ((NewExpression)expression).Constructor; case ExpressionType.Parameter: return expression.Type; default: return null; } }}The class provided overloads for the Member.Of method that enables MemberInfo instances to be retrieved from either strongly-types expressions or directly from an instance of an object. Most of the work is performed in the GetTargetMemberInfo, which uses recursion to digg down into the expression to identify the correct MemberInfo instance.
Taking a look at the OfResult type
I decided to not include the implicit cast operators directly into my Member class, like Jeff has done, but instead I am returning a class that accepts a MemberInfo instance and that has the ability to be implicitly cast to a couple of types.
using System.Reflection;/// <summary>/// Provides the ability to implicitly cast a <see cref="MemberInfo"/> instance into various types./// </summary>public class OfResult{ /// <summary> /// Initializes a new instance of the <see cref="OfResult"/> class. /// </summary> /// <param name="member">The <see cref="MemberInfo"/> instance to wrap.</param> public OfResult(MemberInfo member) { this.Member = member; } /// <summary> /// Gets the <see cref="MemberInfo"/> instance that the result wraps. /// </summary> /// <value>The <see cref="MemberInfo"/> instance that the result wraps.</value> public MemberInfo Member { get; private set; } /// <summary> /// Performs an implicit conversion from <see cref="OfResult"/> to <see cref="MemberInfo"/>. /// </summary> /// <param name="result">The <see cref="OfResult"/> instance to cast.</param> /// <returns>The <see cref="MemberInfo"/> that the <see cref="OfResult"/> instance wraps.</returns> public static implicit operator MemberInfo(OfResult result) { return result.Member; } /// <summary> /// Performs an implicit conversion from <see cref="OfResult"/> to <see cref="System.String"/>. /// </summary> /// <param name="result">The <see cref="OfResult"/> instance to cast.</param> /// <returns>The string name of the <see cref="MemberInfo"/> that the <see cref="OfResult"/> instance wraps.</returns> public static implicit operator string(OfResult result) { return result.Member.ToString(); }}As you can see there is almost no logic in the implementation of the OfResult type, so why did I decide to use this approach? There are a couple of reasons as to why. First of all I got the idea in my head and I decided to run with it to see where I would end up and then take the decision of whether or not I would keep it. The key points that made me decide to keep it was the fact that by using it I cut the number of method overloads of Member.Of in half, since I did not have to provide one for each return type (adding more implicit casts in the future will save even more code), and I also got a nice type to add extension methods to.
Adding additional functionality to the OfResult type with the help of extension methods
One of the biggest difference between my old implementation and the new one was the fact that the old implementation handed out FieldInfo, MethodInfo, PropertyInfo, and so on, instance where as the new implementation hands out OfResult instances that can implicitly be cast to either MemberInfo or a string. Even though all of the other info types all inherit from MemberInfo, I some times want to have an instance of the correct type.
There is no problem at getting that, all that is needed is an explicit cast and I am good to go. However, I really wanted to avoid explicit casts since they clutter up the code and makes it less readable. What I ended up doing was create a set of extension methods, for the OfResult type, that encapsulate these explicit casts.
using System;using System.Reflection;/// <summary>/// Defines extension methods for the <see cref="OfResult"/> class./// </summary>public static class OfResultExtensions{ /// <summary> /// Casts the <see cref="OfResult.Member"/> property to a <see cref="ConstructorInfo"/> object. /// </summary> /// <param name="result">The <see cref="OfResult"/> instance to cast the member of.</param> /// <returns>A <see cref="ConstructorInfo"/> object.</returns> public static ConstructorInfo AsConstructor(this OfResult result) { return (ConstructorInfo)result.Member; } /// <summary> /// Casts the <see cref="OfResult.Member"/> property to a <see cref="FieldInfo"/> object. /// </summary> /// <param name="result">The <see cref="OfResult"/> instance to cast the member of.</param> /// <returns>A <see cref="FieldInfo"/> object.</returns> public static FieldInfo AsField(this OfResult result) { return (FieldInfo)result.Member; } /// <summary> /// Casts the <see cref="OfResult.Member"/> property to a <see cref="MethodInfo"/> object. /// </summary> /// <param name="result">The <see cref="OfResult"/> instance to cast the member of.</param> /// <returns>A <see cref="MethodInfo"/> object.</returns> public static MethodInfo AsMethod(this OfResult result) { return (MethodInfo)result.Member; } /// <summary> /// Casts the <see cref="OfResult.Member"/> property to a <see cref="PropertyInfo"/> object. /// </summary> /// <param name="result">The <see cref="OfResult"/> instance to cast the member of.</param> /// <returns>A <see cref="PropertyInfo"/> object.</returns> public static PropertyInfo AsProperty(this OfResult result) { return (PropertyInfo)result.Member; } /// <summary> /// Casts the <see cref="OfResult.Member"/> property to a <see cref="Type"/> object. /// </summary> /// <param name="result">The <see cref="OfResult"/> instance to cast the member of.</param> /// <returns>A <see cref="Type"/> object.</returns> public static Type AsType(this OfResult result) { return (Type)result.Member; }}When using these the reading experience, of the code, is a lot cleaner.
// Using explicit castvar method1 = (MethodInfo)Member.Of(() => instance.SomeMethod());// Using cast extension methodvar method2 = Member.Of(() => instance.SomeMethod()).AsMethod();
This is definitely a class that I will be using in my projects from now on, instead of my old reflection helper class. If you can think of any other useful features to add to it, please let me know. I do have a suite of unit tests for the code as well and if anyone is interested then drop me a line.