Recuperando el nombre de la propiedad de la expresión lambda

¿Hay una mejor manera de obtener el nombre de la propiedad cuando se pasa a través de una expresión lambda? Esto es lo que tengo actualmente.

p.ej.

GetSortingInfo(u => u.UserId); 

Funcionó al convertirlo en una memberexpresión solo cuando la propiedad era una cadena. debido a que no todas las propiedades son cadenas, tuve que usar el objeto, pero luego devolvería una expresión no parecida para ellas.

 public static RouteValueDictionary GetInfo(this HtmlHelper html, Expression<Func> action) where T : class { var expression = GetMemberInfo(action); string name = expression.Member.Name; return GetInfo(html, name); } private static MemberExpression GetMemberInfo(Expression method) { LambdaExpression lambda = method as LambdaExpression; if (lambda == null) throw new ArgumentNullException("method"); MemberExpression memberExpr = null; if (lambda.Body.NodeType == ExpressionType.Convert) { memberExpr = ((UnaryExpression)lambda.Body).Operand as MemberExpression; } else if (lambda.Body.NodeType == ExpressionType.MemberAccess) { memberExpr = lambda.Body as MemberExpression; } if (memberExpr == null) throw new ArgumentException("method"); return memberExpr; } 

Hace poco hice algo muy similar para hacer un método seguro OnPropertyChanged.

Aquí hay un método que devolverá el objeto PropertyInfo para la expresión. Lanza una excepción si la expresión no es una propiedad.

 public PropertyInfo GetPropertyInfo( TSource source, Expression> propertyLambda) { Type type = typeof(TSource); MemberExpression member = propertyLambda.Body as MemberExpression; if (member == null) throw new ArgumentException(string.Format( "Expression '{0}' refers to a method, not a property.", propertyLambda.ToString())); PropertyInfo propInfo = member.Member as PropertyInfo; if (propInfo == null) throw new ArgumentException(string.Format( "Expression '{0}' refers to a field, not a property.", propertyLambda.ToString())); if (type != propInfo.ReflectedType && !type.IsSubclassOf(propInfo.ReflectedType)) throw new ArgumentException(string.Format( "Expression '{0}' refers to a property that is not from type {1}.", propertyLambda.ToString(), type)); return propInfo; } 

El parámetro source se usa para que el comstackdor pueda hacer una inferencia de tipo sobre la llamada al método. Puedes hacer lo siguiente

 var propertyInfo = GetPropertyInfo(someUserObject, u => u.UserID); 

Descubrí que otra forma de hacerlo es tener la fuente y la propiedad fuertemente tipadas e inferir explícitamente la entrada para la lambda. No estoy seguro si esa es la terminología correcta, pero aquí está el resultado.

 public static RouteValueDictionary GetInfo(this HtmlHelper html, Expression> action) where T : class { var expression = (MemberExpression)action.Body; string name = expression.Member.Name; return GetInfo(html, name); } 

Y luego llámalo así.

 GetInfo((User u) => u.UserId); 

y listo, funciona
Gracias a todos.

Estaba jugando con lo mismo y trabajé esto. No está completamente probado, pero parece manejar el problema con los tipos de valores (el problema de la expresión no coincidente con el que te topaste)

 public static string GetName(Expression> exp) { MemberExpression body = exp.Body as MemberExpression; if (body == null) { UnaryExpression ubody = (UnaryExpression)exp.Body; body = ubody.Operand as MemberExpression; } return body.Member.Name; } 
 public string GetName(Expression> Field) { return (Field.Body as MemberExpression ?? ((UnaryExpression)Field.Body).Operand as MemberExpression).Member.Name; } 

Esto maneja miembros y expresiones unarias. La diferencia es que obtendrás un UnaryExpression si tu expresión representa un tipo de valor, mientras que obtendrás un MemberExpression si tu expresión representa un tipo de referencia. Todo se puede convertir en un objeto, pero los tipos de valores deben estar enmarcados. Esta es la razón por la que existe UnaryExpression. Referencia.

