10

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.

Piotr Zierhoffer
  • 5,005
  • 1
  • 38
  • 59
  • 1
    Have you tried playing with the BindingFlags.FlattenHierarchy value in GetProperties? See if it changes anything? – Avner Shahar-Kashtan Apr 11 '12 at 20:50
  • 1
    No luck there, but thank you for a suggestion. I **think** BindingFlags can only change _which_ members are returned, but they will not affect their own properties. I believe that the solution would be in some play with FindProperty. – Piotr Zierhoffer Apr 11 '12 at 21:00
  • 1
    Maybe adding a second, hacky step to FindProperty, after you've gotten the Member - run GetProperty on the type (which you can also get via the expression) with the member name? It's a hack, but it might work. – Avner Shahar-Kashtan Apr 11 '12 at 21:04
  • 1
    Well, it's a fine solution, but it won't work with x.Nested.Properties without further combinations. I think @payo solution, where we leave FindProperty alone, is a bit more expandable. Yeah, I change my mind easily ;-) Thank you anyway! :) – Piotr Zierhoffer Apr 11 '12 at 21:35
  • Also see related: http://stackoverflow.com/questions/6658669/lambda-expression-not-returning-expected-memberinfo. I think it has a better solution which gives you the actual `PropertyInfo`. – nawfal Jun 01 '13 at 14:50

2 Answers2

5

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()
payo
  • 4,501
  • 1
  • 24
  • 32
  • This looks fine, but I don't really know, why would lambda use Base? There is no casting, `lambdaExpression.Parameters[0].Type` says `Derived`. Could you please explain why does it happen like this? (an explanatory link or some keywords would be more than enough ;-) ) – Piotr Zierhoffer Apr 11 '12 at 21:26
  • The lambda parameter Type is the Type of the parameter, as declared, but the actual method invocation is _using_ the base type to make the call (the method is in the base type). – payo Apr 11 '12 at 21:33
  • @xavier See the additional info in my answer [also, if this solution works and 6 people have upvoted it - even one starred it, why no love for my answer :( I don't understand SO sometimes] – payo Apr 11 '12 at 21:41
  • Please, give me a minute, I'm coding and testing it ;-) – Piotr Zierhoffer Apr 11 '12 at 21:47
  • @PrestonGuillot I understand that perfectly - I'm just whining that my efforts are seldom profitable in SO cred :). It'd be awesome if all the people liking this question also liked the answer :) [manually] – payo Apr 11 '12 at 21:50
  • Everything works just fine, thank you. I'm no IL expert, but it seems understandable now. Still, I don't like this behavior ;] Props for @payo! It will still need some work to enable filtering out nested properties, but it's good anyway. – Piotr Zierhoffer Apr 11 '12 at 23:46
5

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.

  • 3
    This is very interesting! According to msdn, `MetadataToken`, in combination with `Module`, uniquely identifies the element. Thank you! – Piotr Zierhoffer Apr 14 '12 at 22:16