从Expression <func <tmodel,tproperty>&gt; </func <tmodel,tproperty>中获取字符串属性

时间:2010-05-07 14:57:53

标签: c# lambda

我使用了一些强类型表达式,这些表达式被序列化以允许我的UI代码具有强类型排序和搜索表达式。这些类型为Expression<Func<TModel,TProperty>>,因此使用:SortOption.Field = (p => p.FirstName);。对于这个简单的案例,我已经完美地完成了这项工作。

我用来解析“FirstName”属性的代码实际上重用了我们使用的第三方产品中的一些现有功能,并且它工作得很好,直到我们开始使用深层嵌套属性( SortOption.Field = (p => p.Address.State.Abbreviation);)。此代码在支持深层嵌套属性的需求方面有一些非常不同的假设。

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

要求:

  • 鉴于表达式p => p.FirstName,我需要一串"FirstName"
  • 鉴于表达式p => 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
}

9 个答案:

答案 0 :(得分:92)

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

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

所以很明显,解决这个问题的方法是检查Expression的{​​{1}}属性,直到它不再是MemberExpression为止。


更新:您的问题似乎有一个额外的旋转。可能你有一些看起来的lambda就像MemberExpression ......

Func<T, int>

...但实际上是 a p => p.Age ;在这种情况下,编译器会将上面的表达式转换为:

Func<T, object>

调整此问题实际上并不像看起来那么难。看看我的更新代码,找到一种方法来处理它。请注意,通过抽象代码将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));

答案 1 :(得分:14)

这是一个让你获得字符串表示的方法,即使你有嵌套属性:

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

答案 2 :(得分:9)

来自"FirstName"

p => p.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 => p.FirstName )

答案 3 :(得分:5)

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

答案 4 :(得分:4)

我为此写了一些代码,它似乎有效。

给出以下三个类定义:

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

答案 5 :(得分:2)

来自MVC的ExpressionHelper来源

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

只需参加此课程 - 您就可以避免依赖MVC并为您处理特殊的边缘案例。

免责声明:不确定许可是如何运作这样的类 - 但似乎非常无害

答案 6 :(得分:2)

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

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

您可以像以下一样使用它:

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

然而,这种方法比其他人发布的错误检查更少 - 基本上它被认为是理所当然的,这在你的应用程序中可能不是一个安全的假设。

答案 7 :(得分:1)

我现在100%工作的代码如下所示,但我并不真正理解它正在做什么(尽管我修改了它以使其能够处理这些深度嵌套的场景,这要归功于调试器)。 / p>

    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<Func<TModel,object>>类型时,此解决方案处理它,并且我为我的参数传递了各种对象类型。当我执行此操作时,我的x => x.Age表达式将变为x => Convert(x.Age)并且会打破其他解决方案。但是,我不明白这会处理Convert部分的内容。 : - /

答案 8 :(得分:0)

Retrieving Property name from lambda expression

的交叉发布

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

"o => o.ParentProperty.ChildProperty"

然后你可以从第一个时期子串。

根据某些LinqPad tests,效果具有可比性。