私のテストフレームワークを作成する際に、私は奇妙な問題を発見しました。Type.GetProperties()とラムダ式からPropertyInfoを比較
同じタイプのオブジェクトをプロパティで比較できますが、その一部を無視する可能性のある静的クラスを作成したいとします。
私はこのためのシンプルな流暢なAPIを持つようにしたいので、与えられたオブジェクトがId
とName
(彼らが一致するかどうかは確認されません)を除くすべてのプロパティに等しい場合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));
}
問題がDoMyEquals
でExcept
方法です。
FindProperty
によって返されるプロパティは、Type.GetProperties
によって返されるプロパティと等しくありません。違いは、PropertyInfo.ReflectedType
です。
かかわらず、私のオブジェクトの種類に、
FindProperty
は、反射型はBase
であることを私に伝えます。Type.GetProperties
によって返さ特性は、実際のオブジェクトの種類に応じて、
Base
またはDerived
へのReflectedType
セットを有しています。
私はそれを解決する方法がわかりません。私はラムダのパラメータのタイプをチェックすることができましたが、次のステップではIgnore(x=>x.Some.Deep.Property)
のような構造を許可したいので、おそらくしません。
PropertyInfo
をどのように比較するか、またはlambdaから適切に検索する方法については、ご了承ください。
GetPropertiesのBindingFlags.FlattenHierarchyの値で再生しようとしましたか?それが何か変わるかどうか確認してください。 –
そこには運がありませんが、ご提案いただきありがとうございます。私は思う** BindingFlagsはメンバーが返されるものだけを変更することができますが、彼ら自身のプロパティには影響しません。私はその解決策がFindPropertyを使っていると信じています。 –
メンバーにGetPropertyを実行させた後で、FindPropertyにもう1つのハッキーなステップを追加することがあります(式で取得することもできます)。それはハックですが、うまくいくかもしれません。 –