Equality, at face value, seems like a very simple concept. The following, it can be agreed, are equal:

var first = 1;
var second = 1;
Console.WriteLine(first == second);

As are these:

var first = "apple";
var second = "apple";
Console.WriteLine(first == second);

Let us introduce a new type, the Contact.

public class Contact
{
  public required string FirstName { get; init; }
  public required string MiddleName { get; init; }
  public required string Surname { get; init; }
}

What about now? What should this code return:

var conrad = new Contact
{
  FirstName = "Conrad",
  Surname = "Akunga",
  MiddleName = "Marc"
};

var marc = new Contact
{
  FirstName = "Conrad",
  Surname = "Akunga",
  MiddleName = "Marc"
};

Unsurprisingly, it returns false. Remember, for a class equality means reference equality - both references point at the same object.

We could solve this problem by making the type a Record.

public record Contact
{
  public required string FirstName { get; init; }
  public required string MiddleName { get; init; }
  public required string Surname { get; init; }
}

Here, the compiler will compare the properties of the type and if they are all equal, then two classes with the same properties are equal.

The check:

Console.WriteLine(first == second);

Returns true

But this, technically, is cheating.

What if we made the type more complex?

public class Contact
{
  public required string FirstName { get; init; }
  public required string MiddleName { get; init; }
  public required string Surname { get; init; }
  public required string Notes { get; init; }
}

We then define that two contacts are equal if they share the FirstName, MiddleName and Surname, but we don’t care what the Notes are.

Our shortcut of making it a record won’t work here.

There is a solution to this problem, but it requires a bit of effort.

  1. Override the equality operators (== and !=)
  2. Implement Equals
  3. Implement GetHashCode methods.
public class Contact
{
  public required string FirstName { get; init; }
  public required string MiddleName { get; init; }
  public required string Surname { get; init; }
  public required string Notes { get; init; }

  public static bool operator ==(Contact? left, Contact? right)
  {
  	if (ReferenceEquals(left, right))
  		return true;

    if (left is null || right is null)
    	return false;

    return left.FirstName == right.FirstName &&
      left.MiddleName == right.MiddleName &&
      left.Surname == right.Surname;
  }


  public static bool operator !=(Contact? left, Contact? right)
  {
  	return !(left == right);
  }

  public override bool Equals(object? obj)
  {
    if (obj is not Contact other)
    	return false;

    return this == other;
  }
  
  public bool Equals(Contact contact)
  {
    if (contact is not Contact other)
    	return false;

  	return this == other;
  }

  public override int GetHashCode()
  {
  	return HashCode.Combine(FirstName, MiddleName, Surname);
  }
}

Finally, we re-run our check:

Console.WriteLine(first == second);

With this update the code now prints what we expect - true.

The final improvement is to make sure our type behaves properly and will be performant in the larger .NET ecosystem. Luckily, this is very trivial.

All we need to do is indicate that our type implements the generic IEquatable«T» interface. We don’t need to implement any new logic - we have already met the requirements - we have implemented the Equals method.

Our final class looks like this:

public class Contact : IEquatable<Contact>
{
  public required string FirstName { get; init; }
  public required string MiddleName { get; init; }
  public required string Surname { get; init; }
  public required string Notes { get; init; }

  public static bool operator ==(Contact? left, Contact? right)
  {
      // If either is null, return true
      if (ReferenceEquals(left, right))
      	return true;

      // If either (at least one) is null, return false
      if (left is null || right is null)
      	return false;

      // Implement our equality check
      return left.FirstName == right.FirstName &&
        left.MiddleName == right.MiddleName &&
        left.Surname == right.Surname;
  }

  public override bool Equals(object? obj)
  {
    if (obj is not Contact other)
    	return false;

    return this == other;
  }

  public bool Equals(Contact contact)
  {
    if (contact is not Contact other)
    	return false;

  	return this == other;
  }
  public static bool operator !=(Contact? left, Contact? right)
  {
  	return !(left == right);
  }

  public override int GetHashCode()
  {
  	return HashCode.Combine(FirstName, MiddleName, Surname);
  }
}

Happy hacking!