Is this a bug in .Net reflection?
Asked Answered
C

4

5

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.

Chevrette answered 7/10, 2012 at 3:8 Comment(2)
If you just do PropertyInfo pi1 = typeof(ClassA)... You get the same results. What is interesting is if compare ClassA to ClassA or ClassB to ClassB then all true. So I have to agree this behavior does not seem correct.Ciliolate
Should I make a new question about a reliable way of comparing these objects, or should I edit this question's title? Any opinions?Chevrette
F
4

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.

Fennell answered 7/10, 2012 at 4:28 Comment(14)
PropertyInfo is not part of the System.Linq.Expressions. It is a System.Reflection object, just like the Type class.Chevrette
I have assumed that PropertyInfo objects would behave the same as the Type objects do. It does not matter how you get a Type of a type, they are always the same object. I thought that the same would be true for property infos.Chevrette
Ah right, I thought you were comparing Expression objects. Well, the reason why == works with Type objects is because type objects are implemented as singletons. So typeof(string) == typeof(string) will always be true since they are really referring to the same type instance.Fennell
Yes, and as such, the PropertyInfos that exist inside a singleton Type are also singles... at least they should be, I think. In fact, if I try to get the same property two times from a Type object, it will give me the same instance... so, how did this second instance of the PropertyInfo come to life.Chevrette
I mean... if all PropertyInfos come from a Type, then how was this second instance created? Unless there is another hidden way of getting these objects... and that is why I am saying that this may be a bug.Chevrette
But the documentation indicates it does override Equals msdn.microsoft.com/en-us/library/… And if you test ClassA to ClassA then the result is true.Ciliolate
Very strange... if it overrides Equals method, it should at least return true even for two different instances of the same property (I expected that, so just my opinion). But as I have tested, it does not. I was unabled to find a reliable way of comparing two PropertyInfo objects.Chevrette
Thanks @Blam, I didn't think it did. But either way, it's still not going to work since he's using the == operator and that still isn't overloaded.Fennell
It overrides Equals because it has to, the inherited _PropertyInfo COM interface requires it. But it just calls base.Equals()Decreasing
You are right. I have assumed a bug, where there is no bug... what a shame! =\ ... but now, how can I compare these two objects? Even if they are different objects (and there is a reason for it), how can I determine whether they represent the same property?Chevrette
-1: The bold+italics is misleading considering PropertyInfo does override Equals. The statement that "PropertyInfo objects have no notion of equality" is flat wrong. Also, since .NET 4 PropertyInfo also defines the == and != operators.Bluey
Another incorrect statement about reference equality being an implementation detail: I can't find it in ECMA-335 (yet), but since (at least) .NET 1.1, System.Type has had a note similar to the following: "A Type object that represents a type is unique; that is, two Type object references refer to the same object if and only if they represent the same type. This allows for comparison of Type objects using reference equality."Bluey
@Sam Harwell - That's not true. I've just been dealing with this issue today. I had 2 System.RuntimeType objects that referred to exactly the same type - same Assembly, Module, GUID, Name, etc. - but Equals returned false. The singleton thing is probably only true for compile-time type objects.Hydroxide
-1 I am looking at mono implementation and it actually does implement custom == and != so the bolded statement is incorrect. It even overrides Equals() inside PropertyInfo.Chopper
C
4

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; }
}
Ciliolate answered 7/10, 2012 at 19:23 Comment(0)
L
2

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.

Larondalarosa answered 21/12, 2012 at 16:37 Comment(0)
P
0

On the outset, it would seem to make sense that two MemberInfo are equal if they return the same value when accessing that member directly (not via reflection). For FieldInfo this seems more reasonable. However, for PropertyInfo it is not so clear because the property could be extended in a subclass, and different CustomAttributes could be added to the member declaration. This means that strictly considering the accessed value is insufficient to define equality. However, if that is the definition of equality you want then you may want to consider the AreEqual3(...) approach:

private class Person {
    [CustomAttribute1]
    public virtual String Name { get; set; }
}

private class Person2 : Person {
    [CustomAttribute2]
    public override String Name { get; set; }
}

public static void TestMemberInfoEquality() {
    MemberInfo m1 = ExpressionEx.GetMemberInfo<Person>(p => p.Name);
    MemberInfo m2 = ExpressionEx.GetMemberInfo<Person2>(p => p.Name);
    bool b1 = m1.MetadataToken == m2.MetadataToken; // false
    bool b2 = m1 == m2; // false (because ReflectedType is different)
    bool b3 = m1.DeclaringType == m2.DeclaringType; // false
    bool b4 = AreEqual1(m1, m2); // false
    bool b5 = AreEqual2(m1, m2); // false
    bool b6 = AreEqual3(m1, m2); // true
}

public static bool AreEqual1(MemberInfo m1, MemberInfo m2) {
    return m1.MetadataToken == m2.MetadataToken && m1.Module == m2.Module;
}

public static bool AreEqual2(MemberInfo m1, MemberInfo m2) {
    return m1.DeclaringType == m2.DeclaringType && m1.Name == m2.Name;
}

public static bool AreEqual3(MemberInfo m1, MemberInfo m2) {
    return m1.GetRootDeclaration() == m2.GetRootDeclaration();
}

public static MemberInfo GetRootDeclaration(this MemberInfo mi) {
    Type ty = mi.ReflectedType;
    while (ty != null) {
        MemberInfo[] arr = ty.GetMember(mi.Name, mi.MemberType, BindingFlags.Instance | BindingFlags.Public);
        if (arr == null || arr.Length == 0)
            break;
        mi = arr[0];
        ty = ty.BaseType;
    }
    return mi;
}

The method has only been written for Public and Instance members. Some other discussion threads suggest using the AreEqual1(...) or AreEqual2(...) approaches, but they return false for the given example.

Path answered 14/6, 2017 at 17:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.