Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compare PropertyInfo from Type.GetProperties() and lambda expressions

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.

like image 237
Piotr Zierhoffer Avatar asked Apr 11 '12 20:04

Piotr Zierhoffer


2 Answers

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()
like image 102
payo Avatar answered Nov 20 '22 15:11

payo


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.

like image 33
Tony Tanzillo Avatar answered Nov 20 '22 14:11

Tony Tanzillo