2012-04-11 96 views
11

在创建我的测试框架时,我发现了一个奇怪的问题。比较Type.GetProperties()和lambda表达式中的PropertyInfo

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

我要对此有一个简单流畅的API,所以如果给定对象是上除了IdName每个属性(他们不会被检查是否相等)等于像TestEqualityComparer.Equals(first.Ignore(x=>x.Id).Ignore(y=>y.Name), second);通话将返回true。

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

方法FindProperty几乎是从AutoMapper library复制粘贴。为流畅的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)); 
} 

的问题是,在DoMyEqualsExcept方法。

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

  • 不管我对象的类型,FindProperty告诉我,反射型为Base。通过Type.GetProperties返回

  • 属性都有自己的一套ReflectedTypeBaseDerived,根据实际对象的类型。

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

任何有关如何比较PropertyInfo或如何从lambdas正确检索他们的建议,将不胜感激。

+1

你有没有试过玩GetProperties中的BindingFlags.FlattenHierarchy值?看看它是否改变了什么? – 2012-04-11 20:50:05

+1

那里没有运气,但谢谢你的建议。我**认为** BindingFlags只能改变_which_成员返回,但他们不会影响他们自己的属性。我相信这个解决方案会在FindProperty中发挥作用。 – 2012-04-11 21:00:37

+1

在获得成员运行GetProperty的类型(也可以通过表达式获取)和成员名称之后,可能会向FindProperty添加第二个冒险步骤?这是一个黑客攻击,但它可能会起作用。 – 2012-04-11 21:04:37

回答

5

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

你可能知道这个:)

相反的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); 
} 

要解释一下为什么拉姆达实际上是直接使用基本方法,以及你看到本质不同的PropertyInfo,可能会更好地解释看IL

考虑以下代码:

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

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

这里是IL为b.Id

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() 
+0

这看起来很好,但我真的不知道,为什么lambda会使用Base?没有投射,'lambdaExpression.Parameters [0] .Type'表示'Derived'。你能解释为什么会发生这样的事情吗? (一个解释性链接或一些关键字可能绰绰有余;-)) – 2012-04-11 21:26:58

+0

lambda参数Type是声明的参数的类型,但实际的方法调用是_using_基类型进行调用(方法在基本类型)。 – payo 2012-04-11 21:33:59

+0

@xavier请参阅我的答案中的其他信息[另外,如果此解决方案有效,并且有6人已投票支持 - 即使是一个人出演了它,为什么不爱我的答案:(我有时候不明白) – payo 2012-04-11 21:41:59

5

不知道这是否会有所帮助,但我已经注意到,MetaDataToken财产两个PropertyInfo实例的值是相等的,如果两个实例引用相同的逻辑属性,则不管ReflectedType是哪一个。也就是说,两个PropertyInfo实例的Name,PropertyType,DeclaringType和index参数都是相等的。

+3

这非常有趣!根据msdn,“MetadataToken”与“Module”结合,可以唯一标识元素。谢谢! – 2012-04-14 22:16:52