53 votes

Obtenir la propriété, en tant que chaîne, d'une expression <Func<TModel,TProperty> >

J'utilise fortement typées expressions qui se sérialisé pour permettre à mon code de l'INTERFACE utilisateur d'avoir fortement typées de tri et de recherche d'expressions. Ce sont de type Expression<Func<TModel,TProperty>> et sont utilisés en tant que tels: SortOption.Field = (p => p.FirstName);. J'ai obtenu ce travail parfaitement pour ce cas simple.

Le code que j'utilise pour l'analyse de la "FirstName" propriété de, il est en fait de la réutilisation de certaines fonctionnalités existantes dans un tiers des produits que nous utilisons et il fonctionne très bien, jusqu'à ce que nous commençons à travailler avec profondément propriétés imbriquées(SortOption.Field = (p => p.Address.State.Abbreviation);). Ce code a certains très différentes hypothèses dans la nécessité de soutenir profondément propriétés imbriquées.

Comme pour ce que ce code fait, je ne comprends pas vraiment, et plutôt que de changer le code, j'ai pensé que je devrais écrire à partir de zéro cette fonctionnalité. Cependant, je ne sais pas d'une bonne façon de le faire. Je pense que nous pouvons faire quelque chose de mieux que de faire un ToString() et d'effectuer le traitement de chaîne. Donc, ce est une bonne façon de le faire pour gérer le trivial et profondément imbriquée cas?

Exigences:

  • Compte tenu de l'expression p => p.FirstName - je besoin d'une chaîne d' "FirstName".
  • Compte tenu de l'expression p => p.Address.State.Abbreviation - je besoin d'une chaîne d' "Address.State.Abbreviation"

Alors que ce n'est pas important pour une réponse à ma question, je soupçonne mon sérialisation/désérialisation code pourrait être utile à quelqu'un d'autre qui trouve cette question dans l'avenir, de sorte qu'il est ci-dessous. Encore une fois, ce code n'est pas important pour la question - j'ai juste pensé qu'il pourrait aider quelqu'un. Notez que DynamicExpression.ParseLambda provient de la Dynamique LINQ trucs et Property.PropertyToString() est ce que cette question est à propos.

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

92voto

Dan Tao Points 60518

Voici l'astuce: n'importe quelle expression de cette forme...

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

...c'est vraiment juste un tas de imbriquée MemberExpression objets.

D'abord vous avez:

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

L'évaluation Expression ci-dessus en tant que MemberExpression vous donne:

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

Enfin, au-dessus que ("haut"), vous avez:

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

Il semble donc clair que la manière d'aborder ce problème consiste à vérifier l' Expression de la propriété d'un MemberExpression jusqu'au point où il n'est plus lui-même un MemberExpression.


Mise à JOUR: Il semble qu'il n'y est ajouté un tour sur votre problème. Peut-être que vous avez quelques lambda qui regarde comme un Func<T, int>...

p => p.Age

...mais est en fait un Func<T, object>; dans ce cas, le compilateur va convertir l'expression ci-dessus à:

p => Convert(p.Age)

Le réglage de ce problème en réalité n'est pas aussi difficile que cela puisse paraître. Jetez un oeil à mon code mis à jour pour un moyen de traiter avec elle. Notez qu'en faisant abstraction de la code pour obtenir un MemberExpression loin dans sa propre méthode (TryFindMemberExpression), cette approche maintient l' GetFullPropertyName méthode assez propre et permet d'ajouter des contrôles supplémentaires dans les années à venir-si, peut-être, vous vous trouvez face à un nouveau scénario qui vous n'aviez pas initialement comptabilisés, sans avoir à parcourir trop de code.


Pour illustrer: ce code a fonctionné pour moi.

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

Utilisation:

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

Sortie:

FirstName
Address.State.Abbreviation
Age

16voto

driis Points 70872

Voici une méthode qui vous permet d'obtenir la représentation sous forme de chaîne, même lorsque vous avez des propriétés imbriquées:

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

Si vous êtes toujours sur .NET 3.5, vous devez coller un ToArray() après l'appel à Reverse() , car la surcharge de String.Join qui prend un IEnumerable été ajouté pour la première fois dans .NET 4.

9voto

Khurram Aziz Points 938

Pour "Prénom" à partir de p => p.Premier Nom

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

Je vais vous suggérer de vérifier le code ASP.NET MVC 2 (de aspnet.codeplex.com) car il a une API similaire pour les aides HTML ... Html.TextBoxFor (p => p.FirstName), etc.

5voto

Dani Points 181

Une autre approche simple consiste à utiliser la méthode System.Web.Mvc.ExpressionHelper.GetExpressionText. Dans mon prochain coup, j'écrirai plus en détail. Jetez un coup d'œil à http://carrarini.blogspot.com/ .

4voto

Michael Nero Points 644

J'ai écrit un petit code pour cela, et cela a semblé fonctionner.

Étant donné les trois définitions de classe suivantes:

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

La méthode suivante vous donnera le chemin complet de la propriété

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

Pour les deux appels:

 fullName = GetFullSortName<Person, string>(p => p.FirstName);
fullName = GetFullSortName<Person, string>(p => p.Address.State.Abbreviation);
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X