Получить свойство в виде строки из выражения <Func <TModel, TProperty >> - PullRequest
52 голосов
/ 07 мая 2010

Я использую некоторые строго типизированные выражения, которые сериализуются, чтобы мой код пользовательского интерфейса мог иметь строго типизированные выражения сортировки и поиска. Они имеют тип Expression<Func<TModel,TProperty>> и используются как таковые: SortOption.Field = (p => p.FirstName);. Я отлично сработал для этого простого случая.

Код, который я использую для анализа свойства FirstName, фактически повторно использует некоторые существующие функциональные возможности в используемом нами стороннем продукте, и он прекрасно работает, пока мы не начнем работать с глубоко вложенными свойствами ( SortOption.Field = (p => p.Address.State.Abbreviation);). В этом коде есть несколько совсем других предположений о необходимости поддержки глубоко вложенных свойств.

Что касается того, что делает этот код, я его не совсем понимаю, и вместо того, чтобы изменять этот код, я решил, что мне просто нужно написать эту функциональность с нуля. Однако я не знаю хорошего способа сделать это. Я подозреваю, что мы можем сделать что-то лучше, чем делать ToString () и выполнять разбор строк. Итак, что является хорошим способом сделать это для обработки тривиальных и глубоко вложенных случаев?

Требования:

  • Учитывая выражение p => p.FirstName Мне нужна строка "FirstName".
  • Учитывая выражение p => p.Address.State.Abbreviation Мне нужна строка "Address.State.Abbreviation"

Хотя это и не важно для ответа на мой вопрос, я подозреваю, что мой код сериализации / десериализации может быть полезен для кого-то еще, кто найдет этот вопрос в будущем, поэтому он ниже. Опять же, этот код не важен для вопроса - я просто подумал, что он может кому-то помочь. Обратите внимание, что DynamicExpression.ParseLambda взято из материала Dynamic LINQ , а Property.PropertyToString() - о чем этот вопрос.

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

Ответы [ 9 ]

92 голосов
/ 07 мая 2010

Вот хитрость: любое выражение этой формы ...

obj => obj.A.B.C // etc.

... на самом деле это просто набор вложенных MemberExpression объектов.

Первое, что вы получили:

MemberExpression: obj.A.B.C
Expression:       obj.A.B   // MemberExpression
Member:           C

Оценка Expression выше как MemberExpression дает вам:

MemberExpression: obj.A.B
Expression:       obj.A     // MemberExpression
Member:           B

Наконец, выше , что (на «вершине») у вас есть:

MemberExpression: obj.A
Expression:       obj       // note: not a MemberExpression
Member:           A

Таким образом, кажется очевидным, что способ решения этой проблемы заключается в проверке свойства Expression для MemberExpression вплоть до точки, в которой он больше не является MemberExpression.


ОБНОВЛЕНИЕ : Кажется, в вашей проблеме добавлено вращение. Возможно, у вас есть лямбда, которая выглядит как Func<T, int> ...

p => p.Age

... но на самом деле a Func<T, object>; в этом случае компилятор преобразует вышеприведенное выражение в:

p => Convert(p.Age)

Приспособление к этой проблеме на самом деле не так сложно, как может показаться. Взгляните на мой обновленный код, чтобы разобраться с ним. Обратите внимание, что путем абстрагирования кода для передачи MemberExpression в его собственный метод (TryFindMemberExpression) этот подход делает метод GetFullPropertyName достаточно чистым и позволяет добавлять дополнительные проверки в будущем - если, возможно, вы Вы окажетесь перед новым сценарием, который вы изначально не учитывали - без необходимости разбираться со слишком большим количеством кода.


Для иллюстрации: этот код работал для меня.

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

Использование:

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

Выход:

FirstName
Address.State.Abbreviation
Age
14 голосов
/ 07 мая 2010

Вот метод, который позволяет вам получить строковое представление, даже если у вас есть вложенные свойства:

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

Если вы все еще используете .NET 3.5, вам нужно добавить ToArray() после вызова Reverse(), поскольку перегрузка String.Join, которая занимает IEnumerable, была впервые добавлена ​​в .NET 4.

9 голосов
/ 07 мая 2010

Для "FirstName" из p => p.FirstName

Expression<Func<TModel, TProperty>> expression; //your given expression
string fieldName = ((MemberExpression)expression.Body).Member.Name; //watch out for runtime casting errors

Я предлагаю вам проверить код ASP.NET MVC 2 (из aspnet.codeplex.com), поскольку он имеет аналогичный API для помощников HTML... Html.TextBoxFor( p => p.FirstName ) и т. Д.

5 голосов
/ 09 февраля 2011

