从lambda表达式中获取属性名称

598

有没有更好的方法在通过lambda表达式传递属性名时获取属性名?以下是我目前拥有的代码。

例如:

GetSortingInfo<User>(u => u.UserId);

只有当属性是字符串时,将其转换为成员表达式才会起作用。因为并非所有属性都是字符串,所以我不得不使用对象,但这样对于那些属性将返回一元表达式。

public static RouteValueDictionary GetInfo<T>(this HtmlHelper html, 
    Expression<Func<T, object>> action) where T : class
{
    var expression = GetMemberInfo(action);
    string name = expression.Member.Name;

    return GetInfo(html, name);
}

private static MemberExpression GetMemberInfo(Expression method)
{
    LambdaExpression lambda = method as LambdaExpression;
    if (lambda == null)
        throw new ArgumentNullException("method");

    MemberExpression memberExpr = null;

    if (lambda.Body.NodeType == ExpressionType.Convert)
    {
        memberExpr = 
            ((UnaryExpression)lambda.Body).Operand as MemberExpression;
    }
    else if (lambda.Body.NodeType == ExpressionType.MemberAccess)
    {
        memberExpr = lambda.Body as MemberExpression;
    }

    if (memberExpr == null)
        throw new ArgumentException("method");

    return memberExpr;
}

2
我已经根据您的评论进行了更新;但是使用lambda获取字符串以便使用动态LINQ似乎是在反其道而行之...如果您使用lambda,请继续使用lambda ;-p 您不必一步完成整个查询 - 您可以使用“常规/lambda” OrderBy,“动态LINQ/string” Where等。 - Marc Gravell
2
可能是 get-property-name-and-type-using-lambda-expression 的重复问题。 - nawfal
5
з»ҷеӨ§е®¶дёҖдёӘжҸҗзӨәпјҡеҸӘдҪҝз”ЁжӯӨеӨ„еҲ—еҮәзҡ„MemberExpressionж–№жі•иҺ·еҸ–жҲҗе‘ҳзҡ„еҗҚз§°пјҢиҖҢдёҚжҳҜиҺ·еҸ–е®һйҷ…зҡ„MemberInfoжң¬иә«пјҢеӣ дёәеңЁжҹҗдәӣвҖңжҙҫз”ҹ:еҹәзЎҖвҖқеңәжҷҜдёӯпјҢиҝ”еӣһзҡ„MemberInfoдёҚиғҪдҝқиҜҒжҳҜеҸҚе°„зұ»еһӢгҖӮиҜҰи§Ғlambda-expression-not-returning-expected-memberinfoгҖӮжӣҫз»Ҹи®©жҲ‘зҠҜдәҶй”ҷгҖӮжҺҘеҸ—зҡ„зӯ”жЎҲд№ҹеӯҳеңЁжӯӨй—®йўҳгҖӮ - nawfal
1
@nawfal 猜测你的意思是“派生”。 - George Birbilis
4
从C# 6开始,您可以直接使用nameof(),例如:nameof(User.UserId)。不需要任何辅助方法,它在编译时被替换! - S.Serpooshan
显示剩余3条评论
23个回答

408

最近我做了一个非常类似的事情,用于创建类型安全的OnPropertyChanged方法。

这里有一个方法,它将返回表达式的PropertyInfo对象。如果表达式不是属性,则会抛出异常。

public static PropertyInfo GetPropertyInfo<TSource, TProperty>(
    TSource source,
    Expression<Func<TSource, TProperty>> propertyLambda)
{
    if (propertyLambda.Body is not MemberExpression member)
    {
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a method, not a property.",
            propertyLambda.ToString()));
    }

    if (member.Member is not PropertyInfo propInfo)
    {
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a field, not a property.",
            propertyLambda.ToString()));
    }

    Type type = typeof(TSource);
    if (propInfo.ReflectedType != null && 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;
}

source参数被用于让编译器对方法调用进行类型推断。您可以执行以下操作:

var propertyInfo = GetPropertyInfo(someUserObject, u => u.UserID);

