3

I want to compare records to see if there are differences between them.

Person table:

ID    Name          Address
--------------------------------
1     John Smith    123 A Street
2     John Smith    123 A Street
3     John Smith    234 B Street

Records 1 and 2 are "equal". Records 2 and 3 are "not equal".

I have implemented IEquatable on model Person as follows.

public static bool operator ==(Person p1, Person p2)
{
    if (System.Object.ReferenceEquals(p1, p2)) return true;

    return p1.Equals(p2);
}

public static bool operator !=(Person p1, Person p2)
{
    return !(p1== p2);
}

public bool Equals(Person other)
{
    if (System.Object.ReferenceEquals(this, other)) return true;

    if (Name != other.Name) return false;
    if (Address != other.Address) return false;

    return true;
}

public override bool Equals(object obj)
{
    Person person = obj as Person;
    if (person == null) return false;

    return Equals(person);
}

public override int GetHashCode()
{
    unchecked
    {
        int hash = (int)2166136261;
        hash = hash * 25165843 ^ (Name != null ? Name .GetHashCode() : 0);
        hash = hash * 25165843 ^ (Address != null ? Address.GetHashCode() : 0);

        return hash;
    }
}

The issue is that when the Persons ICollection from a navigational property is materialized. It is missing records "equal" to each other (ie a single John Smith 123 A Street record is returned). I am guessing this is because by default it considers distinct entities ones that have unique primary keys. By overriding equals it thinks both records are the same entity.

Screenshot showing Addresses instead of Persons: (Top is with IEquatable, bottom is without) enter image description here

//Addresses Definition (generated code)
public virtual ICollection<Address> Addresses { get; set; }

How can I reconcile EF needing see equality at the object level versus me wanting to see a logical equality?

jamesSampica
  • 12,230
  • 3
  • 63
  • 85
  • Can you show the declaration of the `Addresses` property on `EmployeeElection`? – Charles Mager Jan 14 '15 at 21:43
  • @CharlesMager Sure thing, I've added it under the screenshot. It is part of the generated EF code. – jamesSampica Jan 14 '15 at 21:48
  • Do you set this as `new Hashset
    ()` in the constructor? This is often the case in EF samples. Addtionally, the `virtual` keyword allows lazy loading, the created EF proxy may use an `ISet
    ` (I don't know the specifics!). Set semantics mean duplicates cannot be added, and they would use the default equality comparer by default (so your `Equals` override). So although the query may return duplicates, the model/EF won't show them.
    – Charles Mager Jan 14 '15 at 21:53
  • @CharlesMager No I don't set anything in the constructor. That's interesting about `ISet` which could explain my problem. – jamesSampica Jan 14 '15 at 21:57
  • @CharlesMager [I found something on that](https://social.msdn.microsoft.com/Forums/en-US/37863ba7-c9fd-46f4-a64f-1a22c036d1e1/isethashset-support?forum=adonetefx) It says that if the virtual property is backed by the `ICollection` type then they will put `List` into it. Granted this is from EF4 and I'm not sure if that changed from EF6. – jamesSampica Jan 14 '15 at 22:02

3 Answers3

6

The key seems to be in the EF source code

In the remarks for EntityUtil.DetermineCollectionType(Type requestedType), there are these 'rules`:

    // The rules are:
    // If the collection is defined as a concrete type with a publicly accessible parameterless constructor, then create an instance of that type
    // Else, if HashSet{T} can be assigned to the type, then use HashSet{T}
    // Else, if List{T} can be assigned to the type, then use List{T}
    // Else, throw a nice exception.

So, from this it would seem that EF will new up a HashSet<Address> for your navigation property. This will use the default equality comparer and prevent any duplicates being added. As your Equals implementation identifies two of your results as equal, only one will be included.

Entities are usually uniquely identified - the overriding of the Equals ignoring the unique identifier is possibly not correct. The best solution would be to remove the override and to implement a separate IEqualityComparer. Most methods that use equality semantics will take this as an argument.

Charles Mager
  • 25,735
  • 2
  • 35
  • 45
2

IEquatable and IEqualityComparer are concepts that are pretty much exclusive to LINQ to objects. EF has no practical way of translating any definition of "equality" defined in such a way into SQL, and so cannot perform such operations.

To get distinct items based on certain columns, in a way that can be translated into SQL, just group the items based on those values and then grab one item from each group:

var query = context.Table.GroupBy(row => new
    {
        row.Name,
        row.Address,
    })
    .Select(group => group.FirstOrDefault());
Servy
  • 202,030
  • 26
  • 332
  • 449
  • I don't have a problem getting distinct items, I am interested in answering the question of "I have two Person objects, are they logically the same?". – jamesSampica Jan 14 '15 at 21:00
  • @Shoe Regardless, if your query isn't returning what you want it to, one needs to look at that query and adjust what operators it uses or how it uses them. The first paragraph of the answer still stands; `IEquatable` cannot be interpreted at all by EF. – Servy Jan 14 '15 at 21:01
  • I understand that but I don't have any queries doing anything. If I simply use the code `context.Persons.ToList()` the materialized set will not contain more than one record that are "equal". I need to answer the question in my comment above, but implementing IEquatable seems to be breaking the loading of entities entirely. – jamesSampica Jan 14 '15 at 21:06
  • @Shoe If you just pull the entire table into a list it will do nothing to remove items that are equal according to the `IEquatable` definition of equality. That just doesn't happen. Either you don't have the data you think you have, you're filtering it somewhere without realizing it, you're not properly checking that there are no "equal" items, or something along those lines. – Servy Jan 14 '15 at 21:08
  • I've added a screenshot to the OP. It is a navigational property, no filtering or anything. – jamesSampica Jan 14 '15 at 21:23
  • @Shoe So if adding custom equality to the database objects breaks your code, why are you adding custom equality to your database objects? – Servy Jan 14 '15 at 21:24
  • My code isn't breaking. EF seems to be breaking because it isn't loading the instances correctly. I don't need to use `IEquatable`, but that's really what it's meant for, no? – jamesSampica Jan 14 '15 at 21:27
  • @Shoe If you have no reason whatsoever to override the equality semantics of your objects, and doing so is breaking your application, then just *don't do it*. If you're sure that that's the cause, then it would seem EF is getting distinct items for some reason in this context, or otherwise assumes that the object's equality semantics aren't overridden. – Servy Jan 14 '15 at 21:30
  • I do have a reason for overriding equality semantics. I want to test if two objects are logically equal. In the case of `Person` that means they have the same Name and Address. To Entity Framework it seems to mean a unique ID. – jamesSampica Jan 14 '15 at 21:34
  • @Shoe Then it would seem you'll need to test for equality without changing the object's definition of equality, and instead using an external comparer. – Servy Jan 14 '15 at 21:35
  • It would seem so. I'm looking at `IEqualityComparer` now. – jamesSampica Jan 14 '15 at 21:49
1

Don't use IEquatable etc, create your own AreEquivalent, or IsEquivalentTo method.

Duncan Smart
  • 31,172
  • 10
  • 68
  • 70