Why ICollection<>.Contains ignores my overridden Equals and the IEquatable<> interface?
<p>I have an issue with a navigation property in an entity framework project. </p>

<p>Here is the class <code>MobileUser</code>:</p>

<pre><code>[DataContract]
[Table("MobileUser")]
public class MobileUser: IEquatable&lt;MobileUser&gt;
{
    // constructors omitted....

    /// &lt;summary&gt;
    /// The primary-key of MobileUser.
    /// This is not the VwdId which is stored in a separate column
    /// &lt;/summary&gt;
    [DataMember, Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int UserId { get; set; }

    [DataMember, Required, Index(IsUnique = true), MinLength(VwdIdMinLength), MaxLength(VwdIdMaxLength)]
    public string VwdId { get; set; }

    // other properties omitted ...

    [DataMember]
    public virtual ICollection&lt;MobileDeviceInfo&gt; DeviceInfos { get; private set; }

    public bool Equals(MobileUser other)
    {
        return this.UserId == other?.UserId || this.VwdId == other?.VwdId;
    }

    public override bool Equals(object obj)
    {
        if(object.ReferenceEquals(this, obj))return true;
        MobileUser other = obj as MobileUser;
        if (other == null) return false;
        return this.Equals(other);
    }

    public override int GetHashCode()
    {
        // ReSharper disable once NonReadonlyMemberInGetHashCode
        return VwdId.GetHashCode();
    }

    public override string ToString()
    {
        return "foo"; // omitted actual implementation
    }

    #region constants
    // irrelevant
    #endregion
}
</code></pre>

<p>The relevant part is this navigation property:</p>

<pre><code>public virtual ICollection&lt;MobileDeviceInfo&gt; DeviceInfos { get; private set; }
</code></pre>

<p>This is the class <code>MobileDeviceInfo</code>:</p>

<pre><code>[DataContract]
[Table("MobileDeviceInfo")]
public class MobileDeviceInfo : IEquatable&lt;MobileDeviceInfo&gt;
{
    [DataContract]
    public enum MobilePlatform
    {
        [EnumMember]
        // ReSharper disable once InconsistentNaming because correct spelling is iOS
        iOS = 1,
        [EnumMember] Android = 2,
        [EnumMember] WindowsPhone = 3,
        [EnumMember] Blackberry = 4
    }

    // constructors omitted ...

    [DataMember, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int DeviceInfoId { get; private set; }

    [DataMember, Required, Index(IsUnique = true), MinLength(DeviceTokenMinLength), MaxLength(DeviceTokenMaxLength)]
    public string DeviceToken { get; set; }

    [DataMember, Required, MinLength(DeviceNameMinLength), MaxLength(DeviceNameMaxLength)]
    public string DeviceName { get; set; }

    [DataMember, Required]
    public MobilePlatform Platform { get; set; }

    // other properties ...

    [DataMember]
    public virtual MobileUser MobileUser { get; private set; }

    /// &lt;summary&gt;
    ///     The foreign-key to the MobileUser.
    ///     This is not the VwdId which is stored in MobileUser
    /// &lt;/summary&gt;
    [DataMember, ForeignKey("MobileUser")]
    public int UserId { get; set; }

    public bool Equals(MobileDeviceInfo other)
    {
        if (other == null) return false;
        return DeviceToken == other.DeviceToken;
    }

    public override string ToString()
    {
        return "Bah"; // implementation omitted

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(this, obj)) return true;
        MobileDeviceInfo other = obj as MobileDeviceInfo;
        if (other == null) return false;
        return Equals(other);
    }

    public override int GetHashCode()
    {
        // ReSharper disable once NonReadonlyMemberInGetHashCode
        return DeviceToken.GetHashCode();
    }

    #region constants
    // irrelevant
    #endregion
}
</code></pre>

<p>As you can see, it implements <code>IEquatable&lt;MobileDeviceInfo&gt;</code> and overrides also <code>Equals</code> and <code>GetHashCode</code> from <code>System.Object</code>.</p>

<p>I have following test, i've expected that  <code>Contains</code> would call my <code>Equals</code> but it does not. It seems to use <code>Object.ReferenceEquals</code> instead, so won't find my device because it's a different reference:</p>

<pre><code>var userRepo = new MobileUserRepository((ILog)null);
var deviceRepo = new MobileDeviceRepository((ILog)null);

IReadOnlyList&lt;MobileUser&gt; allUser = userRepo.GetAllMobileUsersWithDevices();
MobileUser user = allUser.First();

IReadOnlyList&lt;MobileDeviceInfo&gt; allDevices = deviceRepo.GetMobileDeviceInfos(user.VwdId, true);
MobileDeviceInfo device = allDevices.First();
bool contains = user.DeviceInfos.Contains(device);
bool anyEqual = user.DeviceInfos.Any(x =&gt; x.DeviceToken == device.DeviceToken);
Assert.IsTrue(contains); // no, it's false
</code></pre>

<p>The second approach with <code>LINQ</code>'s <code>Enumerable.Any</code> returns the expected <code>true</code>.</p>

<p>If i don't use <code>user.DeviceInfos.Contains(device)</code> but <code>user.DeviceInfos.ToList().Contains(device)</code> it also works as expected since <code>List&lt;&gt;.Contains</code> uses my <code>Equals</code>.</p>

<p>The actual type of the <code>ICollection&lt;&gt;</code> seems to be a <code>System.Collections.Generic.HashSet&lt;MobileDeviceInfo&gt;</code> but if i use following code that uses also a <code>HashSet&lt;&gt;</code> it again works as expected:</p>

<pre><code>bool contains = new HashSet&lt;MobileDeviceInfo&gt;(user.DeviceInfos).Contains(device); // true
</code></pre>

<p>So why are only references compared and my custom <code>Equals</code> is ignored?</p>

<p><strong>Update</strong>:</p>

<p>even more confusing is the result is <code>false</code> even if i cast it to the 
<code>HashSet&lt;MobileDeviceInfo&gt;</code>:</p>

<pre><code> // still false
bool contains2 = ((HashSet&lt;MobileDeviceInfo&gt;)user.DeviceInfos).Contains(device);
// but this is true as already mentioned
bool contains3 = new HashSet&lt;MobileDeviceInfo&gt;(user.DeviceInfos).Contains(device); 
</code></pre>

<p><strong>Update 2:</strong>: the reason for this really seems to be that both HashSets use different comparers. The entity-framework-HashSet uses a:</p>

<pre><code>System.Data.Entity.Infrastructure.ObjectReferenceEqualityComparer
</code></pre>

<p>and the standard <code>HashSet&lt;&gt;</code> uses a:</p>

<pre><code>GenericEqualityComparer&lt;T&gt;
</code></pre>

<p>That explains the issue, although i don't understand why entity framework uses an implementation that  ignores custom <code>Equals</code> implementations under certain circumstances. That's a nasty trap, isn't it?</p>

<hr>

<p><strong>Conclusion</strong>: never use <code>Contains</code> if you don't know what comparer will be used or use  <a href="https://msdn.microsoft.com/en-us/library/bb339118(v=vs.110).aspx" rel="nofollow"><code>Enumerable.Contains</code></a> with the overload that takes a custom comparer:</p>

<pre><code>bool contains = user.DeviceInfos.Contains(device, EqualityComparer&lt;MobileDeviceInfo&gt;.Default);  // true
</code></pre>