Click here to Skip to main content
15,884,099 members
Articles / Programming Languages / C#
Tip/Trick

Using Expressions to Provide a Type-Safe Member Lookup

Rate me:
Please Sign up or sign in to vote.
4.38/5 (7 votes)
10 May 2015CPOL4 min read 21.6K   22   6   11
When GetProperty leaves you wondering if there really is a property named "Addreess" (sic) on the Type, using an Expression may be a better option.

Introduction

Embedding constant strings in code is a practice that makes many programmers wary. Even when these strings are placed in const variables, sometimes that feeling just doesn't go away.

In the .NET Reflection library, the System.Type class allows an application to get a field, method, or property (collectively "member") on a type by specifying its name as a string. Sometimes this is necessary. But sometimes, the programmer knows the name at compile-time.

If you know the exact Field or Property that is interesting, but feel dirty passing the name of the said field or property as a parameter to GetField or GetProperty, this article may be for you.

Background

I wrote this code when I was implementing a lightweight datastore framework. I did not know the Types of the objects stored. I only knew that they needed to be indexed by some field. The field was to be specified when the application configured the datastore. The datastore would then use some field to look up objects whose Types were unknown when the framework was compiled. The application would know the field(s) when it configured the datastore, however.

You could use this code any time you know the name of a Field or Property at compile time. I am not including my datastore code, as it is not part of this article.

Using the Code

By way of example, say your application needs a FieldInfo or PropertyInfo object for a given field or property. Using simple Reflection, one may have code that looks similar to:

C#
public class Person
{
    public string Name;
    public int Age { get; set; }
}

var fiName = typeof( Person ).GetField( "Name" );
var piAge = typeof( Person ).GetProperty( "Age" )

The application knows the Type and Member, and relies on the name of the member as a string in order to look it up. This, by the way, completely breaks when you use a refactoring tool to rename your members!

The code presented in this tip is meant to provide a mechanism for taking the strings out of the code. Check out this example:

C#
Expression<Func<Person, string>> exprName = _p => _p.Name;
var fiName = exprName.Body.GetFieldInfo();

Expression<Func<Person, int>> exprAge = _p => _p.Age;
var piAge = exprAge.Body.GetPropertyInfo();

OK, this gets the type safety issue out of the way, but it sure is ugly! Writing some convenience methods around the core implementation can help with the ugly:

C#
var fiName2 = GetFieldInfo<Person>( _p => _p.Name );
var piAge2 = GetPropertyInfo<Person>( _p => _p.Age );

In the second, less ugly example, the "GetFieldInfo" and "GetPropertyInfo" methods are static methods on the same class as the code. If you put these static methods into their own class (and you should!), you would obviously need to add the appropriate class name in front of the method name.

In the first example, an extension method was added on to Expression. In the second example, the expression was passed to a static method without explicitly marking it as an Expression, thereby making the code a bit more readable.

The Code Behind

Here's the core method used to get member information out of an Expression:

C#
/// <summary>
/// Get MemberInfo even if the referenced member is a primitive value nestled inside
/// a UnaryExpression which is a Convert operation.
/// </summary>
/// <param name="p_body">
/// The Expression Body which is evaluated as a Member expression
/// </param>
/// <returns>
/// A FieldInfo or PropertyInfo representing the member described in the expression.
/// </returns>
public static MemberInfo GetMemberInfo( this Expression p_body )
{
    MemberExpression memberExpr = p_body as MemberExpression;

    if (p_body is UnaryExpression)
    {
        var unaryBody = p_body as UnaryExpression;
        if (unaryBody.NodeType != ExpressionType.Convert &&
            unaryBody.NodeType != ExpressionType.ConvertChecked)
            throw new ArgumentException( "A Non-Convert Unary Expression was found." );

        memberExpr = unaryBody.Operand as MemberExpression;
        if (memberExpr == null)
            throw new ArgumentException
            ( "The target of the Convert operation was not a MemberExpression." );
    }
    else if (memberExpr == null)
    {
        throw new ArgumentException( "The Expression must identify a single member." );
    }

    var member = memberExpr.Member;
    if (!(member is FieldInfo || member is PropertyInfo))
        throw new ArgumentException
        ( "The member specified was not a Field or Property: " + member.GetType() );

    return memberExpr.Member;
}

Note: The check for the UnaryExpression is to support primitive values when the expression is in the form Func<T,object>. If the member specified in the expression is a primitive member, then it will be expressed as a "Convert" operation because the return-type of a Func<T,object> delegate is object. This is effectively a boxing operation that the expression builder would have performed for you automatically. This has no effect for the first example shown in the "Using the code" section.

The wrapper methods that make this type-specific to Fields and Properties are quite straightforward:

C#
/// <summary>
/// Wrapper around GetMemberInfo that assures a Field is returned
/// </summary>
/// <param name="p_body">The expression identifying a field</param>
/// <returns>A FieldInfo object for the identified field</returns>
public static FieldInfo GetFieldInfo( this Expression p_body )
{
    var member = GetMemberInfo( p_body );
    if (!(member is FieldInfo))
        throw new ArgumentException( "The specified member is not a Field: " + member.GetType() );

    return member as FieldInfo;
}