Другой простой подход - использовать метод System.Web.Mvc.ExpressionHelper.GetExpressionText.В своем следующем ударе я напишу более подробно.Посмотрите на http://carrarini.blogspot.com/.

4 голосов
/ 07 мая 2010

Я написал небольшой код для этого, и он, кажется, работает.

Учитывая следующие три определения класса:

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

Следующий метод даст вам полный путь свойства

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

Для двух звонков:

fullName = GetFullSortName<Person, string>(p => p.FirstName);
fullName = GetFullSortName<Person, string>(p => p.Address.State.Abbreviation);
2 голосов
/ 07 января 2013

Основываясь на этом и нескольких связанных вопросах / ответах здесь, вот простой метод, который я использую:

protected string propertyNameFromExpression<T>(Expression<Func<T, object>> prop)
{
    // /2212507/poluchit-svoistvo-v-vide-stroki-iz-vyrazheniya-func-tmodel-tproperty
    // http://stackoverflow.com/questions/767733/converting-a-net-funct-to-a-net-expressionfunct
    // http://stackoverflow.com/questions/793571/why-would-you-use-expressionfunct-rather-than-funct
    MemberExpression expr;

    if (prop.Body is MemberExpression)
        // .Net interpreted this code trivially like t => t.Id
        expr = (MemberExpression)prop.Body;
    else
        // .Net wrapped this code in Convert to reduce errors, meaning it's t => Convert(t.Id) - get at the
        // t.Id inside
        expr = (MemberExpression)((UnaryExpression)prop.Body).Operand;

    string name = expr.Member.Name;

    return name;
}

Вы можете использовать его просто как:

string name = propertyNameFromExpression(t => t.Id); // returns "Id"

Однако этот метод меньше проверяет ошибки, чем другие, размещенные здесь - в основном он принимает как должное, что он вызывается правильно, что не может быть безопасным допущением в вашем приложении.

2 голосов
/ 19 декабря 2012

Источник ExpressionHelper из MVC находится здесь

https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/ExpressionHelper.cs

Просто возьмите этот класс - и вы избежите зависимости от MVC и получите специальные крайние случаи для вас.

Отказ от ответственности: Не уверен, как работает лицензирование, просто посещая такой класс - но кажется довольно безобидным

1 голос
/ 07 мая 2010

Код, который я сейчас работаю на 100%, выглядит следующим образом, но я не совсем понимаю, что он делает (несмотря на то, что я изменил его, чтобы он обрабатывал эти глубоко вложенные сценарии благодаря отладчику).

    internal static string MemberWithoutInstance(this LambdaExpression expression)
    {
        var memberExpression = expression.ToMemberExpression();

        if (memberExpression == null)
        {
            return null;
        }

        if (memberExpression.Expression.NodeType == ExpressionType.MemberAccess)
        {
            var innerMemberExpression = (MemberExpression) memberExpression.Expression;

            while (innerMemberExpression.Expression.NodeType == ExpressionType.MemberAccess)
            {
                innerMemberExpression = (MemberExpression) innerMemberExpression.Expression;
            }

            var parameterExpression = (ParameterExpression) innerMemberExpression.Expression;

            // +1 accounts for the ".".
            return memberExpression.ToString().Substring(parameterExpression.ToString().Length + 1);
        }

        return memberExpression.Member.Name;
    }

    internal static MemberExpression ToMemberExpression(this LambdaExpression expression)
    {
        var memberExpression = expression.Body as MemberExpression;

        if (memberExpression == null)
        {
            var unaryExpression = expression.Body as UnaryExpression;

            if (unaryExpression != null)
            {
                memberExpression = unaryExpression.Operand as MemberExpression;
            }
        }

        return memberExpression;
    }

    public static string PropertyToString<TModel, TProperty>(this Expression<Func<TModel, TProperty>> source)
    {
        return source.MemberWithoutInstance();
    }

Это решение обрабатывает его, когда мое выражение имеет тип Expression<Func<TModel,object>>, и я передаю всевозможные типы объектов для своих параметров. Когда я делаю это, мое x => x.Age выражение превращается в x => Convert(x.Age), и это нарушает другие решения здесь. Я не понимаю, что в этом обрабатывает часть Convert, хотя. : - /

0 голосов
/ 15 января 2014

Кросс-постинг от Получение имени свойства из лямбда-выражения

Как упоминалось в вопросе, хитрый ответ таков: если вы позвоните expression.ToString(), он даст вам что-то вроде:

"o => o.ParentProperty.ChildProperty"

, который вы можете затем просто подстроку из первого периода.

На основании некоторых тестов LinqPad производительность была сопоставимой.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...