I have an issue with a navigation property in an entity framework project.
Here is the class MobileUser:
[DataContract]
[Table("MobileUser")]
public class MobileUser: IEquatable<MobileUser>
{
// constructors omitted....
/// <summary>
/// The primary-key of MobileUser.
/// This is not the VwdId which is stored in a separate column
/// </summary>
[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<MobileDeviceInfo> 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
}
The relevant part is this navigation property:
public virtual ICollection<MobileDeviceInfo> DeviceInfos { get; private set; }
This is the class MobileDeviceInfo:
[DataContract]
[Table("MobileDeviceInfo")]
public class MobileDeviceInfo : IEquatable<MobileDeviceInfo>
{
[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; }
/// <summary>
/// The foreign-key to the MobileUser.
/// This is not the VwdId which is stored in MobileUser
/// </summary>
[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
}
As you can see, it implements IEquatable<MobileDeviceInfo> and overrides also Equals and GetHashCode from System.Object.
I have following test, i've expected that Contains would call my Equals but it does not. It seems to use Object.ReferenceEquals instead, so won't find my device because it's a different reference:
var userRepo = new MobileUserRepository((ILog)null);
var deviceRepo = new MobileDeviceRepository((ILog)null);
IReadOnlyList<MobileUser> allUser = userRepo.GetAllMobileUsersWithDevices();
MobileUser user = allUser.First();
IReadOnlyList<MobileDeviceInfo> allDevices = deviceRepo.GetMobileDeviceInfos(user.VwdId, true);
MobileDeviceInfo device = allDevices.First();
bool contains = user.DeviceInfos.Contains(device);
bool anyEqual = user.DeviceInfos.Any(x => x.DeviceToken == device.DeviceToken);
Assert.IsTrue(contains); // no, it's false
The second approach with LINQ's Enumerable.Any returns the expected true.
If i don't use user.DeviceInfos.Contains(device) but user.DeviceInfos.ToList().Contains(device) it also works as expected since List<>.Contains uses my Equals.
The actual type of the ICollection<> seems to be a System.Collections.Generic.HashSet<MobileDeviceInfo> but if i use following code that uses also a HashSet<> it again works as expected:
bool contains = new HashSet<MobileDeviceInfo>(user.DeviceInfos).Contains(device); // true
So why are only references compared and my custom Equals is ignored?
Update:
even more confusing is the result is false even if i cast it to the
HashSet<MobileDeviceInfo>:
// still false
bool contains2 = ((HashSet<MobileDeviceInfo>)user.DeviceInfos).Contains(device);
// but this is true as already mentioned
bool contains3 = new HashSet<MobileDeviceInfo>(user.DeviceInfos).Contains(device);
Update 2:: the reason for this really seems to be that both HashSets use different comparers. The entity-framework-HashSet uses a:
System.Data.Entity.Infrastructure.ObjectReferenceEqualityComparer
and the standard HashSet<> uses a:
GenericEqualityComparer<T>
That explains the issue, although i don't understand why entity framework uses an implementation that ignores custom Equals implementations under certain circumstances. That's a nasty trap, isn't it?
Conclusion: never use Contains if you don't know what comparer will be used or use Enumerable.Contains with the overload that takes a custom comparer:
bool contains = user.DeviceInfos.Contains(device, EqualityComparer<MobileDeviceInfo>.Default); // true
Why ICollection<>.Contains ignores my overridden Equals and the IEquatable<> interface?
Because there is no requirement from the implementors of the interface to do so.
ICollection<T>.Contains method MSDN documentation states:
Determines whether the ICollection<T> contains a specific value.
And then
Remarks
Implementations can vary in how they determine equality of objects; for example, List<T> uses Comparer<T>.Default, whereas Dictionary<TKey, TValue> allows the user to specify the IComparer<T> implementation to use for comparing keys.
Side note: Looks like they messed up IComparer<T> with IEqualityComparer<T>, but you get the point :)
Conclusion: never use Contains if you don't know what comparer will be used or use Enumerable.Contains with the overload that takes a custom comparer
According to the Enumerable.Contains<T>(IEnumerable<T>, T) method overload (i.e. without custom comparer) documentation:
Determines whether a sequence contains a specified element by using the default equality comparer.
which sounds like your overrides will be called. But then comes the following:
Remarks
If the type of source implements ICollection<T>, the Contains method in that implementation is invoked to obtain the result. Otherwise, this method determines whether source contains the specified element.
which conflicts with the initial statement.
It's really a mess. All I can say is that I fully agree with that conclusion!
From the EF source, you might stumble on CreateCollectionCreateDelegate, which seems to be called as part of hooking up navigation properties.
This calls EntityUtil.DetermineCollectionType and returns a HashSet<T> as the type if that is compatible with the property.
Then, armed with HashSet<T>, it makes a call to DelegateFactory.GetNewExpressionForCollectionType which, per the code and the description, handles HashSet<T> as a special case and passes it an ObjectReferenceEqualityComparer in the constructor.
So: the HashSet<T> EF creates for you isn't using your equality implementation, it uses reference equality instead.
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