En aras de la legibilidad (@Jowen), aquí hay un equivalente expandido:

 public string GetName(Expression> Field) { if (object.Equals(Field, null)) { throw new NullReferenceException("Field is required"); } MemberExpression expr = null; if (Field.Body is MemberExpression) { expr = (MemberExpression)Field.Body; } else if (Field.Body is UnaryExpression) { expr = (MemberExpression)((UnaryExpression)Field.Body).Operand; } else { const string Format = "Expression '{0}' not supported."; string message = string.Format(Format, Field); throw new ArgumentException(message, "Field"); } return expr.Member.Name; } 

Hay un caso de borde cuando se trata de Array .Length. Si bien ‘Longitud’ se expone como una propiedad, no se puede usar en ninguna de las soluciones propuestas anteriormente.

 using Contract = System.Diagnostics.Contracts.Contract; using Exprs = System.Linq.Expressions; static string PropertyNameFromMemberExpr(Exprs.MemberExpression expr) { return expr.Member.Name; } static string PropertyNameFromUnaryExpr(Exprs.UnaryExpression expr) { if (expr.NodeType == Exprs.ExpressionType.ArrayLength) return "Length"; var mem_expr = expr.Operand as Exprs.MemberExpression; return PropertyNameFromMemberExpr(mem_expr); } static string PropertyNameFromLambdaExpr(Exprs.LambdaExpression expr) { if (expr.Body is Exprs.MemberExpression) return PropertyNameFromMemberExpr(expr.Body as Exprs.MemberExpression); else if (expr.Body is Exprs.UnaryExpression) return PropertyNameFromUnaryExpr(expr.Body as Exprs.UnaryExpression); throw new NotSupportedException(); } public static string PropertyNameFromExpr(Exprs.Expression> expr) { Contract.Requires(expr != null); Contract.Requires(expr.Body is Exprs.MemberExpression || expr.Body is Exprs.UnaryExpression); return PropertyNameFromLambdaExpr(expr); } public static string PropertyNameFromExpr(Exprs.Expression> expr) { Contract.Requires(expr != null); Contract.Requires(expr.Body is Exprs.MemberExpression || expr.Body is Exprs.UnaryExpression); return PropertyNameFromLambdaExpr(expr); } 

Ahora uso de ejemplo:

 int[] someArray = new int[1]; Console.WriteLine(PropertyNameFromExpr( () => someArray.Length )); 

Si PropertyNameFromUnaryExpr no verifica ArrayLength , se ArrayLength “someArray” en la consola (el comstackdor parece generar acceso directo al campo de longitud de respaldo, como una optimización, incluso en Debug, por lo tanto el caso especial).

Esta es una implementación general para obtener el nombre de la cadena de campos / propiedades / indexadores / métodos / métodos de extensión / delegates de struct / class / interface / delegate / array. He probado con combinaciones de variantes estáticas / de instancia y no genéricas / genéricas.

 //involves recursion public static string GetMemberName(this LambdaExpression memberSelector) { Func nameSelector = null; //recursive func nameSelector = e => //or move the entire thing to a separate recursive method { switch (e.NodeType) { case ExpressionType.Parameter: return ((ParameterExpression)e).Name; case ExpressionType.MemberAccess: return ((MemberExpression)e).Member.Name; case ExpressionType.Call: return ((MethodCallExpression)e).Method.Name; case ExpressionType.Convert: case ExpressionType.ConvertChecked: return nameSelector(((UnaryExpression)e).Operand); case ExpressionType.Invoke: return nameSelector(((InvocationExpression)e).Expression); case ExpressionType.ArrayLength: return "Length"; default: throw new Exception("not a proper member selector"); } }; return nameSelector(memberSelector.Body); } 

Esto también se puede escribir en un ciclo simple:

 //iteration based public static string GetMemberName(this LambdaExpression memberSelector) { var currentExpression = memberSelector.Body; while (true) { switch (currentExpression.NodeType) { case ExpressionType.Parameter: return ((ParameterExpression)currentExpression).Name; case ExpressionType.MemberAccess: return ((MemberExpression)currentExpression).Member.Name; case ExpressionType.Call: return ((MethodCallExpression)currentExpression).Method.Name; case ExpressionType.Convert: case ExpressionType.ConvertChecked: currentExpression = ((UnaryExpression)currentExpression).Operand; break; case ExpressionType.Invoke: currentExpression = ((InvocationExpression)currentExpression).Expression; break; case ExpressionType.ArrayLength: return "Length"; default: throw new Exception("not a proper member selector"); } } } 

