Retrieving Property name from lambda expression

前端 未结 21 1721
迷失自我
迷失自我 2020-11-21 11:12

Is there a better way to get the Property name when passed in via a lambda expression? Here is what i currently have.

eg.

GetSortingInfo         


        
相关标签:
21条回答
  • 2020-11-21 11:40

    I've found that some of the suggested answers which drill down into the MemberExpression/UnaryExpression don't capture nested/subproperties.

    ex) o => o.Thing1.Thing2 returns Thing1 rather than Thing1.Thing2.

    This distinction is important if you're trying to work with EntityFramework DbSet.Include(...).

    I've found that just parsing the Expression.ToString() seems to work fine, and comparatively quickly. I compared it against the UnaryExpression version, and even getting ToString off of the Member/UnaryExpression to see if that was faster, but the difference was negligible. Please correct me if this is a terrible idea.

    The Extension Method

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

    (Checking for the delimiter might even be overkill)

    Demo (LinqPad)

    Demonstration + Comparison code -- https://gist.github.com/zaus/6992590

    0 讨论(0)
  • 2020-11-21 11:41

    I found another way you can do it was to have the source and property strongly typed and explicitly infer the input for the lambda. Not sure if that is correct terminology but here is the result.

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

    And then call it like so.

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

    and voila it works.

    0 讨论(0)
  • 2020-11-21 11:43

    I recently did a very similar thing to make a type safe OnPropertyChanged method.

    Here's a method that'll return the PropertyInfo object for the expression. It throws an exception if the expression is not a property.

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

    The source parameter is used so the compiler can do type inference on the method call. You can do the following

    var propertyInfo = GetPropertyInfo(someUserObject, u => u.UserID);
    
    0 讨论(0)
  • 2020-11-21 11:43

    There's an edge case when it comes to Array.Length. While 'Length' is exposed as a property, you can't use it in any of the previously proposed solutions.

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

    Now example usage:

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

    If PropertyNameFromUnaryExpr didn't check for ArrayLength, "someArray" would be printed to the console (compiler seems to generate direct access to the backing Length field, as an optimization, even in Debug, thus the special case).

    0 讨论(0)
  • 2020-11-21 11:47

    This is another answer:

    public static string GetPropertyName<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                          Expression<Func<TModel, TProperty>> expression)
        {
            var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
    
            return metaData.PropertyName;
        }
    
    0 讨论(0)
  • 2020-11-21 11:50

    I was playing around with the same thing and worked this up. It's not fully tested but seems to handle the issue with value types (the unaryexpression issue you ran into)

    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;
    }
    
    0 讨论(0)
提交回复
热议问题