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
?
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
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.
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; }
}
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With