Tengo un problema con una propiedad de navegación en un proyecto de marco de entidad.

Aquí está la clase 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
}

La parte relevante es esta propiedad de navegación:

public virtual ICollection<MobileDeviceInfo> DeviceInfos { get; private set; }

Esta es la clase 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
}

Como puede ver, implementa IEquatable<MobileDeviceInfo> y anula también Equals y GetHashCode de System.Object.

Tengo la siguiente prueba, esperaba que Contains llamara a mi Equals pero no es así. Parece usar Object.ReferenceEquals en su lugar, así que no encontraré mi dispositivo porque es una referencia diferente:

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

El segundo enfoque con LINQ Enumerable.Any devuelve el true esperado.

Si no uso user.DeviceInfos.Contains(device) pero user.DeviceInfos.ToList().Contains(device), también funciona como se esperaba ya que List<>.Contains usa mi Equals.

El tipo real de ICollection<> parece ser un System.Collections.Generic.HashSet<MobileDeviceInfo> pero si utilizo el siguiente código que usa también un HashSet<>, nuevamente funciona como se esperaba:

bool contains = new HashSet<MobileDeviceInfo>(user.DeviceInfos).Contains(device); // true

Entonces, ¿por qué solo se comparan las referencias y se ignora mi Equals personalizada?

Actualización :

Aún más confuso es el resultado es false incluso si lo lanzo al 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); 

Actualización 2: : la razón de esto realmente parece ser que ambos HashSets usan comparadores diferentes. El entity-framework-HashSet usa un:

System.Data.Entity.Infrastructure.ObjectReferenceEqualityComparer

Y el estándar HashSet<> usa un:

GenericEqualityComparer<T>

Eso explica el problema, aunque no entiendo por qué el marco de la entidad usa una implementación que ignora las implementaciones Equals personalizadas bajo ciertas circunstancias. Esa es una trampa desagradable, ¿no?


Conclusión : nunca use Contains si no sabe qué comparador se usará o use Enumerable.Contains con la sobrecarga que requiere un comparador personalizado:

bool contains = user.DeviceInfos.Contains(device, EqualityComparer<MobileDeviceInfo>.Default);  // true
10
Tim Schmelter 23 ago. 2016 a las 16:30

2 respuestas

La mejor respuesta

Desde la fuente de EF, es posible que te encuentres con y devuelve {{X0} HashSet<T> como el tipo si es compatible con la propiedad.

Luego, armado con HashSet<T>, realiza una llamada a ObjectReferenceEqualityComparer en el constructor.

Entonces: el HashSet<T> EF que crea para usted no está usando su implementación de igualdad, en su lugar usa la igualdad de referencia.

8
Charles Mager 23 ago. 2016 a las 14:02

¿Por qué ICollection <>. Contiene ignora mi Igualdad anulada y la interfaz IEquatable <>?

Porque no hay ningún requisito por parte de los implementadores de la interfaz para hacerlo.

ICollection<T>.Contains método MSDN documentación estados:

Determina si ICollection contiene un valor específico.

Y entonces

Observaciones

Las implementaciones pueden variar en la forma en que determinan la igualdad de objetos; por ejemplo, List usa Comparer .Default, mientras que Dictionary permite al usuario especificar la implementación IComparer que se utilizará para comparar claves.

Nota al margen: parece que se equivocaron IComparer<T> con IEqualityComparer<T>, pero entiendes el punto :)

Conclusión: nunca use Contiene si no sabe qué comparador se usará o use Enumerable. Contiene con la sobrecarga que toma un comparador personalizado

Según la sobrecarga del método Enumerable.Contains<T>(IEnumerable<T>, T) (es decir, sin comparador personalizado) documentación:

Determina si una secuencia contiene un elemento especificado mediante el comparador de igualdad predeterminado .

Que parece que se llamarán sus anulaciones. Pero luego viene lo siguiente:

Comentarios
Si el tipo de fuente implementa ICollection , se invoca el método Contains en esa implementación para obtener el resultado. De lo contrario, este método determina si la fuente contiene el elemento especificado.

Que entra en conflicto con la declaración inicial.

Realmente es un desastre. ¡Todo lo que puedo decir es que estoy totalmente de acuerdo con esa conclusión!

3
Ivan Stoev 23 ago. 2016 a las 16:30