Me gusta el enfoque recursivo, aunque el segundo podría ser más fácil de leer. Uno puede llamarlo así:

 someExpr = x => x.Property.ExtensionMethod()[0]; //or someExpr = x => Static.Method().Field; //or someExpr = x => VoidMethod(); //or someExpr = () => localVariable; //or someExpr = x => x; //or someExpr = x => (Type)x; //or someExpr = () => Array[0].Delegate(null); //etc string name = someExpr.GetMemberName(); 

para imprimir el último miembro

Nota:

  1. En el caso de expresiones encadenadas como ABC , se devuelve “C”.

  2. Esto no funciona con const s, indexadores de matriz o enum s (imposible de cubrir todos los casos).

ahora en C # 6 puedes usar el nombre de este nameof(User.UserId)

que tiene muchos beneficios, entre ellos es que esto se hace en tiempo de comstackción , no en tiempo de ejecución.

https://msdn.microsoft.com/en-us/magazine/dn802602.aspx

Aquí hay una actualización del método propuesto por Cameron . El primer parámetro no es obligatorio.

 public PropertyInfo GetPropertyInfo( Expression> propertyLambda) { Type type = typeof(TSource); MemberExpression member = propertyLambda.Body as MemberExpression; if (member == null) throw new ArgumentException(string.Format( "Expression '{0}' refers to a method, not a property.", propertyLambda.ToString())); PropertyInfo propInfo = member.Member as PropertyInfo; if (propInfo == null) throw new ArgumentException(string.Format( "Expression '{0}' refers to a field, not a property.", propertyLambda.ToString())); if (type != propInfo.ReflectedType && !type.IsSubclassOf(propInfo.ReflectedType)) throw new ArgumentException(string.Format( "Expresion '{0}' refers to a property that is not from type {1}.", propertyLambda.ToString(), type)); return propInfo; } 

Puedes hacer lo siguiente:

 var propertyInfo = GetPropertyInfo(u => u.UserID); var propertyInfo = GetPropertyInfo((SomeType u) => u.UserID); 

Métodos de extensión:

 public static PropertyInfo GetPropertyInfo(this TSource source, Expression> propertyLambda) where TSource : class { return GetPropertyInfo(propertyLambda); } public static string NameOfProperty(this TSource source, Expression> propertyLambda) where TSource : class { PropertyInfo prodInfo = GetPropertyInfo(propertyLambda); return prodInfo.Name; } 

Usted puede:

 SomeType someInstance = null; string propName = someInstance.NameOfProperty(i => i.Length); PropertyInfo propInfo = someInstance.GetPropertyInfo(i => i.Length); 

Descubrí que algunas de las respuestas sugeridas que profundizan en MemberExpression / UnaryExpression no capturan subpropiedades / nesteds.

ex) o => o.Thing1.Thing2 devuelve Thing1 lugar de Thing1.Thing2 .

Esta distinción es importante si intenta trabajar con EntityFramework DbSet.Include(...) .

Descubrí que solo analizar el Expression.ToString() parece funcionar bien, y comparativamente rápido. Lo UnaryExpression contra la versión de UnaryExpression , e incluso desconecté ToString del Member/UnaryExpression para ver si eso era más rápido, pero la diferencia era insignificante. Por favor corrígeme si esta es una idea terrible.

El método de extensión

 ///  /// Given an expression, extract the listed property name; similar to reflection but with familiar LINQ+lambdas. Technique @via https://stackoverflow.com/a/16647343/1037948 ///  /// Cheats and uses the tostring output -- Should consult performance differences /// the model type to extract property names /// the value type of the expected property /// expression that just selects a model property to be turned into a string /// Expression toString delimiter to split from lambda param /// Sometimes the Expression toString contains a method call, something like "Convert(x)", so we need to strip the closing part from the end /// indicated property name public static string GetPropertyName(this Expression> propertySelector, char delimiter = '.', char endTrim = ')') { var asString = propertySelector.ToString(); // gives you: "o => o.Whatever" var firstDelim = asString.IndexOf(delimiter); // make sure there is a beginning property indicator; the "." in "o.Whatever" -- this may not be necessary? return firstDelim < 0 ? asString : asString.Substring(firstDelim+1).TrimEnd(endTrim); }//-- fn GetPropertyNameExtended 

