Recuperando o nome da propriedade da expressão lambda

Existe uma maneira melhor de obter o nome da propriedade quando passada através de uma expressão lambda? Aqui está o que eu tenho atualmente.

por exemplo.

GetSortingInfo(u => u.UserId); 

Ele funcionava apenas como uma expressão de membro quando a propriedade era uma string. porque nem todas as propriedades são strings que eu tive que usar o object, mas ele retornaria uma unaryexpression para eles.

 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; } 

Recentemente, fiz uma coisa muito semelhante para criar um método OnPropertyChanged seguro.

Aqui está um método que retornará o object PropertyInfo para a expressão. Ele lança uma exceção se a expressão não for uma propriedade.

 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; } 

O parâmetro source é usado para que o compilador possa fazer inferências de tipos na chamada do método. Você pode fazer o seguinte

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

Eu encontrei uma outra maneira que você pode fazer é ter a fonte e a propriedade fortemente tipadas e inferir explicitamente a input para o lambda. Não tenho certeza se essa é a terminologia correta, mas aqui está o 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); } 

E então chame assim.

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

e voila funciona.
Obrigado a todos.

Eu estava brincando com a mesma coisa e trabalhei nisso. Ele não está totalmente testado, mas parece lidar com o problema com tipos de valor (o problema de unaryexpression que você encontrou)

 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; } 

Isso lida com expressões membro e unárias. A diferença é que você obterá uma UnaryExpression se sua expressão representar um tipo de valor, enquanto você obterá um MemberExpression se sua expressão representar um tipo de referência. Tudo pode ser convertido em um object, mas os tipos de valor precisam ser encaixotados. É por isso que existe a expressão UnaryExpression. Referência.

Por uma questão de legibilidade (@Jowen), aqui está um 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; } 

Há um caso extremo quando se trata de Array .Comprimento. Enquanto ‘Comprimento’ é exposto como uma propriedade, você não pode usá-lo em nenhuma das soluções propostas 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); } 

Agora exemplo de uso:

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

Se PropertyNameFromUnaryExpr não verificasse ArrayLength , “someArray” seria impresso no console (o compilador parece gerar access direto ao campo Comprimento de suporte, como uma otimização, mesmo em Debug, portanto, o caso especial).

Esta é uma implementação geral para obter o nome da cadeia de campos / propriedades / indexadores / methods / methods de extensão / delegates de struct / class / interface / delegate / array. Eu testei com combinações de variantes estáticas / de instâncias e não 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); } 

Essa coisa pode ser escrita em um loop while simples também:

 //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"); } } } 

Eu gosto da abordagem recursiva, embora a segunda possa ser mais fácil de ler. Pode-se chamar assim:

 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 o último membro.

Nota:

  1. No caso de expressões encadeadas como ABC , “C” é retornado.

  2. Isso não funciona com const s, indexadores de array ou enum s (impossível cobrir todos os casos).

agora em C # 6 você pode simplesmente usar nameof como este nameof(User.UserId)

que tem muitos benefícios, entre eles é que isso é feito em tempo de compilation , não em tempo de execução.

https://msdn.microsoft.com/pt-br/magazine/dn802602.aspx

Aqui está uma atualização do método proposto por Cameron . O primeiro parâmetro não é obrigatório.

 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; } 

Você pode fazer o seguinte:

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

Métodos de extensão:

 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; } 

Você pode:

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

Descobri que algumas das respostas sugeridas que detalham o MemberExpression / UnaryExpression não capturam propriedades / aninhadas.

ex) o => o.Thing1.Thing2 retorna Thing1 vez de Thing1.Thing2 .

Essa distinção é importante se você estiver tentando trabalhar com EntityFramework DbSet.Include(...) .

Eu descobri que apenas parsing o Expression.ToString() parece funcionar bem, e relativamente rápido. Eu o comparei com a versão UnaryExpression , e até mesmo obtive ToString do Member/UnaryExpression para ver se era mais rápido, mas a diferença era insignificante. Por favor, corrija-me se esta é uma ideia terrível.

O método de extensão

 ///  /// 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 o delimitador pode até ser um exagero)

Demo (LinqPad)

Demonstração + código de comparação - https://gist.github.com/zaus/6992590

Bem, não há necessidade de chamar .Name.ToString() , mas, em geral, é sobre isso, sim. A única consideração que você pode precisar é se x.Foo.Bar deve retornar “Foo”, “Bar” ou uma exceção – ou seja, você precisa iterar.

(re comment) para mais informações sobre ordenação flexível, veja aqui .

Eu estou usando um método de extensão para projetos pre C # 6 e o nameof () para aqueles que segmentam 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; } } 

E eu chamo assim:

 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 bem com campos e propriedades.

Eu criei um método de extensão em ObjectStateEntry para poder sinalizar propriedades (de classs POCO do Entity Framework) como modificadas em uma maneira segura de tipo, desde que o método padrão só aceita uma seqüência de caracteres. Aqui está a minha maneira de obter o nome da propriedade:

 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); } 

Eu fiz a implementação INotifyPropertyChanged semelhante ao método abaixo. Aqui as propriedades são armazenadas em um dictionary na class base mostrada abaixo. É claro que nem sempre é desejável usar inheritance, mas para os modelos de visão, eu acho que é aceitável e fornece referências de propriedades muito limpas nas classs do modelo de visão.

 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); } } } 

A class base um pouco mais complexa é mostrada abaixo. Ele lida com a tradução da expressão lambda para o nome da propriedade. Observe que as propriedades são realmente pseudo-propriedades, pois somente os nomes são usados. Mas ele parecerá transparente para o modelo de exibição e referências às propriedades no modelo de exibição.

 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 é outra resposta:

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

Deixo esta function se você quiser obter múltiplos 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); } 

Aqui está outra maneira de obter o PropertyInfo baseado nesta resposta. Elimina a necessidade de uma instância de object.

 ///  /// 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; } 

Pode ser chamado assim:

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

Atualizei a resposta de @ Cameron para include algumas verificações de segurança contra expressões lambda convertidas em Convert :

 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; } 

A partir do .NET 4.0, você pode usar o ExpressionVisitor para encontrar propriedades:

 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); } } 

Aqui está como você usa esse 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."); }