Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c#

lambda

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 } 
like image 977
Jaxidian Avatar asked May 07 '10 14:05

Jaxidian


1 Answers

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 
like image 73
Dan Tao Avatar answered Sep 23 '22 04:09

Dan Tao