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:
p => p.FirstName
I need a string of "FirstName"
.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 }
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With