Получение имени свойства из лямбда-выражения - PullRequest
475 голосов
/ 23 марта 2009

Есть ли лучший способ получить имя свойства при передаче через лямбда-выражение? Вот что у меня сейчас есть.

например.

GetSortingInfo<User>(u => u.UserId);

Он работал, выражая его как выражение, только когда свойство было строкой. потому что не все свойства являются строками, я должен был использовать объект, но тогда он возвращал бы унарное выражение для них.

public static RouteValueDictionary GetInfo<T>(this HtmlHelper html, 
    Expression<Func<T, object>> 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;
}

Ответы [ 19 ]

327 голосов
/ 23 марта 2009

Я недавно сделал очень похожую вещь, чтобы сделать безопасный метод типа OnPropertyChanged.

Вот метод, который возвращает объект PropertyInfo для выражения. Выдает исключение, если выражение не является свойством.

public PropertyInfo GetPropertyInfo<TSource, TProperty>(
    TSource source,
    Expression<Func<TSource, TProperty>> 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;
}

Параметр source используется, чтобы компилятор мог делать вывод типа при вызове метода. Вы можете сделать следующее

var propertyInfo = GetPropertyInfo(someUserObject, u => u.UserID);
177 голосов
/ 23 марта 2009

Я нашел другой способ, которым вы могли бы сделать, это строго указать тип источника и свойства и явно сделать вывод для лямбды Не уверен, что это правильная терминология, но вот результат.

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

    return GetInfo(html, name);
}

А потом назови это так.

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

и вуаля это работает.
Спасибо всем.

139 голосов
/ 27 мая 2010

Я играл с тем же и работал над этим. Он не полностью протестирован, но, похоже, решает проблему с типами значений (проблема с одним выражением, с которой вы столкнулись)

public static string GetName(Expression<Func<object>> exp)
{
    MemberExpression body = exp.Body as MemberExpression;

    if (body == null) {
       UnaryExpression ubody = (UnaryExpression)exp.Body;
       body = ubody.Operand as MemberExpression;
    }

    return body.Member.Name;
}
49 голосов
/ 23 июня 2012
public string GetName<TSource, TField>(Expression<Func<TSource, TField>> Field)
{
    return (Field.Body as MemberExpression ?? ((UnaryExpression)Field.Body).Operand as MemberExpression).Member.Name;
}

Это обрабатывает членские и унарные выражения. Разница в том, что вы получите UnaryExpression, если ваше выражение представляет тип значения, тогда как вы получите MemberExpression, если ваше выражение представляет ссылочный тип. Все может быть приведено к объекту, но типы значений должны быть упакованы. Вот почему существует UnaryExpression. См.

Для удобства чтения (@Jowen) вот расширенный эквивалент:

public string GetName<TSource, TField>(Expression<Func<TSource, TField>> 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;
}
19 голосов
/ 13 сентября 2018

С соответствием C # 7:

public static string GetMemberName<T>(this Expression<T> expression)
{
    switch (expression.Body)
    {
        case MemberExpression m:
            return m.Member.Name;
        case UnaryExpression u when u.Operand is MemberExpression m:
            return m.Member.Name;
        default:
            throw new NotImplementedException(expression.GetType().ToString());
    }
}

Пример:

public static RouteValueDictionary GetInfo<T>(this HtmlHelper html, 
    Expression<Func<T, object>> action) where T : class
{
    var name = action.GetMemberName();
    return GetInfo(html, name);
}
19 голосов
/ 12 декабря 2013

Это общая реализация для получения строкового имени полей / свойств / индексаторов / методов / методов расширения / делегатов структуры / класса / интерфейса / делегата / массива. Я проверил с комбинациями статических / экземпляров и неуниверсальных / универсальных вариантов.

//involves recursion
public static string GetMemberName(this LambdaExpression memberSelector)
{
    Func<Expression, string> 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);
}

Эту вещь можно записать и в простой цикл while:

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

Мне нравится рекурсивный подход, хотя второй может быть проще для чтения. Это можно назвать так:

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

чтобы напечатать последний член.

