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, FindProperty
tells me that the reflected type is Base
.
properties returned by Type.GetProperties
have their ReflectedType
set to Base
or Derived
, 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 FindProperty
is telling you the reflected Type
is Base
is 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
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);
}
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:
static void Foo()
{
var b = new Base { Id = 4 };
var d = new Derived { Id = 5 };
decimal dm = b.Id;
dm = d.Id;
}
And here is the IL for b.Id
IL_002f: callvirt instance valuetype [mscorlib]System.Decimal ConsoleApplication1.Base::get_Id()
And the IL for d.Id
IL_0036: callvirt instance valuetype [mscorlib]System.Decimal ConsoleApplication1.Base::get_Id()
Don't know if this helps, but I've noticed that the MetaDataToken property value of two PropertyInfo instances are equal, if both instances refer to the same logical property, regardless of the ReflectedType of either. That is, the Name, PropertyType, DeclaringType and index parameters of both PropertyInfo instances are all equal.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With