2

I have two classes that are decorated with the same attribute but different values. When I dump them in LINQPad I can see that they are different but when I do x.Equals(y) then it yields true even though the implementation of Equals actually compares property values.

This code reproduces this issue:

void Main()
{
    var a1 = typeof(T1).GetCustomAttribute<A2>().Dump();
    var a2 = typeof(T3).GetCustomAttribute<A2>().Dump();
    a1.Equals(a2).Dump();
}


[A1(V = "I1")]
interface I1
{
    [A1(V = "I1.P1")]
    string P1 { get; set; }
}

[A2(V = "T1")] // <-- typeof(T1).GetCustomAttribute<A2>()
class T1 : I1
{
    [A1(V = "T1.P1")]
    public virtual string P1 { get; set; }
}

class T2 : T1 { }

[A1(V = "T3"), A2(V = "T3")] // <-- typeof(T3).GetCustomAttribute<A2>()
class T3 : T2
{
    [A1(V = "T3.P1")]
    public override string P1 { get; set; }
}

class A1 : Attribute { public string V { get; set; } }
class A2 : A1 { }

And these are the results:

UserQuery+A2 
TypeId = typeof(A2) 
V      = T1 

UserQuery+A2 

TypeId = typeof(A2) 
V      = T3 

True // <-- a1.Equals(a2).Dump();

What am I missing here and how can I properly compare them?

t3chb0t
  • 16,340
  • 13
  • 78
  • 118
  • 1
    I don't know the details of LinqPad's Dump method, but notice that you **don't** assign the attribute objects to the variables a1/a2, but you rather assign the value/object returend by the Dump method. Why you get what you got there is thus very much dependent on the behavior/implementation of that Dump() method... –  Jun 12 '19 at 17:04
  • 1
    @elgonzo _I don't know the details of LinqPad's Dump method_ - I can see that because your theory is completely wrong. `Dump` is _transparent_ and returns the same `T` it receives. – t3chb0t Jun 12 '19 at 17:12
  • Hmm, okay. I have just fired up a VS test project (.NET framework 4.6.1) and looked into it. It seems the Equal method implemented by the `Attribute` class does not recognize inherited fields. (You can quickly verify by making the attribute property in A1 virtual and then simple override it in A2. This should then make Equal return _false_). Now, i don't know why this is so and whether the same behavior would also be observable in .NET Core, hence no answer. (The documentation unfortunately doesn't mention this particular behavior...) –  Jun 12 '19 at 17:18
  • The `a1 == a2` [returns false](https://dotnetfiddle.net/8U4MRQ) but `a1.Equals(a2)` returns true. This suggests the default Equals for attributes works incorrectly here. – Wiktor Zychla Jun 12 '19 at 17:19
  • @WiktorZychla I guess it uses the default `==` `object`'s operator because `Attribute` doesn't seem to override it. At least I couldn't find it in the source code. – t3chb0t Jun 12 '19 at 17:32
  • @elgonzo crap, this means I need some plan B... ;-( thx – t3chb0t Jun 12 '19 at 17:33
  • @WiktorZychla, since the Attribute class does not provide its own `==` overload, `a1 == a2` will always return false if a1 and a2 are not referring to the same Attribute instance, irregardless of whether two different Attribute instances possess equal field values. (equality by reference: https://stackoverflow.com/questions/7345970/c-default-implementation-for-and-operators-for-objects) –  Jun 12 '19 at 17:38
  • Well, the reason why Equals behaves like it does is rather obvious, come to think of it (speaking about not seeing the forest for all the trees). Since the backing field of the A1 property is private, it will not appear in the list of reflected fields when querying A2 for fields, as A2 will be able to see/access private members from base types. Possible solution/workaround: override the Equals method in your base attribute class (A1), which reflects both over (public) fields and properties and compares their values... –  Jun 12 '19 at 17:48
  • @elgonzo: the lecture on how reference equality works was not required nor expected here. The OP's concern is only related to true returned by `Equals`, not `false` returned by `==`. In fact, my comment was a suggestion to override the invalid behavior of the default `Equals`, without going into these details on how the Equals actually works here (which is clearly stated in the docs). – Wiktor Zychla Jun 12 '19 at 18:13
  • @WiktorZychla, i don't understand your last comment. You were the one bringing up `a1==a2` in the comments, seemingly using it to compare its behavior against that of Attribute.Equals in an attempt to deduce that the default Equals works incorrectly here. Your comment there did not express any suggestion with regard to overriding Attributes.Equal either. Am i just so bad at reading between the lines instead of reading the actual lines of your first comment? o.O? –  Jun 12 '19 at 18:16
  • @elgonzo: could be, could also be the word *incorrecly* in my comment should rather be *unexpectedly*. Regards. – Wiktor Zychla Jun 12 '19 at 18:33

1 Answers1

1

The A1 attribute class declares an auto-property with a private compiler-generated backing field.

Now, when the Attribute.Equals method reflects over A2 to access all its instance fields (Attribute.Equals does not reflect over properties), it will not "see" the private backing field declared in A1, since private members of a base type are not accessible through a derived type. (See also here: Are private members inherited in C#?)

Thus, when trying to compare two instances of type A2 - which itself does not declare any fields - using the Attribute.Equals implementation, the result will be true (because the type of the two attribute instances is the same type A2, and the instances do not feature any fields that would be accessible through the A2 type).

Possible solutions (depending on the actual application scenario at hand) could be (among other things) using public fields instead of public properties in attribute classes, or perhaps overriding the Equals() method in your base attribute class (A1) which reflects and compares the type, all public fields and additionally all public properties of two attribute instances.

  • I think I'll redesign my attributes in such a way that I have a _dummy_ base one and I put all other properties in other attributes and add an interface for working with common properties. Rewriting `Equals` could be even more tricky. – t3chb0t Jun 12 '19 at 18:12
  • 1
    @t3chb0t A better comparison for these cases where you want to break encapsulation a bit when comparing two `MemberInfo` instances, is to take into account the `ReflectedType` in the comparison. –  Jun 13 '19 at 07:46