Примечание:

  1. В случае цепочечных выражений, таких как A.B.C, возвращается «C».

  2. Это не работает с const с, индексаторами массивов или enum с (невозможно охватить все случаи).

19 голосов
/ 02 декабря 2015

теперь в C # 6 вы можете просто использовать nameof вот так nameof(User.UserId)

, который имеет много преимуществ, среди которых то, что это делается во время компиляции , а не время выполнения.

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

19 голосов
/ 13 июня 2012

Есть крайний случай, когда дело доходит до Array .Длина. Хотя «Длина» выставляется как свойство, его нельзя использовать ни в одном из ранее предложенных решений.

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<TProp>(Exprs.Expression<Func<TProp>> expr)
{
    Contract.Requires<ArgumentNullException>(expr != null);
    Contract.Requires<ArgumentException>(expr.Body is Exprs.MemberExpression || expr.Body is Exprs.UnaryExpression);

    return PropertyNameFromLambdaExpr(expr);
}

public static string PropertyNameFromExpr<T, TProp>(Exprs.Expression<Func<T, TProp>> expr)
{
    Contract.Requires<ArgumentNullException>(expr != null);
    Contract.Requires<ArgumentException>(expr.Body is Exprs.MemberExpression || expr.Body is Exprs.UnaryExpression);

    return PropertyNameFromLambdaExpr(expr);
}

Теперь пример использования:

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

Если PropertyNameFromUnaryExpr не проверяет ArrayLength, «someArray» будет выводиться на консоль (кажется, что компилятор генерирует прямой доступ к полю Back * Length , как оптимизация, даже в Отладка, при этом частный случай).

16 голосов
/ 25 сентября 2014

Вот обновление для метода, предложенного Камероном . Первый параметр не обязателен.

public PropertyInfo GetPropertyInfo<TSource, TProperty>(
    Expression<Func<TSource, TProperty>> 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;
}

Вы можете сделать следующее:

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

Методы расширения:

public static PropertyInfo GetPropertyInfo<TSource, TProperty>(this TSource source,
    Expression<Func<TSource, TProperty>> propertyLambda) where TSource : class
{
    return GetPropertyInfo(propertyLambda);
}

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

Вы можете:

SomeType someInstance = null;
string propName = someInstance.NameOfProperty(i => i.Length);
PropertyInfo propInfo = someInstance.GetPropertyInfo(i => i.Length);
14 голосов
/ 20 июня 2013

Я обнаружил, что некоторые из предлагаемых ответов , которые детализируются до MemberExpression / UnaryExpression, не охватывают вложенные / подвойства.

ex) o => o.Thing1.Thing2 возвращает Thing1 вместо Thing1.Thing2.

Это различие важно, если вы пытаетесь работать с EntityFramework DbSet.Include(...).

Я обнаружил, что простой анализ Expression.ToString(), кажется, работает нормально и сравнительно быстро. Я сравнил его с версией UnaryExpression и даже извлек ToString из Member/UnaryExpression, чтобы проверить, быстрее ли это, но разница была незначительной. Пожалуйста, поправьте меня, если это ужасная идея.

Метод расширения

/// <summary>
/// Given an expression, extract the listed property name; similar to reflection but with familiar LINQ+lambdas.  Technique @via https://stackoverflow.com/a/16647343/1037948
/// </summary>
/// <remarks>Cheats and uses the tostring output -- Should consult performance differences</remarks>
/// <typeparam name="TModel">the model type to extract property names</typeparam>
/// <typeparam name="TValue">the value type of the expected property</typeparam>
/// <param name="propertySelector">expression that just selects a model property to be turned into a string</param>
/// <param name="delimiter">Expression toString delimiter to split from lambda param</param>
/// <param name="endTrim">Sometimes the Expression toString contains a method call, something like "Convert(x)", so we need to strip the closing part from the end</param>
/// <returns>indicated property name</returns>
public static string GetPropertyName<TModel, TValue>(this Expression<Func<TModel, TValue>> 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

(Проверка на разделитель может быть даже излишней)

Демо (LinqPad)

Демонстрация + код сравнения - https://gist.github.com/zaus/6992590

...