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.
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). ForFieldInfo
this seems more reasonable. However, forPropertyInfo
it is not so clear because the property could be extended in a subclass, and differentCustomAttributes
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 theAreEqual3(...)
approach:The method has only been written for
Public
andInstance
members. Some other discussion threads suggest using theAreEqual1(...)
orAreEqual2(...)
approaches, but they returnfalse
for the given example.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.
I compare
DeclaringType
andName
. This reports that the "same" property from two different generic types is different (e.g.,List<int>.Count
andList<string>.Count
). ComparingMetadataToken
andModule
would report that these two properties are the same.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:
Going up and down the inheritance chain (
RuntimePropertyInfo
->PropertyInfo
->MemberInfo
->Object
),Equals()
calls the base implementation all the way up toObject
so it in effect does a object reference equality comparison.The
==
operator specifically checks to make sure that neitherPropertyInfo
object is aRuntimePropertyInfo
object. And as far as I can tell, everyPropertyInfo
object you would get using reflection (in the use-cases shown here) will return aRuntimePropertyInfo
.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 samePropertyInfo
instance. I can't tell you why they've made this decision (I have my theories), you'd have to hear it from them.