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

9

55

I use some strongly-typed expressions that get serialized to allow my UI code to have strongly-typed sorting and searching expressions. These are of type Expression<Func<TModel,TProperty>> and are used as such: SortOption.Field = (p => p.FirstName);. I've gotten this working perfectly for this simple case.

The code that I'm using for parsing the "FirstName" property out of there is actually reusing some existing functionality in a third-party product that we use and it works great, until we start working with deeply-nested properties(SortOption.Field = (p => p.Address.State.Abbreviation);). This code has some very different assumptions in the need to support deeply-nested properties.

As for what this code does, I don't really understand it and rather than changing that code, I figured I should just write from scratch this functionality. However, I don't know of a good way to do this. I suspect we can do something better than doing a ToString() and performing string parsing. So what's a good way to do this to handle the trivial and deeply-nested cases?

Requirements:

  • Given the expression p => p.FirstName I need a string of "FirstName".
  • Given the expression p => p.Address.State.Abbreviation I need a string of "Address.State.Abbreviation"

While it's not important for an answer to my question, I suspect my serialization/deserialization code could be useful to somebody else who finds this question in the future, so it is below. Again, this code is not important to the question - I just thought it might help somebody. Note that DynamicExpression.ParseLambda comes from the Dynamic LINQ stuff and Property.PropertyToString() is what this question is about.

/// <summary>
/// This defines a framework to pass, across serialized tiers, sorting logic to be performed.
/// </summary>
/// <typeparam name="TModel">This is the object type that you are filtering.</typeparam>
/// <typeparam name="TProperty">This is the property on the object that you are filtering.</typeparam>
[Serializable]
public class SortOption<TModel, TProperty> : ISerializable where TModel : class
{
    /// <summary>
    /// Convenience constructor.
    /// </summary>
    /// <param name="property">The property to sort.</param>
    /// <param name="isAscending">Indicates if the sorting should be ascending or descending</param>
    /// <param name="priority">Indicates the sorting priority where 0 is a higher priority than 10.</param>
    public SortOption(Expression<Func<TModel, TProperty>> property, bool isAscending = true, int priority = 0)
    {
        Property = property;
        IsAscending = isAscending;
        Priority = priority;
    }

    /// <summary>
    /// Default Constructor.
    /// </summary>
    public SortOption()
        : this(null)
    {
    }

    /// <summary>
    /// This is the field on the object to filter.
    /// </summary>
    public Expression<Func<TModel, TProperty>> Property { get; set; }

    /// <summary>
    /// This indicates if the sorting should be ascending or descending.
    /// </summary>
    public bool IsAscending { get; set; }

    /// <summary>
    /// This indicates the sorting priority where 0 is a higher priority than 10.
    /// </summary>
    public int Priority { get; set; }

    #region Implementation of ISerializable

    /// <summary>
    /// This is the constructor called when deserializing a SortOption.
    /// </summary>
    protected SortOption(SerializationInfo info, StreamingContext context)
    {
        IsAscending = info.GetBoolean("IsAscending");
        Priority = info.GetInt32("Priority");

        // We just persisted this by the PropertyName. So let's rebuild the Lambda Expression from that.
        Property = DynamicExpression.ParseLambda<TModel, TProperty>(info.GetString("Property"), default(TModel), default(TProperty));
    }

    /// <summary>
    /// Populates a <see cref="T:System.Runtime.Serialization.SerializationInfo"/> with the data needed to serialize the target object.
    /// </summary>
    /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> to populate with data. </param>
    /// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext"/>) for this serialization. </param>
    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        // Just stick the property name in there. We'll rebuild the expression based on that on the other end.
        info.AddValue("Property", Property.PropertyToString());
        info.AddValue("IsAscending", IsAscending);
        info.AddValue("Priority", Priority);
    }

    #endregion
}
Rakes answered 7/5, 2010 at 14:57 Comment(4)
possible duplicate of C#: Getting Names of properties in a chain from lambda expressionVerrucose
@Verrucose That question is going after a slightly different thing that also has slightly different problems. They want to split each part of the namespacing into separate strings. I wanted it in a single string. Also, the answers to that question do not handle unboxing (object -> int), which was a part of my problem. So this question is not a duplicate of that question.Rakes
Jaxidian, I see, but it still is a duplicate acc to me. A difference of string array being returned and the same thing returned as a single joined string isn't a differentiator, considering that's not the bigger problem at all. Yes it had an incomplete answer, you're right to ask it again. But since answers are amended there, we can close it now, I think.Verrucose
possible duplicate of #672468Rusty
A
92

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
Angary answered 7/5, 2010 at 15:14 Comment(5)
This worked for my posted question but I just found out that I have a more complex scenario simply because I'm using it as Expression<Func<Person, object>> so I can handle both an int and a string. Doing it this way, the expression, even though I type it as x => x.Age is stored as x => Convert(x.Age) for non-string properties. I have actually modified the third-party code to work as it handles this (I didn't realize that) but your solution and answer is very thorough. I'll shortly post the code I'm using as another answer but would love to see your answer adapt it.Rakes
FWIW, your code without change works for strings even when using it as Expression<Func<Person, object>>.Rakes
@Jaxidian: I've updated my answer with one possible approach to account for your scenario. It works with the example you provided. Try it out and see how it works for you!Angary
Based on this answer and other in SO, I have made up a library: Mariuzzo.Web.Mvc.Extras that handle this specific. Any contribution will be appreciated.Novah
If you're getting namespace conflicts about the Expression type, specify System.Linq.Expressions.Expression, not System.Windows.Expression.Quentinquercetin
S
16

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.