(Verificar si el delimitador puede ser excesivo)

Demo (LinqPad)

Demostración + código de comparación - https://gist.github.com/zaus/6992590

Bueno, no hay necesidad de llamar a .Name.ToString() , pero en general eso es todo, sí. La única consideración que puede necesitar es si x.Foo.Bar debería devolver “Foo”, “Barra” o una excepción, es decir, si necesita iterar en absoluto.

(volver a comentar) para obtener más información sobre la clasificación flexible, ver aquí .

Estoy usando un método de extensión para proyectos pre C # 6 y el nombre de () para aquellos que se dirigen a C # 6.

 public static class MiscExtentions { public static string NameOf(this object @object, Expression> propertyExpression) { var expression = propertyExpression.Body as MemberExpression; if (expression == null) { throw new ArgumentException("Expression is not a property."); } return expression.Member.Name; } } 

Y lo llamo así:

 public class MyClass { public int Property1 { get; set; } public string Property2 { get; set; } public int[] Property3 { get; set; } public Subclass Property4 { get; set; } public Subclass[] Property5 { get; set; } } public class Subclass { public int PropertyA { get; set; } public string PropertyB { get; set; } } // result is Property1 this.NameOf((MyClass o) => o.Property1); // result is Property2 this.NameOf((MyClass o) => o.Property2); // result is Property3 this.NameOf((MyClass o) => o.Property3); // result is Property4 this.NameOf((MyClass o) => o.Property4); // result is PropertyB this.NameOf((MyClass o) => o.Property4.PropertyB); // result is Property5 this.NameOf((MyClass o) => o.Property5); 

Funciona bien con ambos campos y propiedades.

Creé un método de extensión en ObjectStateEntry para poder marcar las propiedades (de las clases de POCO de Entity Framework) como modificado de una manera segura, ya que el método predeterminado solo acepta una cadena. Esta es mi forma de obtener el nombre de la propiedad:

 public static void SetModifiedProperty(this System.Data.Objects.ObjectStateEntry state, Expression> action) { var body = (MemberExpression)action.Body; string propertyName = body.Member.Name; state.SetModifiedProperty(propertyName); } 

He hecho la implementación INotifyPropertyChanged similar al método a continuación. Aquí las propiedades se almacenan en un diccionario en la clase base que se muestra a continuación. Por supuesto, no siempre es deseable utilizar la herencia, pero para los modelos de visualización, creo que es aceptable y proporciona referencias de propiedad muy claras en las clases de modelos de vista.

 public class PhotoDetailsViewModel : PropertyChangedNotifierBase { public bool IsLoading { get { return GetValue(x => x.IsLoading); } set { SetPropertyValue(x => x.IsLoading, value); } } public string PendingOperation { get { return GetValue(x => x.PendingOperation); } set { SetPropertyValue(x => x.PendingOperation, value); } } public PhotoViewModel Photo { get { return GetValue(x => x.Photo); } set { SetPropertyValue(x => x.Photo, value); } } } 

La clase base algo más compleja se muestra a continuación. Maneja la traducción de la expresión lambda al nombre de la propiedad. Tenga en cuenta que las propiedades son realmente pseudo propiedades ya que solo se usan los nombres. Pero aparecerá transparente para el modelo de vista y las referencias a las propiedades en el modelo de vista.

 public class PropertyChangedNotifierBase : INotifyPropertyChanged { readonly Dictionary _properties = new Dictionary(); protected U GetValue(Expression> property) { var propertyName = GetPropertyName(property); return GetValue(propertyName); } private U GetValue(string propertyName) { object value; if (!_properties.TryGetValue(propertyName, out value)) { return default(U); } return (U)value; } protected void SetPropertyValue(Expression> property, U value) { var propertyName = GetPropertyName(property); var oldValue = GetValue(propertyName); if (Object.ReferenceEquals(oldValue, value)) { return; } _properties[propertyName] = value; RaisePropertyChangedEvent(propertyName); } protected void RaisePropertyChangedEvent(Expression> property) { var name = GetPropertyName(property); RaisePropertyChangedEvent(name); } protected void RaisePropertyChangedEvent(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } private static string GetPropertyName(Expression> property) { if (property == null) { throw new NullReferenceException("property"); } var lambda = property as LambdaExpression; var memberAssignment = (MemberExpression) lambda.Body; return memberAssignment.Member.Name; } public event PropertyChangedEventHandler PropertyChanged; } 

