IEqualityComparer
delegate
C#
programming
custom comparer

Wrap a delegate in an IEqualityComparer

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

LINQ methods like Distinct(), GroupBy(), and Except() accept an IEqualityComparer<T> parameter, but creating a full class for each comparison is verbose. A delegate wrapper lets you pass a lambda instead. You create a generic class that implements IEqualityComparer<T> by wrapping a Func<T, T, bool> for equality and optionally a Func<T, int> for hash codes. This pattern eliminates boilerplate when you need one-off custom comparisons.

The Problem

csharp
1// You want to use Distinct() with custom equality logic
2var products = new List<Product>
3{
4    new("Widget", "Electronics"),
5    new("Widget", "Home"),
6    new("Gadget", "Electronics"),
7};
8
9// Distinct by name only — but Distinct() needs IEqualityComparer<Product>
10// You must create an entire class:
11class ProductNameComparer : IEqualityComparer<Product>
12{
13    public bool Equals(Product x, Product y) => x.Name == y.Name;
14    public int GetHashCode(Product obj) => obj.Name.GetHashCode();
15}
16
17var unique = products.Distinct(new ProductNameComparer());
18// Creating a class for every comparison is tedious

The Solution: Delegate Wrapper

csharp
1public class LambdaEqualityComparer<T> : IEqualityComparer<T>
2{
3    private readonly Func<T, T, bool> _equals;
4    private readonly Func<T, int> _getHashCode;
5
6    public LambdaEqualityComparer(Func<T, T, bool> equals, Func<T, int> getHashCode)
7    {
8        _equals = equals ?? throw new ArgumentNullException(nameof(equals));
9        _getHashCode = getHashCode ?? throw new ArgumentNullException(nameof(getHashCode));
10    }
11
12    public bool Equals(T x, T y)
13    {
14        if (ReferenceEquals(x, y)) return true;
15        if (x is null || y is null) return false;
16        return _equals(x, y);
17    }
18
19    public int GetHashCode(T obj)
20    {
21        return obj is null ? 0 : _getHashCode(obj);
22    }
23}

Usage

csharp
1var comparer = new LambdaEqualityComparer<Product>(
2    (x, y) => x.Name == y.Name,
3    obj => obj.Name.GetHashCode()
4);
5
6var unique = products.Distinct(comparer).ToList();
7// [Widget (Electronics), Gadget (Electronics)]

Simplified Version: Key-Based Comparer

Most custom comparisons compare a single property. A key-based comparer is even cleaner:

csharp
1public class KeyEqualityComparer<T, TKey> : IEqualityComparer<T>
2{
3    private readonly Func<T, TKey> _keySelector;
4
5    public KeyEqualityComparer(Func<T, TKey> keySelector)
6    {
7        _keySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector));
8    }
9
10    public bool Equals(T x, T y)
11    {
12        if (ReferenceEquals(x, y)) return true;
13        if (x is null || y is null) return false;
14        return EqualityComparer<TKey>.Default.Equals(_keySelector(x), _keySelector(y));
15    }
16
17    public int GetHashCode(T obj)
18    {
19        return obj is null ? 0 : EqualityComparer<TKey>.Default.GetHashCode(_keySelector(obj));
20    }
21}
22
23// Static helper for type inference
24public static class KeyEqualityComparer
25{
26    public static KeyEqualityComparer<T, TKey> Create<T, TKey>(Func<T, TKey> keySelector)
27        => new(keySelector);
28}
csharp
1// Now comparisons are one-liners
2var byName = products.Distinct(KeyEqualityComparer.Create<Product, string>(p => p.Name));
3var byCategory = products.Distinct(KeyEqualityComparer.Create<Product, string>(p => p.Category));
4
5// Composite key
6var byBoth = products.Distinct(
7    KeyEqualityComparer.Create<Product, (string, string)>(p => (p.Name, p.Category))
8);

Extension Method for Fluent API

csharp
1public static class EnumerableExtensions
2{
3    public static IEnumerable<T> DistinctBy<T, TKey>(
4        this IEnumerable<T> source, Func<T, TKey> keySelector)
5    {
6        var comparer = new KeyEqualityComparer<T, TKey>(keySelector);
7        return source.Distinct(comparer);
8    }
9}
10
11// Usage — clean and fluent
12var unique = products.DistinctBy(p => p.Name).ToList();

Note: .NET 6+ includes Enumerable.DistinctBy natively, making this extension unnecessary for newer frameworks.

Using with Dictionaries and HashSets

csharp
1// HashSet with custom comparer
2var set = new HashSet<Product>(
3    new LambdaEqualityComparer<Product>(
4        (x, y) => x.Name == y.Name,
5        obj => obj.Name.GetHashCode()
6    )
7);
8
9set.Add(new Product("Widget", "Electronics"));
10set.Add(new Product("Widget", "Home"));       // Not added — same Name
11set.Count;  // 1
12
13// Dictionary with custom key comparer
14var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
15dict["Hello"] = 1;
16dict["hello"] = 2;  // Overwrites — case-insensitive
17dict.Count;  // 1

GroupBy and Except with Custom Comparer

csharp
1var comparer = KeyEqualityComparer.Create<Product, string>(p => p.Name);
2
3// GroupBy with custom equality
4var groups = products.GroupBy(p => p, comparer);
5
6// Except with custom equality
7var list1 = new[] { new Product("A", "X"), new Product("B", "Y") };
8var list2 = new[] { new Product("A", "Z") };
9var diff = list1.Except(list2, comparer).ToList();
10// [Product("B", "Y")] — "A" removed because names match

Common Pitfalls

  • Forgetting GetHashCode: IEqualityComparer<T> requires both Equals and GetHashCode. If GetHashCode returns different values for "equal" objects, hash-based collections (HashSet, Dictionary, Distinct) will treat them as different even though Equals returns true.
  • Using a constant hash code: Returning a constant like 0 from GetHashCode is technically correct but degrades hash-based collections to O(n) performance per lookup (every object lands in the same bucket). Always derive the hash from the same fields used in Equals.
  • Null reference in delegate: If the delegate does not handle null arguments, passing a collection with null elements causes NullReferenceException. The wrapper should check for null before invoking the delegate.
  • Mutable keys: If the key used for equality (e.g., Name) changes after the object is added to a HashSet or Dictionary, the object becomes unreachable. The hash code at insertion time no longer matches. Only use immutable properties as keys.
  • Not using .NET 6+ DistinctBy: Starting with .NET 6, Enumerable.DistinctBy, UnionBy, ExceptBy, and IntersectBy are built in. If targeting .NET 6+, use these instead of building custom comparers.

Summary

  • Wrap Func<T, T, bool> and Func<T, int> in an IEqualityComparer<T> class to pass lambdas to LINQ methods
  • A key-selector comparer (Func<T, TKey>) covers most use cases with less code
  • Always implement both Equals and GetHashCode consistently — hash-based collections depend on both
  • .NET 6+ provides DistinctBy, ExceptBy, etc., eliminating the need for custom comparers in many cases
  • Use EqualityComparer<TKey>.Default inside the wrapper to leverage built-in equality for the key type

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.