Compare PropertyInfo from Type.GetProperties() and lambda expressions

c#, equality, reflection

Solution

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()

Problem

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.

Original source

Related problems