Schaefer answered 7/5, 2010 at 15:28 Comment(0)
C
9

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

Catechism answered 7/5, 2010 at 15:12 Comment(0)
T
5

Another simple approach is to use System.Web.Mvc.ExpressionHelper.GetExpressionText method. In my next blow I will write more in detail. Have a look to http://carrarini.blogspot.com/.

Tilt answered 9/2, 2011 at 8:12 Comment(3)
But this requires me to add dependencies to MVC and web stuff. I don't want to do this in winforms apps, WCF Service apps, or DALs - ain't no way, ain't no how! However, if this is only needed in an MVC app, then perhaps this is an option.Rakes
then just take the sourcecode - its opensource github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/…Calomel
This is the way to go IMO. Here is the same ExpressionHelper.GetExpressionText for ASPNET Core github.com/aspnet/AspNetCore/blob/master/src/Mvc/…Infer
L
4

I wrote a little code for this, and it seemed to work.

Given the following three class definitions:

class Person {
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Address Address { get; set; }
}

class State {
    public string Abbreviation { get; set; }
}

class Address {
    public string City { get; set; }
    public State State { get; set; }
}

The following method will give you the full property path

static string GetFullSortName<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression) {
    var memberNames = new List<string>();

    var memberExpression = expression.Body as MemberExpression;
    while (null != memberExpression) {
        memberNames.Add(memberExpression.Member.Name);
        memberExpression = memberExpression.Expression as MemberExpression;
    }

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

For the two calls:

fullName = GetFullSortName<Person, string>(p => p.FirstName);
fullName = GetFullSortName<Person, string>(p => p.Address.State.Abbreviation);
Legitimist answered 7/5, 2010 at 15:29 Comment(0)
M
4

Based on this and several related questions/answers here, here's the simple method I'm using:

protected string propertyNameFromExpression<T>(Expression<Func<T, object>> prop)
{
    // https://mcmap.net/q/110681/-get-the-property-as-a-string-from-an-expression-lt-func-lt-tmodel-tproperty-gt-gt
    // https://mcmap.net/q/112020/-converting-a-net-func-lt-t-gt-to-a-net-expression-lt-func-lt-t-gt-gt
    // https://mcmap.net/q/21254/-why-would-you-use-expression-lt-func-lt-t-gt-gt-rather-than-func-lt-t-gt
    MemberExpression expr;

    if (prop.Body is MemberExpression)
        // .Net interpreted this code trivially like t => t.Id
        expr = (MemberExpression)prop.Body;
    else
        // .Net wrapped this code in Convert to reduce errors, meaning it's t => Convert(t.Id) - get at the
        // t.Id inside
        expr = (MemberExpression)((UnaryExpression)prop.Body).Operand;

    string name = expr.Member.Name;

    return name;
}

You can use it simply like:

string name = propertyNameFromExpression(t => t.Id); // returns "Id"

This method however does less error checking than others posted here - basically it takes for granted it's called properly, which may not be a safe assumption in your application.

Mildredmildrid answered 6/1, 2013 at 23:19 Comment(0)
C
2

ExpressionHelper source from MVC is here

https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/ExpressionHelper.cs

Just take this class - and you'll avoid taking dependency on MVC and get special edge cases handled for you.

Disclaimer: Not sure how licensing works just taking a class like this - but seems pretty innocuous

Calomel answered 18/12, 2012 at 20:59 Comment(0)
R
1

The code that I have working 100% now is as follows, but I don't really understand what it's doing (despite the fact that I modified it to make it handle these deeply-nested scenarios thanks to the debugger).

    internal static string MemberWithoutInstance(this LambdaExpression expression)
    {
        var memberExpression = expression.ToMemberExpression();

        if (memberExpression == null)
        {
            return null;
        }

        if (memberExpression.Expression.NodeType == ExpressionType.MemberAccess)
        {
            var innerMemberExpression = (MemberExpression) memberExpression.Expression;

            while (innerMemberExpression.Expression.NodeType == ExpressionType.MemberAccess)
            {
                innerMemberExpression = (MemberExpression) innerMemberExpression.Expression;
            }

            var parameterExpression = (ParameterExpression) innerMemberExpression.Expression;

            // +1 accounts for the ".".
            return memberExpression.ToString().Substring(parameterExpression.ToString().Length + 1);
        }

        return memberExpression.Member.Name;
    }

    internal static MemberExpression ToMemberExpression(this LambdaExpression expression)
    {
        var memberExpression = expression.Body as MemberExpression;

        if (memberExpression == null)
        {
            var unaryExpression = expression.Body as UnaryExpression;

            if (unaryExpression != null)
            {
                memberExpression = unaryExpression.Operand as MemberExpression;
            }
        }

        return memberExpression;
    }

    public static string PropertyToString<TModel, TProperty>(this Expression<Func<TModel, TProperty>> source)
    {
        return source.MemberWithoutInstance();
    }

This solution handles it when my expression is of type Expression<Func<TModel,object>> and I pass all sorts of object types in for my parameters. When I do this, my x => x.Age expression gets turned into x => Convert(x.Age) and that breaks the other solutions on here. I don't understand what in this handles the Convert part, though. :-/

Rakes answered 7/5, 2010 at 16:9 Comment(0)
R
0

Cross-posting from Retrieving Property name from lambda expression

As the question alluded to, the sneaky answer is that if you call expression.ToString(), it will give you something like:

"o => o.ParentProperty.ChildProperty"

which you can then just substring from the first period.

Based on some LinqPad tests, performance was comparable.

Rusty answered 15/1, 2014 at 7:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.