Esta es otra respuesta:

 public static string GetPropertyName(this HtmlHelper htmlHelper, Expression> expression) { var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); return metaData.PropertyName; } 

Dejo esta función si quieres obtener múltiples campos:

 ///  /// Get properties separated by , (Ex: to invoke 'd => new { d.FirstName, d.LastName }') ///  ///  ///  ///  public static string GetFields(Expression> exp) { MemberExpression body = exp.Body as MemberExpression; var fields = new List(); if (body == null) { NewExpression ubody = exp.Body as NewExpression; if (ubody != null) foreach (var arg in ubody.Arguments) { fields.Add((arg as MemberExpression).Member.Name); } } return string.Join(",", fields); } 

Aquí hay otra forma de obtener PropertyInfo basado en esta respuesta. Elimina la necesidad de una instancia de objeto.

 ///  /// Get metadata of property referenced by expression. Type constrained. ///  public static PropertyInfo GetPropertyInfo(Expression> propertyLambda) { return GetPropertyInfo((LambdaExpression) propertyLambda); } ///  /// Get metadata of property referenced by expression. ///  public static PropertyInfo GetPropertyInfo(LambdaExpression propertyLambda) { // https://stackoverflow.com/questions/671968/retrieving-property-name-from-lambda-expression MemberExpression member = propertyLambda.Body as MemberExpression; if (member == null) throw new ArgumentException(string.Format( "Expression '{0}' refers to a method, not a property.", propertyLambda.ToString())); PropertyInfo propInfo = member.Member as PropertyInfo; if (propInfo == null) throw new ArgumentException(string.Format( "Expression '{0}' refers to a field, not a property.", propertyLambda.ToString())); if(propertyLambda.Parameters.Count() == 0) throw new ArgumentException(String.Format( "Expression '{0}' does not have any parameters. A property expression needs to have at least 1 parameter.", propertyLambda.ToString())); var type = propertyLambda.Parameters[0].Type; if (type != propInfo.ReflectedType && !type.IsSubclassOf(propInfo.ReflectedType)) throw new ArgumentException(String.Format( "Expression '{0}' refers to a property that is not from type {1}.", propertyLambda.ToString(), type)); return propInfo; } 

Se puede llamar así:

 var propertyInfo = GetPropertyInfo((User u) => u.UserID); 

He actualizado la respuesta de @Car Cameron para incluir algunas comprobaciones de seguridad contra las expresiones Convert lambda mecanografiadas:

 PropertyInfo GetPropertyName( Expression> propertyLambda) { var body = propertyLambda.Body; if (!(body is MemberExpression member) && !(body is UnaryExpression unary && (member = unary.Operand as MemberExpression) != null)) throw new ArgumentException($"Expression '{propertyLambda}' " + "does not refer to a property."); if (!(member.Member is PropertyInfo propInfo)) throw new ArgumentException($"Expression '{propertyLambda}' " + "refers to a field, not a property."); var type = typeof(TSource); if (!propInfo.DeclaringType.GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) throw new ArgumentException($"Expresion '{propertyLambda}' " + "refers to a property that is not from type '{type}'."); return propInfo; } 

Comenzando con .NET 4.0 puede usar ExpressionVisitor para encontrar propiedades:

 class ExprVisitor : ExpressionVisitor { public bool IsFound { get; private set; } public string MemberName { get; private set; } public Type MemberType { get; private set; } protected override Expression VisitMember(MemberExpression node) { if (!IsFound && node.Member.MemberType == MemberTypes.Property) { IsFound = true; MemberName = node.Member.Name; MemberType = node.Type; } return base.VisitMember(node); } } 

Aquí es cómo usa este visitante:

 var visitor = new ExprVisitor(); visitor.Visit(expr); if (visitor.IsFound) { Console.WriteLine("First property in the expression tree: Name={0}, Type={1}", visitor.MemberName, visitor.MemberType.FullName); } else { Console.WriteLine("No properties found."); }