/// <summary>
/// Wrapper around GetMemberInfo that assures a Property is returned
/// </summary>
/// <param name="p_body">The expression identifying a property</param>
/// <returns>A PropertyInfo object for the identified property</returns>
public static PropertyInfo GetPropertyInfo( this Expression p_body )
{
    var member = GetMemberInfo( p_body );
    if (!(member is PropertyInfo))
        throw new ArgumentException( "The specified member is not a Property: " + member.GetType() );

    return member as PropertyInfo;
}

These are merely convenience methods, as are the following three methods which allow an Expression to be passed directly into the method:

C#
/// <summary>
/// Get a MemberInfo object for an expression. Allows the expression to be
/// constructed as a parameter to this method.
/// </summary>
/// <typeparam name="T">
/// The Type of the object declaring the interesting field or property
/// </typeparam>
/// <param name="p_expr">An expression identifying a member on type T</param>
/// <returns>A MemberInfo object for the identified member</returns>
public static MemberInfo GetMemberInfo<T>( Expression<Func<T, object>> p_expr )
{
    return p_expr.Body.GetMemberInfo();
}

/// <summary>
/// Get a FieldInfo object for an expression. Allows the expression to be
/// constructed as a parameter to this method.
/// </summary>
/// <typeparam name="T">
/// The Type of the object declaring the interesting field
/// </typeparam>
/// <param name="p_expr">An expression identifying a field on type T</param>
/// <returns>A FieldInfo object for the identified field</returns>
public static FieldInfo GetFieldInfo<T>( Expression<Func<T, object>> p_expr )
{
    return GetFieldInfo( p_expr.Body );
}

/// <summary>
/// Get a PropertyInfo object for an expression. Allows the expression to be
/// constructed as a parameter to this method.
/// </summary>
/// <typeparam name="T">
/// The Type of the object declaring the interesting property
/// </typeparam>
/// <param name="p_expr">An expression identifying a property on type T</param>
/// <returns>A PropertyInfo object for the identified property</returns>
public static PropertyInfo GetPropertyInfo<T>( Expression<Func<T, object>> p_expr )
{
    return GetPropertyInfo( p_expr.Body );
}

It is these three methods which make the underlying core method more readable and manageable in your application code. It lets the compiler do the work of constructing the expression tree for you behind the scenes, leaving you with more readable code:

C#
var fiName = GetFieldInfo<Person>( _p => _p.Name );
var piAge = GetPropertyInfo<Person>( _p => _p.Age );

Notice that these methods all use Expression.Body to call the other methods. This is because Expression<Func<T,object>> is a strongly typed wrapper around the underlying Expression containing the identifying information.

Points of Interest

The core GetMemberInfo method was originally written with the expectation of an Expression<Func<T,object>> as a parameter. Thus, the check for the UnaryExpression is done there. It makes that core method more general than if this check was done elsewhere with very little cost in performance.

When using the core GetMemberInfo method or its two immediate helpers for fields and properties, the actual Type of the return value of the expression is very flexible. For instance, one could have used the following two Expression initializers instead of the strongly typed ones shown above:

C#
Expression<Func<Person, object>> exprName = _p => _p.Name;
var fiName = exprName.Body.GetFieldInfo();

Expression<Func<Person, object>> exprAge = _p => _p.Age;
var piAge = exprAge.Body.GetPropertyInfo();

There's only one real way to interpret _p.Name and _p.Age in this example, and the Expression builder inside the compiler does this for you.

Summary

When your application knows at compile-time what fields or properties it needs Reflection information for, this provides a type-safe mechanism for getting this information.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionNo longer relevant? Pin
Alex DaSwagga Dresko11-May-15 5:07
Alex DaSwagga Dresko11-May-15 5:07 
AnswerRe: No longer relevant? Pin
VanJenke11-May-15 6:31
VanJenke11-May-15 6:31 
GeneralRe: No longer relevant? Pin
GiedriusBan12-May-15 4:51
GiedriusBan12-May-15 4:51 
GeneralRe: No longer relevant? Pin
VanJenke12-May-15 5:14
VanJenke12-May-15 5:14 
GeneralRe: No longer relevant? Pin
David A. Gray2-Jun-15 10:49
David A. Gray2-Jun-15 10:49 
AnswerRe: No longer relevant? Pin
Mike Oberberger11-May-15 6:40
Mike Oberberger11-May-15 6:40 
GeneralInteresting... Pin
VanJenke11-May-15 3:54
VanJenke11-May-15 3:54 
BugBug found ... Pin
i0011-May-15 0:15
i0011-May-15 0:15 
AnswerRe: Bug found ... Pin
i0011-May-15 0:39
i0011-May-15 0:39 
GeneralRe: Bug found ... Pin
Mike Oberberger11-May-15 4:06
Mike Oberberger11-May-15 4:06 
GeneralMy vote of 5 Pin
webmaster44210-May-15 19:58
webmaster44210-May-15 19:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.