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
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/.
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);