Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this a bug in .Net reflection?

ANSWER is: No, this is not a bug. The difference is in the ReflectedType.

So the real question here is: Is there a way of comparing two PropertyInfo objects, for the same property, but reflected from different types, so that it returns true?

Original question

This code produces two PropertyInfo objects for the very same property, by using two different ways. It comes that, these property infos compare differently somehow. I have lost some time trying to figure out this out.

What am I doing wrong?

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace TestReflectionError
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.BufferWidth = 200;
            Console.WindowWidth = 200;

            Expression<Func<object>> expr = () => ((ClassA)null).ValueA;
            PropertyInfo pi1 = (((expr as LambdaExpression)
                .Body as UnaryExpression)
                .Operand as MemberExpression)
                .Member as PropertyInfo;

            PropertyInfo pi2 = typeof(ClassB).GetProperties()
                .Where(x => x.Name == "ValueA").Single();

            Console.WriteLine("{0}, {1}, {2}, {3}, {4}", pi1, pi1.DeclaringType, pi1.MemberType, pi1.MetadataToken, pi1.Module);
            Console.WriteLine("{0}, {1}, {2}, {3}, {4}", pi2, pi2.DeclaringType, pi2.MemberType, pi2.MetadataToken, pi2.Module);

            // these two comparisons FAIL
            Console.WriteLine("pi1 == pi2: {0}", pi1 == pi2);
            Console.WriteLine("pi1.Equals(pi2): {0}", pi1.Equals(pi2));

            // this comparison passes
            Console.WriteLine("pi1.DeclaringType == pi2.DeclaringType: {0}", pi1.DeclaringType == pi2.DeclaringType);
            Console.ReadKey();
        }
    }

    class ClassA
    {
        public int ValueA { get; set; }
    }

    class ClassB : ClassA
    {
    }
}

The output here is:

Int32 ValueA, TestReflectionError.ClassA, Property, 385875969, TestReflectionError.exe
Int32 ValueA, TestReflectionError.ClassA, Property, 385875969, TestReflectionError.exe
pi1 == pi2: False
pi1.Equals(pi2): False
pi1.DeclaringType == pi2.DeclaringType: True


Culprit: PropertyInfo.ReflectedType

I have found a difference between these two objects... it is in the ReflectedType. The documentation says this:

Gets the class object that was used to obtain this member.

like image 798
Miguel Angelo Avatar asked Oct 07 '12 03:10

Miguel Angelo


3 Answers

Why don't you just compare MetadataToken and Module.

According the documentation that combination uniquely identifies.

MemberInfo.MetadataToken
A value which, in combination with Module, uniquely identifies a metadata element.

