我在实体框架项目中遇到导航属性问题。
这是课程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
}
相关部分是此导航属性:
public virtual ICollection<MobileDeviceInfo> DeviceInfos { get; private set; }
这是班级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
}
如您所见,它实现IEquatable<MobileDeviceInfo>
并覆盖Equals
中的GetHashCode
和System.Object
。
我有以下测试,我预计Contains
会调用我的Equals
,但事实并非如此。它似乎使用了Object.ReferenceEquals
,因此无法找到我的设备,因为它是一个不同的参考:
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
使用LINQ
&#39; Enumerable.Any
的第二种方法会返回预期的true
。
如果我不使用user.DeviceInfos.Contains(device)
,user.DeviceInfos.ToList().Contains(device)
它也可以正常工作,因为List<>.Contains
使用了Equals
。
ICollection<>
的实际类型似乎是System.Collections.Generic.HashSet<MobileDeviceInfo>
,但如果我使用下面同时使用HashSet<>
的代码,它又会按预期运行:
bool contains = new HashSet<MobileDeviceInfo>(user.DeviceInfos).Contains(device); // true
那么为什么只比较引用,我的自定义Equals
会被忽略?
更新:
更令人困惑的是结果是false
即使我把它投射到了。{1}}
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);
更新2::原因似乎是两个HashSets都使用不同的比较器。 entity-framework-HashSet使用:
System.Data.Entity.Infrastructure.ObjectReferenceEqualityComparer
,标准HashSet<>
使用:
GenericEqualityComparer<T>
这解释了这个问题,虽然我不明白为什么实体框架在某些情况下使用忽略自定义Equals
实现的实现。这不是一个令人讨厌的陷阱,不是吗?
结论:如果您不知道将使用什么比较器,请使用Contains
或使用带有自定义比较器的重载Enumerable.Contains
:
bool contains = user.DeviceInfos.Contains(device, EqualityComparer<MobileDeviceInfo>.Default); // true
答案 0 :(得分:8)
从EF来源,您可能偶然发现CreateCollectionCreateDelegate
,这似乎是挂钩导航属性的一部分。
如果与属性兼容,则会调用EntityUtil.DetermineCollectionType
并返回HashSet<T>
作为类型。
然后,在HashSet<T>
的帮助下,它会调用DelegateFactory.GetNewExpressionForCollectionType
,根据代码和说明,将HashSet<T>
作为一个特例进行处理并将ObjectReferenceEqualityComparer
传递给在构造函数中。
所以:HashSet<T>
EF为你创建的不是使用你的相等实现,而是使用引用相等。
答案 1 :(得分:3)
为什么ICollection&lt;&gt; .Contains会忽略我重写的Equals和IEquatable&lt;&gt;接口
因为接口的实现者没有要求这样做。
ICollection<T>.Contains
方法MSDN documentation声明:
确定ICollection&lt; T&gt;是否为&lt; T&gt;。包含特定值。
然后
备注
实施可以改变,确定对象是否相等;例如,List&lt; T&gt;使用Comparer&lt; T&gt; .Default,而Dictionary&lt; TKey,TValue&gt;允许用户指定IComparer&lt; T&gt;。用于比较密钥的实现。
旁注:看起来他们与IComparer<T>
混淆了IEqualityComparer<T>
,但您明白了这一点:)
结论:如果您不知道将使用什么比较器,请永远使用Contains或使用带有自定义比较器的重载的Enumerable.Contains
根据Enumerable.Contains<T>(IEnumerable<T>, T)
方法重载(即没有自定义比较器)documentation:
使用默认相等比较器确定序列是否包含指定元素。
听起来像你的覆盖将被调用。但接下来是:
<强>说明强>
如果源的类型实现ICollection&lt; T&gt;,则调用该实现中的Contains方法以获得结果。否则,此方法确定source是否包含指定的元素。
与初始陈述冲突。
这真的很乱。我只能说完全同意这个结论!