This is a custom equality comparer for C# that allows you to compare objects based on a specific property or calculation you define.
What Can It Do
Instead of comparing entire objects, this comparer lets you specify which part of an object to compare. It’s particularly useful with LINQ methods like Distinct(), GroupBy(), or when using collections like HashSet<T> or Dictionary<TKey, TValue>.
How Does It Work
First, we have this following constructor:
| |
It takes a function that extracts the value you want to compare from each object. For example, if you have a Person object, you might pass p => p.LastName, the projection function, to compare people by their last name only.
Then, we want to implement the Equals method:
| |
It compares two objects by applying your projection function to each, then comparing the results. So it’s comparing the projected values, not the full objects.
Finally, we implement the GetHashCode method:
| |
It returns the hash code of the projected value. Hash-based collections first use the hash code to find a bucket, then call Equals only on items in that bucket. If two objects are equal according to Equals, they must therefore produce the same hash code, otherwise they would land in different buckets and never be compared.
The ?. and ?? 0 handle the case where the projection returns null (for example, a Person without a LastName): object.Equals already handles null safely, but calling GetHashCode() on null would throw a NullReferenceException.
Putting it all together, the full comparer looks like this:
| |
Let’s See a Full Example
We design our class:
| |
In practice, Distinct() keeps the first item it meets for each last name, so John stays and Jane goes. However, the documentation describes the result sequence as unordered, so don’t build logic that depends on that order.
This pattern is useful when you need flexible comparison logic without creating multiple comparer classes for different scenarios.
If all you need is Distinct() and you target .NET 6 or later, people.DistinctBy(p => p.LastName) gives the same result without a custom comparer. The comparer remains useful for HashSet<T>, Dictionary<TKey, TValue>, or older target frameworks.
Why GetHashCode Matters Here
Suppose the comparer kept the projection-based Equals but used the default obj.GetHashCode(), which is reference-based for classes (the hash values below are illustrative, not real computed values):
- John Smith gets hash
48291, so it goes into bucket A. - Jane Smith gets hash
91734, so it goes into bucket B. Distinct()never compares them withEquals, because they are in different buckets.- Result: both are kept, even though the comparer says they are equal.
With the comparer’s GetHashCode, both hash "Smith". They land in the same bucket, Equals runs and returns true, and Jane is dropped.
The rule only goes one way. Equal objects must have equal hashes, but equal hashes don’t mean the objects are equal (that’s a collision, and Equals sorts it out). That’s why hashing the same projected value that Equals compares meets the contract.
What Are the Projected Values in the Example Above
The projected values are not the hash codes—they’re the actual values returned by your projection function.
Let me clarify:
Projected value = whatever your Func<T, object> returns.
For example:
| |
So, in the Equals method:
_projection(x)returns the actual value (e.g., “Smith”)_projection(y)returns the actual value (e.g., “Jones”)- Then it compares these values:
"Smith"vs"Jones"= false
In the GetHashCode method:
| |
_projection(obj)returns the value (e.g., “Smith”)- `.GetHashCode()’ then converts “Smith” into a hash code (e.g., 12345678)
So: the projected value is actual data value (like a string, number, etc.)
hash code is an integer representation of that value for fast lookup
The hash code is derived from the projected value, but they’re different things.
Sources for This Article
- Microsoft Documentation:
IEqualityComparer<T>Interface: defines theEquals(T, T)andGetHashCode(T)pair that a custom comparer implements.IEqualityComparer<T>.GetHashCode(T)Method: states that ifEqualsreturnstruefor two objects,GetHashCodemust return the same value for both.Object.GetHashCodeMethod: explains that equal hash codes don’t imply equality, that the default implementation for reference types is based on the object’s reference, and thatGetHashCode()should not throw exceptions.Enumerable.DistinctMethod: documents the overload that accepts anIEqualityComparer<T>and notes that the result sequence is unordered.Enumerable.DistinctByMethod: available since .NET 6, it removes duplicates using a key selector instead of a custom comparer.
- .NET Runtime Source Code:
Distinct.cs: shows thatDistinct()builds aHashSet<TSource>with the comparer you pass in, which is whyGetHashCodedecides which items get compared.
Follow me
Thanks for reading this article. Make sure to follow me on X, subscribe to my Substack publication and bookmark my blog to read more in the future.
Credit : Photo by SHVETS production on Pexels (https://www.pexels.com/photo/person-holding-toy-cars-9742535/).
