从 Expression<Func<TModel,TProperty>> 以字符串形式获取属性

Posted

技术标签:

【中文标题】从 Expression<Func<TModel,TProperty>> 以字符串形式获取属性【英文标题】:Get the property, as a string, from an Expression<Func<TModel,TProperty>> 【发布时间】:2011-02-16 21:14:38 【问题描述】:

我使用了一些被序列化的强类型表达式,以允许我的 UI 代码具有强类型排序和搜索表达式。这些是Expression&lt;Func&lt;TModel,TProperty&gt;&gt; 类型,并按如下方式使用:SortOption.Field = (p =&gt; p.FirstName);。我已经为这个简单的案例完美地工作了。

我用来解析“FirstName”属性的代码实际上是重用我们使用的第三方产品中的一些现有功能,并且效果很好,直到我们开始使用深度嵌套的属性( SortOption.Field = (p =&gt; p.Address.State.Abbreviation);)。这段代码在需要支持深度嵌套的属性方面有一些非常不同的假设。

至于这段代码的作用,我并不真正理解它,与其更改该代码,我想我应该从头开始编写这个功能。但是,我不知道这样做的 方法。我怀疑我们可以做一些比 ToString() 和执行字符串解析更好的事情。那么有什么好的方法来处理琐碎和深度嵌套的情况呢?

要求:

给定表达式p =&gt; p.FirstName,我需要一个"FirstName" 字符串。 给定表达式p =&gt; p.Address.State.Abbreviation 我需要一个"Address.State.Abbreviation" 的字符串

虽然回答我的问题并不重要,但我怀疑我的序列化/反序列化代码可能对将来发现这个问题的其他人有用,所以它在下面。同样,这段代码对这个问题并不重要——我只是认为它可能会对某人有所帮助。请注意,DynamicExpression.ParseLambda 来自 Dynamic LINQ 东西,Property.PropertyToString() 是这个问题的意义所在。

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

【问题讨论】:

C#: Getting Names of properties in a chain from lambda expression的可能重复 @nawfal 这个问题是针对一个稍微不同的事情,也有稍微不同的问题。他们希望将命名空间的每个部分拆分为单独的字符串。我希望它在一个字符串中。此外,该问题的答案 not 处理拆箱(object -> int),这是我的问题的一部分。所以这个问题不是那个问题的重复。 Jaxidian,我明白了,但它对我来说仍然是重复的。返回的字符串数组和作为单个连接字符串返回的相同内容的差异不是差异化因素,考虑到这根本不是更大的问题。是的,它的答案不完整,您可以再问一次。但由于那里的答案已经修改,我想我们现在可以关闭它了。 ***.com/questions/671968/…的可能重复 【参考方案1】:

这是诀窍:这种形式的任何表达......

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

最后,在 之上(在“顶部”)你有:

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

所以很明显,解决这个问题的方法是检查MemberExpressionExpression 属性,直到它本身不再是MemberExpression


更新:您的问题似乎有额外的变化。可能是您有一些 看起来Func&lt;T, int&gt;...

的 lambda
p => p.Age

...但实际上Func&lt;T, object&gt;;在这种情况下,编译器会将上面的表达式转换为:

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

【讨论】:

这适用于我发布的问题,但我刚刚发现我有一个更复杂的场景,因为我将它用作Expression&lt;Func&lt;Person, object&gt;&gt;,所以我可以同时处理int 和@987654344 @。这样做,即使我将其键入为x =&gt; x.Age,表达式也会存储为x =&gt; Convert(x.Age),用于非字符串属性。我实际上已经修改了第三方代码来处理这个问题(我没有意识到),但你的解决方案和答案非常彻底。我将很快发布我正在使用的代码作为另一个答案,但希望看到你的答案适应它。 FWIW,即使将其用作Expression&lt;Func&lt;Person, object&gt;&gt;,您的代码也适用于字符串。 @Jaxidian:我已经用一种可能的方法更新了我的答案来说明您的情况。它适用于您提供的示例。试试看,看看它对你有什么作用! 基于这个答案和 SO 中的其他内容,我组成了一个库:Mariuzzo.Web.Mvc.Extras 来处理这个特定的问题。任何贡献将不胜感激。 如果遇到有关 Expression 类型的命名空间冲突,请指定 System.Linq.Expressions.Expression,而不是 System.Windows.Expression。【参考方案2】:

这里有一个方法可以让你得到字符串表示,即使你有嵌套的属性:

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,则需要在调用 Reverse() 后添加 ToArray(),因为在 .NET 4 中首先添加了采用 IEnumerableString.Join 的重载。

【讨论】:

【参考方案3】:

来自p =&gt; p.FirstName"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),因为它具有用于 html 帮助程序的类似 API...Html.TextBoxFor( p =&gt; p.FirstName )

【讨论】:

【参考方案4】:

另一个简单的方法是使用 System.Web.Mvc.ExpressionHelper.GetExpressionText 方法。在我的下一个打击中,我将写得更详细。看看http://carrarini.blogspot.com/。

【讨论】:

但这需要我向 MVC 和 Web 内容添加依赖项。我不想在 winforms 应用程序、WCF 服务应用程序或 DAL 中执行此操作 - 不是没有办法,不是没有办法!但是,如果这仅在 MVC 应用程序中需要,那么也许这是一个选项。 然后只需获取源代码 - 它的开源代码github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/… 这是 IMO 的发展方向。这是相同的 ExpressionHelper.GetExpressionText 用于 ASPNET Core github.com/aspnet/AspNetCore/blob/master/src/Mvc/…【参考方案5】:

我为此写了一点代码,它似乎可以工作。

给定以下三个类定义:

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

【讨论】:

【参考方案6】:

来自 MVC 的 ExpressionHelper 源在这里

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

只要上这门课 - 你就可以避免依赖 MVC 并为你处理特殊的边缘情况。

免责声明:仅参加这样的课程就不确定许可如何运作 - 但似乎无害

【讨论】:

【参考方案7】:

基于此以及此处的几个相关问题/答案,这是我正在使用的简单方法:

protected string propertyNameFromExpression<T>(Expression<Func<T, object>> prop)

    // http://***.com/questions/2789504/get-the-property-as-a-string-from-an-expressionfunctmodel-tproperty
    // http://***.com/questions/767733/converting-a-net-funct-to-a-net-expressionfunct
    // http://***.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;

你可以像这样使用它:

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

但是,与此处发布的其他方法相比,此方法执行的错误检查更少 - 基本上它认为它被正确调用是理所当然的,这在您的应用程序中可能不是一个安全的假设。

【讨论】:

【参考方案8】:

我现在 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&lt;Func&lt;TModel,object&gt;&gt; 类型并且我为我的参数传递各种对象类型时,此解决方案会处理它。当我这样做时,我的x =&gt; x.Age 表达式变成了x =&gt; Convert(x.Age),这打破了这里的其他解决方案。不过,我不明白这处理Convert 部分的内容。 :-/

【讨论】:

【参考方案9】:

来自Retrieving Property name from lambda expression的交叉发帖

正如问题所暗示的,偷偷摸摸的回答是,如果你打电话给expression.ToString(),它会给你类似的东西:

"o => o.ParentProperty.ChildProperty"

然后您可以从第一个句点中提取子字符串。

基于一些LinqPad tests,性能相当。

【讨论】:

以上是关于从 Expression<Func<TModel,TProperty>> 以字符串形式获取属性的主要内容,如果未能解决你的问题,请参考以下文章

将 .net Func<T> 转换为 .net Expression<Func<T>>

Expression<Func<T,TResult>>和Func<T,TResult>

Func<T> 如何隐式转换为 Expression<Func<T>>?

Expression<Func<T,TResult>>和Func<T,TResult> 与AOP与WCF

Expression<Func<T,bool>> 声明是啥意思?

从 c# 表达式中删除不需要的装箱转换