11

テスト フレームワークを作成しているときに、奇妙な問題が見つかりました。

プロパティによって同じタイプのオブジェクトを比較できるようにする静的クラスを作成したいと考えていますが、それらの一部を無視する可能性があります。

これには単純な流暢な API が必要なので、指定されたオブジェクトがandTestEqualityComparer.Equals(first.Ignore(x=>x.Id).Ignore(y=>y.Name), second);を除くすべてのプロパティで等しい場合、like の呼び出しは true を返します(等しいかどうかはチェックされません)。IdName

これが私のコードです。もちろん、これは些細な例ですが (メソッドの明らかなオーバーロードがいくつか欠落しています)、可能な限り単純なコードを抽出したかったのです。実際のシナリオはもう少し複雑なので、アプローチを変更したくありません。

この方法は、 AutoMapper ライブラリFindPropertyからほとんどコピーして貼り付けたものです。

流れるような 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 つのテスト スニペットがあります。最初の 1 つは機能し、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));
}

問題は のExceptメソッドにありDoMyEqualsます。

によって返されるプロパティは、によって返されるプロパティFindPropertyと等しくありませんType.GetProperties。私が見つけた違いはPropertyInfo.ReflectedType.

  • オブジェクトのタイプに関係なくFindProperty、反映されたタイプが であることがわかりますBase

  • によって返されるプロパティは、実際のオブジェクトのタイプに応じてまたはType.GetPropertiesReflectedType設定されます。BaseDerived

解決方法がわかりません。ラムダでパラメーターの型を確認できましたが、次のステップで のような構造を許可したいIgnore(x=>x.Some.Deep.Property)ので、おそらくうまくいかないでしょう。

を比較する方法、PropertyInfoまたはラムダからそれらを適切に取得する方法についての提案をいただければ幸いです。

4

2 に答える 2

5

これが役立つかどうかはわかりませんが、両方のインスタンスが同じ論理プロパティを参照している場合、いずれかの ReflectedType に関係なく、2 つの PropertyInfo インスタンスの MetaDataToken プロパティ値が等しいことに気付きました。つまり、両方の PropertyInfo インスタンスの Name、PropertyType、DeclaringType、および index パラメータはすべて同じです。

于 2012-04-13T05:35:31.380 に答える
5

その理由FindPropertyは、反映されたTypeisBaseがラムダが呼び出しに使用するクラスであるためです。

あなたはおそらくこれを知っています:)

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

ラムダが実際に 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()

そしてd.IdのIL

IL_0036: callvirt instance valuetype [mscorlib]System.Decimal ConsoleApplication1.Base::get_Id()
于 2012-04-11T21:14:40.527 に答える