Search code examples
c#oop.net-core

Why IEquatable implementation does not work when applied to an abstract base class?


I have a ValueObject base class like this:

public abstract class ValueObject<T>: IEquatable<T>
{
    public abstract bool checkPropertyEquality(T t);

    public bool Equals(T? other)
    {
        if (ReferenceEquals(null, other)) 
            return false;

        if (ReferenceEquals(this, other)) 
            return true;

        return checkPropertyEquality(other);
    }

    public override bool Equals(object? obj)
    {
        if (ReferenceEquals(null, obj)) 
            return false;

        if (ReferenceEquals(this, obj)) 
            return true;

        if (obj.GetType() != this.GetType()) 
            return false;

        return Equals((T)obj);
    }

    public abstract int GetHashCode();
}

And then I Implemented a class based on this like this:

public class Person : ValueObject<Person>
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Family { get; set; }

    public override bool checkPropertyEquality(Person t)
    {
        return Name == t.Name && Family == t.Family;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Name, Family);
    }
}

So I test this code by running this test scenario and it fails:

[Fact]
public void test1()
{
    var list = new List<Person>()
    {
        new Person { Name = "test1", Family = "testii", Id = Guid.NewGuid() },
        new Person { Name = "test2", Family = "testipoor", Id = Guid.NewGuid() },
    };

    var list1 = new List<Person>()
    {
        new Person { Name = "test1", Family = "testii", Id = Guid.NewGuid() },
        new Person { Name = "test2", Family = "testipoor", Id = Guid.NewGuid() },
    };

    var e = list.Except(list1).ToList();
    var e1 = list1.Except(list).ToList();

    Assert.Empty(e1);
    Assert.Empty(e);
}

But when I move the IEquatable<> implementation into the Person class like this, the test shown above will run successfully:

public abstract class ValueObject<T> //: IEquatable<T>
{
    public abstract bool checkPropertyEquality(T t);
}

public class Person : ValueObject<Person>, IEquatable<Person>
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Family { get; set; }

    public override bool checkPropertyEquality(Person t)
    {
        return Name == t.Name && Family == t.Family;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Name, Family);
    }

    public bool Equals(Person? other)
    {
        if (ReferenceEquals(null, other)) 
            return false;

        if (ReferenceEquals(this, other)) 
            return true;

        return Name == other.Name && Family == other.Family;
    }

    public override bool Equals(object? obj)
    {
        if (ReferenceEquals(null, obj)) 
            return false;

        if (ReferenceEquals(this, obj))  
            return true; 

        if (obj.GetType() != this.GetType()) 
            return false;

        return Equals((Person)obj);
    }
}

Solution

  • Let's have a look at your compiler warnings:

    warning CS0114: 'ValueObject<T>.GetHashCode()' hides inherited member 'object.GetHashCode()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.

    warning CS0659: 'ValueObject<T>' overrides Object.Equals(object o) but does not override Object.GetHashCode()

    GetHashCode is a virtual method on object. Defining public abstract int GetHashCode(); does not turn this into an abstract method: it declares a new method on ValueObject<T> which shadows the method on object.

    When the compiler calls T.GetHashCode, it calls object's implementation, not your shadowed version.

    What you want is:

    public override abstract int GetHashCode();
    

    This does override object's version and turns it into an abstract method. This version works correctly.

    Also: have you come across records?