While creating my testing framework I’ve found a strange problem.
I want to create a static class that would allow me to compare objects of the same type by their properties, but with possibility to ignore some of them.
I want to have a simple fluent API for this, so a call like TestEqualityComparer.Equals(first.Ignore(x=>x.Id).Ignore(y=>y.Name), second); will return true if the given objects are equal on every property except Id and Name (they will not be checked for equality).
Here goes my code. Of course it’s a trivial example (with some obvious overloads of methods missing), but I wanted to extract the simplest code possible. The real case scenario’s a bit more complex, so I don’t really want to change the approach.
The method FindProperty is almost a copy-paste from AutoMapper library.
Object wrapper for fluent API:
public class TestEqualityHelper<T>
{
public List<PropertyInfo> IgnoredProps = new List<PropertyInfo>();
public T Value;
}
Fluent stuff:
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");
}
}
The actual comparer:
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)));
}
}
That’s basically it.
And here are two test snippets, the first one works, the second one fails:
//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));
}
The problem is with the Except method in DoMyEquals.
Properties returned by FindProperty are not equal to those returned by Type.GetProperties. The difference I spot is in PropertyInfo.ReflectedType.
-
regardless to the type of my objects,
FindPropertytells me that the reflected type isBase. -
properties returned by
Type.GetPropertieshave theirReflectedTypeset toBaseorDerived, depending on the type of actual objects.
I don’t know how to solve it. I could check the type of the parameter in lambda, but in the next step I want to allow constructs like Ignore(x=>x.Some.Deep.Property), so it probably will not do.
Any suggestion on how to compare PropertyInfo‘s or how to retrieve them from lambdas properly would be appreciated.
The reason
FindPropertyis telling you the reflectedTypeisBaseis because that’s the class the lambda would use for the invocation.You probably know this 🙂
Instead of GetProperties() from Type, could you use this
To explain more about why the lambda is actually using the Base method directly, and you see essentially a different PropertyInfo, might be better explained looking at the IL
Consider this code:
And here is the IL for b.Id
And the IL for d.Id