9
为什么最后需要检查TSource?Lambda表达式是强类型的,我认为这并不必要。 - HappyNomad
21
截至2012年,类型推断在没有源参数的情况下也可以正常工作。 - HappyNomad
6
想象一个对象,它的成员之一是第三种类型的实例。u => u.OtherType.OtherTypesProperty 会创建这样的情况,并检查最后一个语句。 - joshperry
9
这句话的意思是:“@GrayKing,这不就等同于写成 if(!propInfo.ReflectedType.IsAssignableFrom(type)) 吗?”请注意,我已经尽力使翻译更加通俗易懂,但没有改变原文的含义,并且没有提供任何额外的解释或内容。 - Connell
6
异常消息格式化中的ToString()是多余的。 - Darius Kucinskas
显示剩余3条评论

239

我发现另一种方法是将源和属性强类型化,并明确推断lambda的输入。不确定这是否是正确的术语,但这是结果。

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

然后像这样调用它。

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

看这里,它就能正常工作了。


5
这个解决方案需要稍作更新。请查看下面的文章 - 这里是一个链接:https://dev59.com/HGw15IYBdhLWcg3wYKmo - Pavel Cermak
3
只有在使用ASP.NET MVC且仅限于UI层(HtmlHelper)时,它才是一种选择。 - Marc
6
从C# 6.0开始,您可以使用GetInfo(nameof(u.UserId))。该函数的具体含义需要根据上下文来确定,无法进行解释。 - Vladislav
3
在.NET Core中,我需要使用以下代码:var name = ((MemberExpression) ((UnaryExpression) accessor.Body).Operand).Member.Name - Falk
1
这个解决方案无法处理“UnaryExpression”。它不能用于“double”或“int”。虽然@Falk的评论不允许使用“string”(因为它无法处理“MemberExpression”),但还有其他解决方案可以避免这些缺点。 - Jack Miller

159

我也曾尝试这个,写了这个代码。虽然没有完全测试,但似乎能够处理值类型问题(你遇到的一元表达式问题)。

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

3
最近尝试了一下(来自另一个问题),发现它不能处理子属性:o => o.Thing1.Thing2会返回Thing2,而不是Thing1.Thing2。如果您尝试在EntityFramework中使用它进行包含操作,则是错误的。 - drzaus
1
AKA(如果field.Body是UnaryExpression,则为((UnaryExpression)field.Body).Operand,否则为field.Body)作为MemberExpression。 - user3638471

60
public string GetName<TSource, TField>(Expression<Func<TSource, TField>> Field)
{
    return (Field.Body as MemberExpression ?? ((UnaryExpression)Field.Body).Operand as MemberExpression).Member.Name;
}

这段代码处理成员表达式和一元表达式。不同之处在于,如果您的表达式表示值类型,则会得到一个UnaryExpression,而如果您的表达式表示引用类型,则会得到一个MemberExpression。所有内容都可以转换为对象,但是值类型必须装箱。这就是为什么存在UnaryExpression的原因。参考资料。

为了易读性(@Jowen),这里提供了一个扩展等效版本:

public string GetName<TSource, TField>(Expression<Func<TSource, TField>> Field)
{
    if (object.Equals(Field, null))
    {
        throw new NullReferenceException("Field is required");
    }

    MemberExpression expr = null;

    if (Field.Body is MemberExpression)
    {
        expr = (MemberExpression)Field.Body;
    }
    else if (Field.Body is UnaryExpression)
    {
        expr = (MemberExpression)((UnaryExpression)Field.Body).Operand;
    }
    else
    {
        const string Format = "Expression '{0}' not supported.";
        string message = string.Format(Format, Field);

        throw new ArgumentException(message, "Field");
    }

    return expr.Member.Name;
}

@flem,我为了可读性省略了<TField>,这样有问题吗?LambdaExpressions.GetName<Basket>(m => m.Quantity) - Soren
1
@soren 我相信比我更了解的人可能会建议你在传递值类型表达式时打开代码的潜在不必要的装箱/拆箱,但因为该表达式在此方法中从未被编译和评估,所以这可能不是一个问题。 - Paul Fleming

