2012-04-11 8 views
11

私のテストフレームワークを作成する際に、私は奇妙な問題を発見しました。Type.GetProperties()とラムダ式から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))); 
    } 
} 

基本的にはこれだけです。

そして、ここでは2つのテストスニペットあり、最初のものは動作しますが、もう一つは失敗します。

//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によって返さ

  • 特性は、実際のオブジェクトの種類に応じて、BaseまたはDerivedへのReflectedTypeセットを有しています。

私はそれを解決する方法がわかりません。私はラムダのパラメータのタイプをチェックすることができましたが、次のステップではIgnore(x=>x.Some.Deep.Property)のような構造を許可したいので、おそらくしません。

PropertyInfoをどのように比較するか、またはlambdaから適切に検索する方法については、ご了承ください。

+1

GetPropertiesのBindingFlags.FlattenHierarchyの値で再生しようとしましたか?それが何か変わるかどうか確認してください。 –

+1

そこには運がありませんが、ご提案いただきありがとうございます。私は思う** BindingFlagsはメンバーが返されるものだけを変更することができますが、彼ら自身のプロパティには影響しません。私はその解決策がFindPropertyを使っていると信じています。 –

+1

メンバーにGetPropertyを実行させた後で、FindPropertyにもう1つのハッキーなステップを追加することがあります(式で取得することもできます)。それはハックですが、うまくいくかもしれません。 –

答えて

5

理由はFindPropertyは、TypeBaseであることを伝えているのは、それがラムダが呼び出しに使用するクラスだからです。あなたはおそらくタイプからこの:)

代わりの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); 
} 

を使用することができます

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

    decimal dm = b.Id; 
    dm = d.Id; 
} 
:異なるPropertyInfoを考えてみましょうIL

を見て説明し良いかもしれない、基本的にこのコードを参照してください

そして、ここでILはd.Id

ためb.Id

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

そしてILためである

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

これはうまく見えますが、ラムダがBaseを使う理由は分かりません。キャストはありません。 'lambdaExpression.Parameters [0] .Type'は' Derived'を表します。なぜこのように起こるのか説明してください。 (説明リンクまたはいくつかのキーワードが十分である;-)) –

+0

lambdaパラメータTypeは、宣言されているようにパラメータの型ですが、実際のメソッド呼び出しは呼び出しを行うために基本型を使用していますベースタイプ)。 – payo

+0

@xavier私の答えの追加情報を参照してください[また、この解決策が動作し、6人がそれをupvotedしている - 1つも星印、なぜ私の答えの愛:(私は時々理解していない) – payo

5

このことができます場合は知らないが、私はMetaDataToken財産気づきましたどちらかのReflectedTypeに関係なく、両方のインスタンスが同じ論理プロパティを参照する場合、2つのPropertyInfoインスタンスの値は等しくなります。つまり、両方のPropertyInfoインスタンスのName、PropertyType、DeclaringTypeおよびindexパラメータはすべて等しいです。

+3

これは非常に興味深い! msdnによると、 'MetadataToken'は' Module'との組み合わせで要素を一意に識別します。ありがとうございました! –