tags:

views:

424

answers:

5

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
}
+2  A: 

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

Khurram Aziz
+11  A: 

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
Dan Tao
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.
Jaxidian
FWIW, your code without change works for strings even when using it as `Expression<Func<Person, object>>`.
Jaxidian
@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!
Dan Tao
+2  A: 

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.

driis
+1 for LINQ, Nice and simple!
David Robbins
+2  A: 

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);
mnero0429
A: 

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. :-/

Jaxidian