Skip to content
Happy Programming Guide
Start learning
C#

Distinct() Not Working on Objects in C#

Distinct removes duplicates by equality, and for classes equality means the same instance. Three fixes: DistinctBy, a record, or IEquatable, and which to pick.

Two monitors on a developer desk

Distinct() appears not to work on a list of objects because it is working exactly as defined: it removes elements that are equal, and two instances of a class are equal only if they are the same instance. Two Customer objects with identical fields are not equal, so both survive. The fix is to tell LINQ what equality means for your type, and there are three ways to do it.

What is happening#

CSHARP
class Customer
{
    public string Email { get; set; } = "";
    public string Name { get; set; } = "";
}

var list = new List<Customer>
{
    new() { Email = "a@example.com", Name = "Ann" },
    new() { Email = "a@example.com", Name = "Ann" },
};

list.Distinct().Count();     // 2 - both kept

Distinct uses EqualityComparer<T>.Default, which calls Equals and GetHashCode. A class inherits both from object, where they mean “same reference”. Value types and strings compare by value, which is why Distinct on a List<int> or List<string> works and on a List<Customer> does not.

Fix 1: DistinctBy#

CSHARP
var unique = list.DistinctBy(c => c.Email);

Available since .NET 6. You name the key that defines “the same”, and the first element with each key is kept. For a composite key, use a tuple:

CSHARP
var unique = list.DistinctBy(c => (c.Email, c.Name));

This is the right answer when the notion of duplicate is specific to this query rather than intrinsic to the type. It changes nothing about the class.

Fix 2: make it a record#

CSHARP
record Customer(string Email, string Name);

list.Distinct().Count();     // 1

Records get value-based Equals and GetHashCode generated over all their properties. If your type is a data holder that should compare by content everywhere — DTOs, keys, values — a record is the correct declaration, and Distinct, Contains, GroupBy, dictionary keys and HashSet all start behaving.

The same applies to record struct for small value types.

Fix 3: implement IEquatable#

When the type must stay a class and equality should be by some of its fields:

CSHARP
class Customer : IEquatable<Customer>
{
    public string Email { get; set; } = "";
    public string Name { get; set; } = "";

    public bool Equals(Customer? other) =>
        other is not null &&
        string.Equals(Email, other.Email, StringComparison.OrdinalIgnoreCase);

    public override bool Equals(object? obj) => Equals(obj as Customer);

    public override int GetHashCode() =>
        StringComparer.OrdinalIgnoreCase.GetHashCode(Email);
}

All three members are needed. IEquatable<T> is what LINQ prefers; Equals(object) keeps non-generic code consistent; and GetHashCode is what makes Distinct fast — and correct.

Fix 4: a comparer, for one call#

CSHARP
class CustomerByEmail : IEqualityComparer<Customer>
{
    public bool Equals(Customer? a, Customer? b) => a?.Email == b?.Email;
    public int GetHashCode(Customer c) => c.Email.GetHashCode();
}

list.Distinct(new CustomerByEmail());

Useful when the type is not yours and the rule is reused in several places. For a single query, DistinctBy is shorter.

Mutable objects and hash codes#

If equality depends on a property and that property changes after the object is in a HashSet or used as a dictionary key, the object is filed under its old hash and becomes unfindable. Either base equality on immutable fields, or make the type immutable — which is what records encourage.

Distinct and order#

Distinct preserves the order of first occurrence, though the documentation does not promise it. If order matters, DistinctBy after an OrderBy makes the intent explicit: sort so that the element you want to keep comes first.

Questions people ask#

Why does Distinct work on strings?

Because string overrides Equals and GetHashCode to compare contents. Your class inherits the reference-based defaults from object.

Does GroupBy have the same problem?

Yes, and the same fixes. GroupBy(c => c.Email) groups by a string key, which is why it usually works when Distinct on the object does not.

Is DistinctBy available in older .NET?

From .NET 6. On older frameworks, GroupBy(key).Select(g => g.First()) does the same job.

Should every class implement IEquatable?

No. Only types with a meaningful value identity. For most entity classes, reference equality is correct and a record or DTO handles the value cases.

Where to go next#

C# collections: where equality mattersRead next

Keep reading

C#

C# 14 Extension Members Explained

Extension members let you add properties, static members and operators to types you do not own, not just methods. The new extension…

4 min read

C#

C# Generics

Generic classes, methods and constraints in C#, why the generic List beats ArrayList, and the variance keywords in and out explained plainly.

5 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *