问题 比较Type.GetProperties()和lambda表达式中的PropertyInfo


在创建我的测试框架时,我发现了一个奇怪的问题。

我想创建一个静态类,允许我通过属性比较相同类型的对象,但有可能忽略其中的一些。

我希望有一个简单的流畅API,所以像这样的电话 TestEqualityComparer.Equals(first.Ignore(x=>x.Id).Ignore(y=>y.Name), second); 如果给定对象在每个属性上相等,则返回true Id 和 Name (他们不会检查他们的平等)。

这是我的代码。当然,这是一个微不足道的例子(有些人 明显 缺少方法的重载),但我想提取最简单的代码。真实情况有点复杂,所以我真的不想改变方法。

方法 FindProperty 几乎是一个复制粘贴 AutoMapper库

用于流畅API的对象包装器:

public class TestEqualityHelper<T>
{
    public List<PropertyInfo> IgnoredProps = new List<PropertyInfo>();
    public T Value;
}

流利的东西:

public static class FluentExtension
{
    //Extension method to speak fluently. It finds the property mentioned
    // in 'ignore' parameter and adds it to the list.
    public static TestEqualityHelper<T> Ignore<T>(this T value,
         Expression<Func<T, object>> ignore)
    {
        var eh = new TestEqualityHelper<T> { Value = value };

        //Mind the magic here!
        var member = FindProperty(ignore);
        eh.IgnoredProps.Add((PropertyInfo)member);
        return eh;
    }

    //Extract the MemberInfo from the given lambda
    private static MemberInfo FindProperty(LambdaExpression lambdaExpression)
    {
        Expression expressionToCheck = lambdaExpression;

        var done = false;

        while (!done)
        {
            switch (expressionToCheck.NodeType)
            {
                case ExpressionType.Convert:
                    expressionToCheck 
                        = ((UnaryExpression)expressionToCheck).Operand;
                    break;
                case ExpressionType.Lambda:
                    expressionToCheck
                        = ((LambdaExpression)expressionToCheck).Body;
                    break;
                case ExpressionType.MemberAccess:
                    var memberExpression 
                        = (MemberExpression)expressionToCheck;

                    if (memberExpression.Expression.NodeType 
                          != ExpressionType.Parameter &&
                        memberExpression.Expression.NodeType 
                          != ExpressionType.Convert)
                    {
                        throw new Exception("Something went wrong");
                    }

                    return memberExpression.Member;
                default:
                    done = true;
                    break;
            }
        }

        throw new Exception("Something went wrong");
    }
}

实际的比较器:

public static class TestEqualityComparer
{
    public static bool MyEquals<T>(TestEqualityHelper<T> a, T b)
    {
        return DoMyEquals(a.Value, b, a.IgnoredProps);
    }

    private static bool DoMyEquals<T>(T a, T b,
        IEnumerable<PropertyInfo> ignoredProperties)
    {
        var t = typeof(T);
        IEnumerable<PropertyInfo> props;

        if (ignoredProperties != null && ignoredProperties.Any())
        {
            //THE PROBLEM IS HERE!
            props =
                t.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                    .Except(ignoredProperties);
        }
        else
        {
            props = 
                t.GetProperties(BindingFlags.Instance | BindingFlags.Public);
        }
        return props.All(f => f.GetValue(a, null).Equals(f.GetValue(b, null)));
    }
}

基本上就是这样。

这里有两个测试片段,第一个工作,第二个失败:

//These are the simple objects we'll compare
public class Base
{
    public decimal Id { get; set; }
    public string Name { get; set; }
}
public class Derived : Base
{    }

[TestMethod]
public void ListUsers()
{
   //TRUE
   var f = new Base { Id = 5, Name = "asdas" };
   var s = new Base { Id = 6, Name = "asdas" };
   Assert.IsTrue(TestEqualityComparer.MyEquals(f.Ignore(x => x.Id), s));

   //FALSE
   var f2 = new Derived { Id = 5, Name = "asdas" };
   var s2 = new Derived { Id = 6, Name = "asdas" };
   Assert.IsTrue(TestEqualityComparer.MyEquals(f2.Ignore(x => x.Id), s2));
}

问题在于 Except 方法 DoMyEquals

返回的属性 FindProperty 不等于那些归来的 Type.GetProperties。我发现的差异在于 PropertyInfo.ReflectedType

  • 无论我的物体类型如何, FindProperty 告诉我,反映的类型是 Base

  • 返回的属性 Type.GetProperties 有他们的 ReflectedType 设置 Base 要么 Derived,取决于实际对象的类型。

我不知道如何解决它。我可以检查lambda中参数的类型,但在下一步中我想允许像这样的结构 Ignore(x=>x.Some.Deep.Property),所以它可能不会这样做。

关于如何比较的任何建议 PropertyInfo或者如何正确地从lambdas中检索它们将不胜感激。


4762
2018-04-11 20:38


起源

您是否尝试过使用GetProperties中的BindingFlags.FlattenHierarchy值?看看它有什么变化吗? - Avner Shahar-Kashtan
那里没有运气,但谢谢你的建议。一世 认为 BindingFlags只能改变 哪一个 成员被退回,但他们不会影响他们自己的属性。我相信解决方案可以与FindProperty一起使用。 - Piotr Zierhoffer
在您获得成员后,可能会在FindProperty上添加第二个hacky步骤 - 使用成员名称对类型(您也可以通过表达式获取)运行GetProperty?这是一个黑客,但它可能会奏效。 - Avner Shahar-Kashtan
嗯,这是一个很好的解决方案,但如果没有进一步的组合,它将不适用于x.Nested.Properties。我认为@payo解决方案,我们单独使用FindProperty,可以更加扩展。是的,我很容易改变主意;-)谢谢你! :) - Piotr Zierhoffer
另见相关: stackoverflow.com/questions/6658669/...。我认为它有一个更好的解决方案,为您提供实际的 PropertyInfo。 - nawfal


答案:


原因 FindProperty 告诉你反映的 Type 是 Base 是因为这是lambda用于调用的类。

你可能知道这个:)

你可以使用它来代替Type中的GetProperties()

static IEnumerable<PropertyInfo> GetMappedProperties(Type type)
{
  return type
    .GetProperties()
    .Select(p => GetMappedProperty(type, p.Name))
    .Where(p => p != null);
}

static PropertyInfo GetMappedProperty(Type type, string name)
{
  if (type == null)
    return null;

  var prop = type.GetProperty(name);

  if (prop.DeclaringType == type)
    return prop;
  else
    return GetMappedProperty(type.BaseType, name);
}

为了解释为什么lambda实际上直接使用Base方法,并且你看到一个不同的PropertyInfo,可能会更好地解释看IL

考虑以下代码:

static void Foo()
{
  var b = new Base { Id = 4 };
  var d = new Derived { Id = 5 };

  decimal dm = b.Id;
  dm = d.Id;
}

这里是b.Id的IL

IL_002f: callvirt instance valuetype [mscorlib]System.Decimal ConsoleApplication1.Base::get_Id()

和IL为d.Id

IL_0036: callvirt instance valuetype [mscorlib]System.Decimal ConsoleApplication1.Base::get_Id()

5
2018-04-11 21:14



这看起来很好,但我真的不知道,为什么lambda会使用Base?没有铸造, lambdaExpression.Parameters[0].Type 说 Derived。你能解释一下为什么会这样吗? (解释性链接或某些关键词绰绰有余;-)) - Piotr Zierhoffer
lambda参数Type是声明的参数的Type,但实际的方法调用是 运用 进行调用的基类型(方法是基类型)。 - payo
@xavier在我的回答中查看其他信息[另外,如果这个解决方案有效并且有6个人对它进行了投票 - 即使有一个人出演了它,为什么不爱我的回答:(我有时不理解) - payo
请给我一点,我正在编码和测试它;-) - Piotr Zierhoffer
@payo这个问题本身是投票和主演,而不是你的答案...... - Preston Guillot


不知道这是否有帮助,但我注意到两个PropertyInfo实例的MetaDataToken属性值相等,如果两个实例都引用相同的逻辑属性,则无论两者的ReflectedType如何。也就是说,两个PropertyInfo实例的Name,PropertyType,DeclaringType和index参数都是相等的。


5
2018-04-13 05:35



这很有意思!根据msdn, MetadataToken,与...结合 Module,唯一标识元素。谢谢! - Piotr Zierhoffer