Get the property, as a string, from an Expression<Func<TModel,TProperty>>

Here's the trick: any expression of this form...

obj => obj.A.B.C // etc.

...is really just a bunch of nested MemberExpression objects.

First you've got:

MemberExpression: obj.A.B.C
Expression:       obj.A.B   // MemberExpression
Member:           C

Evaluating Expression above as a MemberExpression gives you:

MemberExpression: obj.A.B
Expression:       obj.A     // MemberExpression
Member:           B

Finally, above that (at the "top") you have:

MemberExpression: obj.A
Expression:       obj       // note: not a MemberExpression
Member:           A

So it seems clear that the way to approach this problem is by checking the Expression property of a MemberExpression up until the point where it is no longer itself a MemberExpression.


UPDATE: It seems there is an added spin on your problem. It may be that you have some lambda that looks like a Func<T, int>...

p => p.Age

...but is actually a Func<T, object>; in this case, the compiler will convert the above expression to:

p => Convert(p.Age)

Adjusting for this issue actually isn't as tough as it might seem. Take a look at my updated code for one way to deal with it. Notice that by abstracting the code for getting a MemberExpression away into its own method (TryFindMemberExpression), this approach keeps the GetFullPropertyName method fairly clean and allows you to add additional checks in the future -- if, perhaps, you find yourself facing a new scenario which you hadn't originally accounted for -- without having to wade through too much code.


To illustrate: this code worked for me.

// code adjusted to prevent horizontal overflow
static string GetFullPropertyName<T, TProperty>
(Expression<Func<T, TProperty>> exp)
{
    MemberExpression memberExp;
    if (!TryFindMemberExpression(exp.Body, out memberExp))
        return string.Empty;

    var memberNames = new Stack<string>();
    do
    {
        memberNames.Push(memberExp.Member.Name);
    }
    while (TryFindMemberExpression(memberExp.Expression, out memberExp));

    return string.Join(".", memberNames.ToArray());
}

// code adjusted to prevent horizontal overflow
private static bool TryFindMemberExpression
(Expression exp, out MemberExpression memberExp)
{
    memberExp = exp as MemberExpression;
    if (memberExp != null)
    {
        // heyo! that was easy enough
        return true;
    }

    // if the compiler created an automatic conversion,
    // it'll look something like...
    // obj => Convert(obj.Property) [e.g., int -> object]
    // OR:
    // obj => ConvertChecked(obj.Property) [e.g., int -> long]
    // ...which are the cases checked in IsConversion
    if (IsConversion(exp) && exp is UnaryExpression)
    {
        memberExp = ((UnaryExpression)exp).Operand as MemberExpression;
        if (memberExp != null)
        {
            return true;
        }
    }

    return false;
}

private static bool IsConversion(Expression exp)
{
    return (
        exp.NodeType == ExpressionType.Convert ||
        exp.NodeType == ExpressionType.ConvertChecked
    );
}

Usage:

Expression<Func<Person, string>> simpleExp = p => p.FirstName;
Expression<Func<Person, string>> complexExp = p => p.Address.State.Abbreviation;
Expression<Func<Person, object>> ageExp = p => p.Age;

Console.WriteLine(GetFullPropertyName(simpleExp));
Console.WriteLine(GetFullPropertyName(complexExp));
Console.WriteLine(GetFullPropertyName(ageExp));

Output:

FirstName
Address.State.Abbreviation
Age

Here is a method that lets you get the string representation, even when you have nested properties:

public static string GetPropertySymbol<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;
    }
}

If you are still on .NET 3.5, you need to stick a ToArray() after the call to Reverse(), because the overload of String.Join that takes an IEnumerable was first added in .NET 4.


For "FirstName" from p => p.FirstName

Expression<Func<TModel, TProperty>> expression; //your given expression
string fieldName = ((MemberExpression)expression.Body).Member.Name; //watch out for runtime casting errors

I will suggest you check out the ASP.NET MVC 2 code (from aspnet.codeplex.com) as it has similar API for Html helpers... Html.TextBoxFor( p => p.FirstName ) etc

Tags:

C#

Lambda