web-dev-qa-db-fra.com

Obtenez la propriété, sous forme de chaîne, à partir d'une expression <Func <TModel, TProperty >>

J'utilise des expressions fortement typées qui sont sérialisées pour permettre à mon code d'interface utilisateur d'avoir des expressions de tri et de recherche fortement typées. Ils sont de type Expression<Func<TModel,TProperty>> Et sont utilisés comme tels: SortOption.Field = (p => p.FirstName);. J'ai obtenu que cela fonctionne parfaitement pour ce cas simple.

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

Quant à ce que fait ce code, je ne le comprends pas vraiment et plutôt que de changer ce code, j'ai pensé que je devrais simplement écrire à partir de zéro cette fonctionnalité. Cependant, je ne connais pas de bonne manière de procéder. Je soupçonne que nous pouvons faire quelque chose de mieux que de faire une ToString () et d'effectuer une analyse de chaîne. Alors, quelle est la bonne façon de procéder pour gérer les cas triviaux et profondément imbriqués?

Exigences:

  • Étant donné l'expression p => p.FirstName J'ai besoin d'une chaîne de "FirstName".
  • Étant donné l'expression p => p.Address.State.Abbreviation J'ai besoin d'une chaîne de "Address.State.Abbreviation"

Bien que ce ne soit pas important pour une réponse à ma question, je soupçonne que mon code de sérialisation/désérialisation pourrait être utile à quelqu'un d'autre qui trouvera cette question à l'avenir, il est donc ci-dessous. Encore une fois, ce code n'est pas important pour la question - je pensais juste qu'il pourrait aider quelqu'un. Notez que DynamicExpression.ParseLambda Vient de la matière Dynamic LINQ et Property.PropertyToString() est de quoi cette question est.

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

Voici l'astuce: toute expression de cette forme ...

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

... est vraiment juste un tas d'objets MemberExpression imbriqués.

Vous avez d'abord:

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

Évaluer Expression ci-dessus en tant que MemberExpression vous donne:

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

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

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

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


UPDATE : Il semble qu'il y ait une rotation supplémentaire sur votre problème. Il se peut que vous ayez un lambda qui ressemble à un Func<T, int>...

p => p.Age

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

p => Convert(p.Age)

L'ajustement pour ce problème n'est pas aussi difficile qu'il y paraît. Jetez un oeil à mon code mis à jour pour une façon de le gérer. Notez qu'en supprimant le code pour obtenir un MemberExpression dans sa propre méthode (TryFindMemberExpression), cette approche maintient la méthode GetFullPropertyName assez propre et vous permet d'ajouter des vérifications supplémentaires dans l'avenir - si, peut-être, vous vous trouvez face à un nouveau scénario que vous n'aviez pas expliqué à l'origine - sans avoir à parcourir trop 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
    );
}

Usage:

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

Production:

FirstName
Address.State.Abbreviation
Age
92
Dan Tao

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 une ToArray() après l'appel à Reverse(), car la surcharge de String.Join Qui prend un IEnumerable a été ajouté pour la première fois dans .NET 4.

14
driis

Pour "FirstName" À partir de p => p.FirstName

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 assistants HTML ... Html.TextBoxFor( p => p.FirstName ) etc

9
Khurram Aziz

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 œil à http://carrarini.blogspot.com/ .

5
Dani

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 d'accès complet à 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);
4
Michael Nero

Sur la base de cela et de plusieurs questions/réponses connexes ici, voici la méthode simple que j'utilise:

protected string propertyNameFromExpression<T>(Expression<Func<T, object>> prop)
{
    // http://stackoverflow.com/questions/2789504/get-the-property-as-a-string-from-an-expressionfunctmodel-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;
}

Vous pouvez l'utiliser simplement comme:

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

Cette méthode fait cependant moins de vérification d'erreurs que les autres publiées ici - fondamentalement, elle tient pour acquis qu'elle s'appelle correctement, ce qui peut ne pas être une hypothèse sûre dans votre application.

2
Chris Moschini

La source ExpressionHelper de MVC est ici

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

Prenez simplement ce cours - et vous éviterez de dépendre de MVC et vous recevrez des cas spéciaux Edge.

Avertissement: Je ne sais pas comment fonctionne la licence en prenant simplement un cours comme celui-ci - mais cela semble assez anodin

2
Simon_Weaver

Le code que je travaille à 100% maintenant est le suivant, mais je ne comprends pas vraiment ce qu'il fait (malgré le fait que je l'ai modifié pour qu'il gère ces scénarios profondément imbriqués grâce au débogueur).

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

Cette solution le gère lorsque mon expression est de type Expression<Func<TModel,object>> Et que je passe toutes sortes de types d'objets pour mes paramètres. Lorsque je fais cela, mon expression x => x.Age Est transformée en x => Convert(x.Age) et cela brise les autres solutions ici. Cependant, je ne comprends pas ce qui gère la partie Convert. : - /

1
Jaxidian

Publication croisée de Récupération du nom de la propriété à partir de l'expression lambda

Comme la question a fait allusion, la réponse sournoise est que si vous appelez expression.ToString(), cela vous donnera quelque chose comme:

"o => o.ParentProperty.ChildProperty"

que vous pouvez ensuite simplement sous-chaîne de la première période.

Sur la base de certains tests LinqPad , les performances étaient comparables.

0
drzaus