58

使用C# 7模式匹配:

public static string GetMemberName<T>(this Expression<T> expression)
{
    switch (expression.Body)
    {
        case MemberExpression m:
            return m.Member.Name;
        case UnaryExpression u when u.Operand is MemberExpression m:
            return m.Member.Name;
        default:
            throw new NotImplementedException(expression.GetType().ToString());
    }
}

例子:

public static RouteValueDictionary GetInfo<T>(this HtmlHelper html, 
    Expression<Func<T, object>> action) where T : class
{
    var name = action.GetMemberName();
    return GetInfo(html, name);
}

[更新] C# 8模式匹配:

public static string GetMemberName<T>(this Expression<T> expression) => expression.Body switch
{
    MemberExpression m => m.Member.Name,
    UnaryExpression u when u.Operand is MemberExpression m => m.Member.Name,
    _ => throw new NotImplementedException(expression.GetType().ToString())
};

27

21

这是一个通用的实现,可以获取结构体/类/接口/委托/数组的字段/属性/索引器/方法/扩展方法/委托的字符串名称。我已经测试了静态/实例和非泛型/泛型变量的各种组合。

//involves recursion
public static string GetMemberName(this LambdaExpression memberSelector)
{
    Func<Expression, string> nameSelector = null;  //recursive func
    nameSelector = e => //or move the entire thing to a separate recursive method
    {
        switch (e.NodeType)
        {
            case ExpressionType.Parameter:
                return ((ParameterExpression)e).Name;
            case ExpressionType.MemberAccess:
                return ((MemberExpression)e).Member.Name;
            case ExpressionType.Call:
                return ((MethodCallExpression)e).Method.Name;
            case ExpressionType.Convert:
            case ExpressionType.ConvertChecked:
                return nameSelector(((UnaryExpression)e).Operand);
            case ExpressionType.Invoke:
                return nameSelector(((InvocationExpression)e).Expression);
            case ExpressionType.ArrayLength:
                return "Length";
            default:
                throw new Exception("not a proper member selector");
        }
    };

    return nameSelector(memberSelector.Body);
}

这件事也可以用简单的while循环来写:

//iteration based
public static string GetMemberName(this LambdaExpression memberSelector)
{
    var currentExpression = memberSelector.Body;

    while (true)
    {
        switch (currentExpression.NodeType)
        {
            case ExpressionType.Parameter:
                return ((ParameterExpression)currentExpression).Name;
            case ExpressionType.MemberAccess:
                return ((MemberExpression)currentExpression).Member.Name;
            case ExpressionType.Call:
                return ((MethodCallExpression)currentExpression).Method.Name;
            case ExpressionType.Convert:
            case ExpressionType.ConvertChecked:
                currentExpression = ((UnaryExpression)currentExpression).Operand;
                break;
            case ExpressionType.Invoke:
                currentExpression = ((InvocationExpression)currentExpression).Expression;
                break;
            case ExpressionType.ArrayLength:
                return "Length";
            default:
                throw new Exception("not a proper member selector");
        }
    }
}

我喜欢递归的方法,虽然第二个方法可能更容易阅读。可以像这样调用它:

I like the recursive approach, though the second one might be easier to read. One can call it like:


someExpr = x => x.Property.ExtensionMethod()[0]; //or
someExpr = x => Static.Method().Field; //or
someExpr = x => VoidMethod(); //or
someExpr = () => localVariable; //or
someExpr = x => x; //or
someExpr = x => (Type)x; //or
someExpr = () => Array[0].Delegate(null); //etc

string name = someExpr.GetMemberName();

打印最后一个成员。

注意:

  1. 对于类似 A.B.C 的链接表达式,返回 "C"。

  2. 这不适用于 const,数组索引器或 enum (不可能覆盖所有情况)。


20

在处理 Array 时,有一个边缘情况需要注意。虽然 'Length' 被公开为属性,但你不能在先前提出的任何解决方案中使用它。

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

现在的示例用法:
int[] someArray = new int[1];
Console.WriteLine(PropertyNameFromExpr( () => someArray.Length ));

