Получите свойство, как строка, от Выражения <Func <TModel, TProperty>>

Я использую некоторые выражения со строгим контролем типов, которые сериализируются, чтобы позволить моему коду UI иметь сортировку со строгим контролем типов и поиск выражений. Они имеют тип Expression> и используются как таковые: 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 прибывает из Динамического материала LINQ и Property.PropertyToString() то, о чем этот вопрос.

/// 
/// This defines a framework to pass, across serialized tiers, sorting logic to be performed.
/// 
/// This is the object type that you are filtering.
/// This is the property on the object that you are filtering.
[Serializable]
public class SortOption : ISerializable where TModel : class
{
    /// 
    /// Convenience constructor.
    /// 
    /// The property to sort.
    /// Indicates if the sorting should be ascending or descending
    /// Indicates the sorting priority where 0 is a higher priority than 10.
    public SortOption(Expression> property, bool isAscending = true, int priority = 0)
    {
        Property = property;
        IsAscending = isAscending;
        Priority = priority;
    }

    /// 
    /// Default Constructor.
    /// 
    public SortOption()
        : this(null)
    {
    }

    /// 
    /// This is the field on the object to filter.
    /// 
    public Expression> Property { get; set; }

    /// 
    /// This indicates if the sorting should be ascending or descending.
    /// 
    public bool IsAscending { get; set; }

    /// 
    /// This indicates the sorting priority where 0 is a higher priority than 10.
    /// 
    public int Priority { get; set; }

    #region Implementation of ISerializable

    /// 
    /// This is the constructor called when deserializing a SortOption.
    /// 
    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(info.GetString("Property"), default(TModel), default(TProperty));
    }

    /// 
    /// Populates a  with the data needed to serialize the target object.
    /// 
    /// The  to populate with data. 
    /// The destination (see ) for this serialization. 
    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
}

53
задан Jaxidian 7 May 2010 в 04:12
поделиться

5 ответов

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

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

Наконец, над that (на "вершине") у вас есть:

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

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


UPDATE: Похоже, что к вашей проблеме добавилась еще одна. Возможно, у вас есть некая лямбда, которая выглядит как Func...

p => p.Age

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

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
92
ответ дан 7 November 2019 в 08:27
поделиться

Код, который у меня работает на 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 > , и я передаю все типы объектов в качестве параметров. Когда я это делаю, мое выражение x => x.Age превращается в x => Convert (x.Age) , и это ломает другие решения здесь. Однако я не понимаю, что здесь обрабатывает часть Convert . : - /

1
ответ дан 7 November 2019 в 08:27
поделиться

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

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.

15
ответ дан 7 November 2019 в 08:27
поделиться

Для "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) и т. д.

9
ответ дан 7 November 2019 в 08:27
поделиться

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

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

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);
4
ответ дан 7 November 2019 в 08:27
поделиться
Другие вопросы по тегам:

Похожие вопросы: