Question

Why Override GetHashCode When Overriding Equals in C#

csharpoverridinghashcode

Question

In C#, consider the following class:

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        if (fooItem == null)
        {
            return false;
        }

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Which implementation is correct?

        return base.GetHashCode();

        // or:
        // return this.FooId.GetHashCode();
    }
}

Equals has been overridden because Foo represents a row in a Foos table, and two Foo objects should be considered equal when they have the same FooId.

Which GetHashCode implementation is preferred in this case, and why is it important to override GetHashCode when Equals is overridden?

Short Answer

By the end of this page, you will understand the contract between Equals and GetHashCode in C#, why hash-based collections depend on both methods, and how to correctly implement value-based equality for a class like Foo. You will also learn common mistakes, practical usage patterns, and how to avoid subtle bugs in Dictionary, HashSet, and similar types.

Concept

In C#, Equals answers the question: Are these two objects logically the same?

GetHashCode answers a different question: Which hash bucket should this object go into?

These two methods are tightly connected.

The rule you must follow

If two objects are equal according to Equals, they must return the same hash code from GetHashCode.

In other words:

  • a.Equals(b) == true implies a.GetHashCode() == b.GetHashCode()
  • The reverse is not required:
    • Two objects can have the same hash code and still not be equal

Why this matters

Many .NET collections use hash codes internally, including:

  • Dictionary<TKey, TValue>
  • HashSet<T>
  • ConcurrentDictionary<TKey, TValue>
  • Lookup<TKey, TElement>

These collections first use GetHashCode to find a bucket, then use Equals to confirm the match.

If Equals says two objects are equal, but GetHashCode gives different values, the collection may place them in different buckets. Then it may never even compare them with Equals, which leads to incorrect behavior.

In your Foo example

Your equality logic is based on FooId:

return fooItem.FooId == this.FooId;

That means GetHashCode should also be based on FooId:

return FooId.GetHashCode();

Using base.GetHashCode() is usually wrong here because the base implementation for reference types is typically based on object identity, not your custom equality definition. Two separate Foo instances with the same FooId would likely have different base hash codes even though your Equals says they are equal.

Important idea: equality should use stable data

Properties used in Equals and GetHashCode should usually not change while the object is being used as a key in a hash-based collection.

For example, if FooId changes after the object is added to a HashSet<Foo>, the object may become hard to find or remove because its hash bucket no longer matches its current state.

Mental Model

Think of a hash-based collection like a library with numbered shelves.

  • GetHashCode chooses the shelf
  • Equals checks whether the book on that shelf is the exact one you want

If two books represent the same thing, they must go to the same shelf. Otherwise, the librarian looks on the wrong shelf and says, "It is not here," even though the matching book exists elsewhere.

So:

  • GetHashCode is the location hint
  • Equals is the final identity check

If those two systems disagree, the collection becomes unreliable.

Take Quiz

Syntax and Examples

Basic pattern

When you override Equals, override GetHashCode using the same fields.

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo other = obj as Foo;
        if (other == null)
        {
            return false;
        }

        return FooId == other.FooId;
    }

    public override int GetHashCode()
    {
        return FooId.GetHashCode();
    }
}

Example: equal objects must have equal hash codes

Foo a = new Foo { FooId = 10, FooName = "Alpha" };
Foo b = new Foo { FooId = 10, FooName = "Beta" };

Console.WriteLine(a.Equals(b));         // True
Console.WriteLine(a.GetHashCode());     // Same as b
Console.WriteLine(b.GetHashCode());

Even though FooName is different, the objects are equal because equality is defined by FooId only.

Better modern version

A more complete implementation often includes type checks and IEquatable<T>:

public class Foo : IEquatable<Foo>
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public bool Equals(Foo other)
    {
        if (other is null)
            return false;

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

        return FooId == other.FooId;
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Foo);
    }

    public override int GetHashCode()
    {
        return FooId.GetHashCode();
    }
}

This is commonly used in real C# code because it avoids boxing in some cases and expresses equality more clearly.

Step by Step Execution

Consider this code:

public class Foo
{
    public int FooId { get; set; }

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

        return FooId == other.FooId;
    }

    public override int GetHashCode()
    {
        return FooId.GetHashCode();
    }
}

var x = new Foo { FooId = 5 };
var y = new Foo { FooId = 5 };

var set = new HashSet<Foo>();
set.Add(x);

Console.WriteLine(set.Contains(y));

What happens step by step

  1. x is created with FooId = 5.
  2. y is created with FooId = 5.
  3. set.Add(x) is called.
  4. HashSet calls x.GetHashCode().
  5. The hash code is based on FooId, so the set places x into the bucket for 5.
  6. set.Contains(y) is called.
  7. HashSet calls y.GetHashCode().
  8. Since y.FooId is also 5, it looks in the same bucket.
  9. It then calls Equals to verify whether y matches an existing item.
  10. y.Equals(x) returns true because both have the same FooId.
  11. Contains returns true.

What if you used base.GetHashCode() instead?

Then x and y would probably have different hash codes because they are different object instances.

That means:

  1. x is stored in one bucket.
  2. y is searched for in a different bucket.
  3. HashSet may never compare y with x using Equals.
  4. Contains(y) could return false even though your equality logic says they are equal.

That is the bug this rule prevents.

Real World Use Cases

1. Entity objects identified by an ID

If two objects represent the same database row, equality is often based on the primary key.

new Foo { FooId = 7, FooName = "A" }
new Foo { FooId = 7, FooName = "B" }

These may be considered equal because they refer to the same entity.

2. Preventing duplicates in a HashSet

var foos = new HashSet<Foo>();
foos.Add(new Foo { FooId = 1, FooName = "One" });
foos.Add(new Foo { FooId = 1, FooName = "Duplicate" });

With correct equality and hash code logic, the second item is treated as a duplicate.

3. Using custom objects as dictionary keys

var cache = new Dictionary<Foo, string>();
cache[new Foo { FooId = 3 }] = "cached value";

string value = cache[new Foo { FooId = 3 }];

This only works reliably if both Equals and GetHashCode are implemented consistently.

4. LINQ operations like Distinct

Methods such as Distinct, Except, Intersect, and GroupBy may depend on equality rules.

var uniqueFoos = foos.Distinct().ToList();

If equality is based on FooId, duplicates by ID can be removed correctly.

Take Quiz

Real Codebase Usage

In real projects, developers often use equality in a few common ways.

Identity-based entities

For database-backed domain models, equality is sometimes based on a stable identifier such as Id.

return FooId == other.FooId;

This is common when objects may be loaded from different places but still represent the same record.

Guard clauses in Equals

Developers usually write fast checks first:

if (other is null) return false;
if (ReferenceEquals(this, other)) return true;

These improve readability and avoid unnecessary work.

Keeping equality and hashing aligned

A common rule in teams is:

  • If a field participates in Equals, it must also participate in GetHashCode
  • If a field does not participate in Equals, it should usually not participate in GetHashCode

Validation and immutability concerns

If an object is used as a dictionary key, the fields involved in hashing should ideally be immutable.

For example, this is risky:

var set = new HashSet<Foo>();
var foo = new Foo { FooId = 1 };
set.Add(foo);
foo.FooId = 2;

Now the object's current hash code no longer matches where it was stored.

Using records when appropriate

In modern C#, developers sometimes use record types for value-based equality because equality and hashing are generated automatically.

public record Foo(int FooId, string FooName);

However, that changes equality semantics to include all record components unless you customize it.

Common Mistakes

1. Overriding Equals but not GetHashCode

This is the classic mistake.

Broken example

public override bool Equals(object obj)
{
    Foo other = obj as Foo;
    return other != null && FooId == other.FooId;
}

If GetHashCode is not updated, hash-based collections may behave incorrectly.

2. Using base.GetHashCode() with custom equality

Broken example

public override int GetHashCode()
{
    return base.GetHashCode();
}

Why it is wrong:

  • base.GetHashCode() is usually based on reference identity
  • Your Equals is based on FooId
  • The two methods now disagree

3. Using different fields in Equals and GetHashCode

Broken example

public override bool Equals(object obj)
{
    Foo other = obj as Foo;
    return other != null && FooId == other.FooId;
}

public override int GetHashCode()
{
    return FooName.GetHashCode();
}

This breaks the equality contract.

4. Using mutable fields for hashing

Risky example

public int FooId { get; set; }

If FooId changes after insertion into a HashSet or Dictionary, lookups may fail.

How to avoid it:

  • Use immutable keys when possible
  • Avoid changing equality-related properties while the object is in hash-based collections

5. Forgetting null safety

Broken example

public override bool Equals(object obj)
{
    return ((Foo)obj).FooId == FooId;
}

This can throw exceptions if obj is null or the wrong type.

Safer version:

public override bool Equals(object obj)
{
    return obj is Foo other && FooId == other.FooId;
}

6. Assuming same hash code means equal

Two different objects can legally produce the same hash code. A hash code is not a unique ID. It is only a fast lookup helper.

Comparisons

ConceptPurposeMust match equality fields?Common use
EqualsDetermines logical equalityYesComparing objects
GetHashCodeHelps place objects in hash bucketsYesDictionary, HashSet
== operatorOperator-based comparisonNot automaticallyConvenience syntax
ReferenceEqualsChecks if two references point to the same objectNoIdentity checks

base.GetHashCode() vs field-based hash code

ApproachWhat it usually meansGood for this Foo class?
base.GetHashCode()Reference identityNo
FooId.GetHashCode()Hash based on logical identityYes

Equals based on ID vs all properties

Equality strategyMeaningExample use
ID-based equalitySame entity/rowORM entities, domain objects
Full-value equalitySame complete valueCoordinates, money, configuration values

If your class represents a database entity, ID-based equality is common. If your class represents a pure value object, all significant fields may belong in both Equals and GetHashCode.

Take Quiz

Cheat Sheet

Quick rules

  • If you override Equals, also override GetHashCode
  • Equal objects must have equal hash codes
  • Use the same fields in both methods
  • Do not use base.GetHashCode() when equality is custom
  • Prefer stable, immutable fields for hashing

Good implementation for the Foo example

public override bool Equals(object obj)
{
    return obj is Foo other && FooId == other.FooId;
}

public override int GetHashCode()
{
    return FooId.GetHashCode();
}

Better pattern with IEquatable<T>

public class Foo : IEquatable<Foo>
{
    public int FooId { get; set; }

    public bool Equals(Foo other)
    {
        return !(other is null) && FooId == other.FooId;
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Foo);
    }

    public override int GetHashCode()
    {
        return FooId.GetHashCode();
    }
}

Edge cases

  • Same hash code does not guarantee equality
  • Mutable hash fields can break HashSet and Dictionary
  • Null and wrong-type comparisons should return false
  • Hash-based collections use GetHashCode first, then Equals

FAQ

Why do I need to override GetHashCode if Equals already works?

Because hash-based collections use GetHashCode to find where to look. If equal objects return different hash codes, lookups can fail.

Is base.GetHashCode() ever correct?

Yes, if your equality is also based on reference identity. It is not correct when you define custom logical equality, such as comparing FooId.

Should GetHashCode use all properties?

Only the properties that participate in equality. If Equals only checks FooId, GetHashCode should also only use FooId.

Can two different objects have the same hash code?

Yes. That is allowed. Hash codes are not unique identifiers.

What happens if I change FooId after adding an object to a HashSet?

The object may become hard to find or remove because its hash code changes after insertion.

Should entity equality be based on database ID?

Often yes, especially when the ID is stable and uniquely identifies the entity. But this depends on the domain model and lifecycle of the entity.

Is implementing IEquatable<Foo> required?

No, but it is recommended. It gives a cleaner, strongly typed equality implementation and is common in real C# code.

Take Quiz

Mini Project

Description

Create a small C# program that stores Foo objects in a HashSet<Foo> and demonstrates why matching Equals and GetHashCode matters. This project helps you see the difference between correct and incorrect implementations in a practical way.

Goal

Build a console app where Foo objects with the same FooId are treated as the same logical item inside a HashSet<Foo>.

Requirements

  • Create a Foo class with FooId and FooName properties.
  • Override Equals so that two Foo objects are equal when their FooId values match.
  • Override GetHashCode using the same field used by Equals.
  • Add multiple Foo objects to a HashSet<Foo>, including duplicates by FooId.
  • Print the number of unique items and show whether a logically equal object is found in the set.
Take Quiz

Keep learning

Related questions

View more