如果PropertyNameFromUnaryExpr没有检查ArrayLength,则会将"someArray"打印到控制台(编译器似乎为了优化,在调试模式下直接访问支持的Length field,因此需要特殊处理)。

17

我发现一些深入解析MemberExpression/UnaryExpression建议答案没有捕获嵌套的子属性。

例如,o => o.Thing1.Thing2 返回的是 Thing1 而不是 Thing1.Thing2

如果你要使用 EntityFramework 的 DbSet.Include(...) ,这种区别很重要。

我发现只需解析 Expression.ToString() 即可正常工作,并且速度比较快。我将其与 UnaryExpression 版本进行了比较,并甚至从 Member/UnaryExpression 获取 ToString 来查看是否更快,但差异微不足道。如果这是一个可怕的想法,请纠正我。

该扩展方法的实现

/// <summary>
/// Given an expression, extract the listed property name; similar to reflection but with familiar LINQ+lambdas.  Technique @via https://dev59.com/AGYr5IYBdhLWcg3wi63d#16647343
/// </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

(检查分隔符甚至可能过度)

演示(LinqPad)

演示+比较代码--https://gist.github.com/zaus/6992590


1
+1 非常有趣。你是否继续在自己的代码中使用这种方法?它工作正常吗?你发现了任何边缘情况吗? - Benjamin Gale
不适用于Korman警告的情况:https://dev59.com/-HRB5IYBdhLWcg3wSVYI#11006147。避免使用hack总是更好的选择。 - nawfal
@nawfal #1 -- 最初的问题是你想要的是 Thing1.Thing2,而不是 Thing1。我说的 Thing2 意思是 o.Thing1.Thing2 的值,这是谓词的重点。我会更新答案以反映这个意图。 - drzaus
@drzaus 抱歉,我还是不太明白你的意思。我真的在努力理解。为什么你会说这里的其他答案返回 Thing1?我认为它根本没有返回那个值。 - nawfal
@drzaus 不用担心,我喜欢讨论 :) 谢谢你的理论,但我已经知道了...我想说的是,这里包括你提供的方法大多数都会得到Grandbaby,而不是你所说的Child。那就是我的全部观点。虽然我认为这种方法非常脆弱,但我没有争论你的方法不能完整地给出Child.Grandbaby。表达式是从右到左遍历的。 - nawfal
显示剩余11条评论

17

这是 Cameron 提出的方法 的更新。第一个参数不是必需的。

public PropertyInfo GetPropertyInfo<TSource, TProperty>(
    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(
            "Expresion '{0}' refers to a property that is not from type {1}.",
            propertyLambda.ToString(),
            type));

    return propInfo;
}

你可以做如下操作:
var propertyInfo = GetPropertyInfo<SomeType>(u => u.UserID);
var propertyInfo = GetPropertyInfo((SomeType u) => u.UserID);

扩展方法:
public static PropertyInfo GetPropertyInfo<TSource, TProperty>(this TSource source,
    Expression<Func<TSource, TProperty>> propertyLambda) where TSource : class
{
    return GetPropertyInfo(propertyLambda);
}

public static string NameOfProperty<TSource, TProperty>(this TSource source,
    Expression<Func<TSource, TProperty>> propertyLambda) where TSource : class
{
    PropertyInfo prodInfo = GetPropertyInfo(propertyLambda);
    return prodInfo.Name;
}

您可以:

SomeType someInstance = null;
string propName = someInstance.NameOfProperty(i => i.Length);
PropertyInfo propInfo = someInstance.GetPropertyInfo(i => i.Length);

不,他不会将u推断为某种类型,因为没有类型可以推断。你可以使用GetPropertyInfo<SomeType>(u => u.UserID) - Lucas
使用 GetPropertyInfo<SomeType>(u => u.UserID); 会给我返回 _"Using .GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>>) requires 2 type arguments."_。另一种方式 GetPropertyInfo((SomeType u) => u.UserID) 可以正常工作。问题可能出在哪里?(不是使用扩展方法而是静态方法)。 - Guillermo Prandi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接