static void Main(string[] args)
{
    Console.BufferWidth = 200;
    Console.WindowWidth = 140;

    PropertyInfo pi1 = typeof(ClassA).GetProperties()
        .Where(x => x.Name == "ValueA").Single();
    PropertyInfo pi2 = typeof(ClassB).GetProperties()
        .Where(x => x.Name == "ValueA").Single();
    PropertyInfo pi0 = typeof(ClassA).GetProperties()
        .Where(x => x.Name == "ValueB").Single();
    PropertyInfo pi3 = typeof(ClassB).GetProperties()
        .Where(x => x.Name == "ValueB").Single();
    PropertyInfo pi4 = typeof(ClassC).GetProperties()
        .Where(x => x.Name == "ValueA").Single();
    PropertyInfo pi5 = typeof(ClassC).GetProperties()
        .Where(x => x.Name == "ValueB").Single();


    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi1, pi1.ReflectedType, pi1.DeclaringType, pi1.MemberType, pi1.MetadataToken, pi1.Module);
    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi2, pi2.ReflectedType, pi2.DeclaringType, pi2.MemberType, pi2.MetadataToken, pi2.Module);
    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi0, pi0.ReflectedType, pi0.DeclaringType, pi0.MemberType, pi0.MetadataToken, pi1.Module);
    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi3, pi3.ReflectedType, pi3.DeclaringType, pi3.MemberType, pi3.MetadataToken, pi3.Module);
    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi4, pi4.ReflectedType, pi4.DeclaringType, pi4.MemberType, pi4.MetadataToken, pi4.Module);
    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi5, pi5.ReflectedType, pi5.DeclaringType, pi5.MemberType, pi5.MetadataToken, pi5.Module);

    // these two comparisons FAIL
    Console.WriteLine("pi1 == pi2: {0}", pi1 == pi2);
    Console.WriteLine("pi1.Equals(pi2): {0}", pi1.Equals(pi2));

    // this comparison passes
    Console.WriteLine("pi1.DeclaringType == pi2.DeclaringType: {0}", pi1.DeclaringType == pi2.DeclaringType);


    pi1 = typeof(ClassA).GetProperties()
        .Where(x => x.Name == "ValueB").Single();

    pi2 = typeof(ClassB).GetProperties()
        .Where(x => x.Name == "ValueB").Single();

    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi1, pi1.ReflectedType, pi1.DeclaringType, pi1.MemberType, pi1.MetadataToken, pi1.Module);
    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi2, pi2.ReflectedType, pi2.DeclaringType, pi2.MemberType, pi2.MetadataToken, pi2.Module);

    // these two comparisons FAIL
    Console.WriteLine("pi1 == pi2: {0}", pi1 == pi2);
    Console.WriteLine("pi1.Equals(pi2): {0}", pi1.Equals(pi2));


    Console.ReadKey();
}
class ClassA
{
    public int ValueA { get; set; }
    public int ValueB { get; set; }
}
class ClassB : ClassA
{
    public new int ValueB { get; set; } 
}
class ClassC
{
    public int ValueA { get; set; }
    public int ValueB { get; set; }
}
like image 67
paparazzo Avatar answered Nov 19 '22 16:11

paparazzo


Never assume there's a bug in the library unless you actually know what you're doing and you have exhaustively tested the issue.

PropertyInfo objects have no notion of equality. Sure they may represent the same result but they do not overload the == operator so you cannot assume that they should. Since they don't, it's just simply doing a reference comparison and guess what, they are referring to two separate objects and are therefore !=.

On the other hand, Type objects also do no overload the == operator but it seems comparing two instances with the == operator will work. Why? Because type instances are actually implemented as singletons and this is an implementation detail. So given two references to the same type, they will compare as expected because you are actually comparing references to the same instance.

Do not expect that every object you will ever get when calling framework methods will work the same way. There isn't much in the framework that use singletons. Check all relevant documentation and other sources before doing so.


Revisiting this, I've been informed that as of .NET 4, the Equals() method and == operator has been implemented for the type. Unfortunately the documentation doesn't explain their behavior much but using tools like .NET Reflector reveals some interesting info.

According to reflector, the implementations of the methods in the mscorlib assembly are as follows:

[__DynamicallyInvokable]
public override bool Equals(object obj)
{
    return base.Equals(obj);
}

[__DynamicallyInvokable]
public static bool operator ==(PropertyInfo left, PropertyInfo right)
{
    return (object.ReferenceEquals(left, right)
        || ((((left != null) && (right != null)) &&
             (!(left is RuntimePropertyInfo) && !(right is RuntimePropertyInfo)))
        && left.Equals(right)));
}

Going up and down the inheritance chain (RuntimePropertyInfo -> PropertyInfo -> MemberInfo -> Object), Equals() calls the base implementation all the way up to Object so it in effect does a object reference equality comparison.

The == operator specifically checks to make sure that neither PropertyInfo object is a RuntimePropertyInfo object. And as far as I can tell, every PropertyInfo object you would get using reflection (in the use-cases shown here) will return a RuntimePropertyInfo.

Based on this, it looks like the framework designers conscientiously made it so (Runtime) PropertyInfo objects non-comparable, even if they represent the same property. You may only check to see if the properties refer to the same PropertyInfo instance. I can't tell you why they've made this decision (I have my theories), you'd have to hear it from them.

like image 4
Jeff Mercado Avatar answered Nov 19 '22 15:11

Jeff Mercado


I compare DeclaringType and Name. This reports that the "same" property from two different generic types is different (e.g., List<int>.Count and List<string>.Count). Comparing MetadataToken and Module would report that these two properties are the same.

like image 2
Will Montgomery Avatar answered Nov 19 '22 15:11

Will Montgomery