Question

LINQ Distinct Equality Comparers and DistinctBy in C#

csharpc#-3.0lambdaextension-methods

Question

Given an IEnumerable<Customer>, how can I return distinct customers based on CustomerId?

Enumerable.Distinct() works without arguments for simple values:

var distinctValues = myStringList.Distinct();

For custom objects, the available overload accepts an IEqualityComparer<T>:

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

Is there an overload that accepts a lambda such as the following?

var distinctValues = myCustomerList.Distinct(
    (c1, c2) => c1.CustomerId == c2.CustomerId);

If not, what is the appropriate concise way to define equality inline or otherwise?

Short Answer

You will learn why LINQ Distinct needs both equality and a hash code, how to create an IEqualityComparer<T>, and when DistinctBy is the clearer choice for removing duplicates by one property.

Concept

Distinct removes duplicate values from a sequence. For primitive values such as string, int, and DateTime, .NET already knows how to compare values.

For a custom class such as Customer, there is no universal definition of “duplicate.” Two Customer objects may be considered equal because they have the same CustomerId, email address, or combination of fields. Your application must define that rule.

Enumerable.Distinct uses an IEqualityComparer<T> because it needs two compatible operations:

  • Equals(x, y): determines whether two values represent the same item.
  • GetHashCode(x): produces a hash code used to find likely matches efficiently.

Internally, Distinct uses a hash-based set. A comparison lambda such as Func<T, T, bool> supplies only the first operation. Without a matching hash-code rule, LINQ cannot reliably and efficiently organize the values.

A key rule is: if Equals(x, y) returns true, GetHashCode(x) and GetHashCode(y) must return the same value.

Distinct is deferred: creating the query does not enumerate it immediately. Enumeration begins when you iterate over it or call an operation such as ToList().

Mental Model

Think of Distinct as sorting mail into numbered mailboxes.

  • The hash code is the mailbox number. It quickly narrows down where an item might belong.
  • Equals is the final name-and-address check for items already in that mailbox.

A lambda that only says “these two customers have the same ID” provides the final check, but not the mailbox number. IEqualityComparer<T> packages both rules together so the mailbox system works correctly.

When your definition of uniqueness is simply one value, such as CustomerId, DistinctBy is like writing that value directly on each envelope and keeping only the first envelope with each label.

Take Quiz

Syntax and Examples

The basic overload uses the type's default equality:

IEnumerable<string> names = new[] { "Ada", "Grace", "Ada" };

var uniqueNames = names.Distinct();
// Ada, Grace

For a custom rule, provide an IEqualityComparer<Customer>:

using System;
using System.Collections.Generic;
using System.Linq;

public sealed class Customer
{
    public int CustomerId { get; init; }
    public string Name { get; init; } = "";
}

public sealed class CustomerIdComparer : IEqualityComparer<Customer>
{
    public bool Equals(Customer? x, Customer? y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x is null || y is null) return false;

        return x.CustomerId == y.CustomerId;
    }

    public int GetHashCode(Customer customer)
    {
        return customer.CustomerId.GetHashCode();
    }
}

var customers = new[]
{
    new Customer { CustomerId = 10, Name = "Ava" },
    new Customer { CustomerId = 20, Name = "Noah" },
    new Customer { CustomerId = 10, Name = "Ava Updated" }
};

var uniqueCustomers = customers.Distinct(new CustomerIdComparer());

The result contains the first customer with ID 10 and the customer with ID 20. Distinct keeps the first occurrence it encounters for each equal group.

Prefer DistinctBy when available

In .NET 6 and later, DistinctBy is usually the most direct option when uniqueness comes from one key:

var uniqueCustomers = customers.DistinctBy(customer => customer.CustomerId);

The selector lambda returns the key used for uniqueness. The key type's default comparer is used, or you can supply a comparer for the key:

var uniqueEmails = customers.DistinctBy(
    customer => customer.Email,
    StringComparer.OrdinalIgnoreCase);

Step by Step Execution

Consider this example:

var customers = new[]
{
    new Customer { CustomerId = 1, Name = "Mina" },
    new Customer { CustomerId = 2, Name = "Theo" },
    new Customer { CustomerId = 1, Name = "Mina (new record)" }
};

var result = customers.DistinctBy(c => c.CustomerId).ToList();

Execution proceeds as follows:

  1. DistinctBy reads the first customer. Its key is 1, which has not appeared, so it is included.
  2. It reads the second customer. Its key is 2, which is new, so it is included.
  3. It reads the third customer. Its key is 1, which has already appeared, so it is skipped.
  4. ToList() forces enumeration and stores the selected objects in a list.

result contains Mina and Theo. The later customer with ID 1 is not included.

With Distinct(new CustomerIdComparer()), the flow is conceptually the same, but the comparer supplies both the equality and hash-code behavior for complete Customer objects.

Real World Use Cases

Common uses of distinct operations include:

  • API results: Return one product per SKU when an upstream service sends duplicates.
  • Database processing: Remove repeated customer IDs before making a batch query.
  • Import scripts: Keep one row for each external account identifier in a CSV import.
  • UI lists: Build a dropdown with one category per category ID.
  • Event processing: Avoid handling the same event ID twice in a batch.
  • Case-insensitive text: Deduplicate usernames or tags regardless of letter casing with StringComparer.OrdinalIgnoreCase.

Choose the uniqueness rule deliberately. For example, removing duplicate emails might be correct for a mailing list but incorrect for customer records if shared email addresses are allowed.

Take Quiz

Real Codebase Usage

In production code, prefer the most expressive option for the rule you need.

Use DistinctBy for a single identity field

var activeCustomerIds = orders
    .Where(order => order.IsActive)
    .DistinctBy(order => order.CustomerId)
    .Select(order => order.CustomerId);

This clearly states that CustomerId defines uniqueness.

Reuse a named comparer for a domain-wide rule

If many parts of an application treat customers with the same ID as equivalent, use a named comparer:

public static class CustomerComparers
{
    public static IEqualityComparer<Customer> ById { get; } = new CustomerIdComparer();
}

var uniqueCustomers = customers.Distinct(CustomerComparers.ById);

A reusable comparer prevents different parts of the codebase from silently using different definitions of equality.

Define equality on the type when it is intrinsic

If a value object's identity is genuinely all of its data, implement value equality on the type or use a record:

public record CurrencyAmount(decimal Amount, string Currency);

var amounts = new[]
{
    new CurrencyAmount(10m, "USD"),
    new CurrencyAmount(10m, "USD")
};

var uniqueAmounts = amounts.Distinct();

Do not redefine a mutable entity's global equality casually. A customer can be distinct in one situation by ID and in another situation by email; DistinctBy or a dedicated comparer keeps those context-specific rules explicit.

Common Mistakes

Supplying only an equality predicate

This does not compile because Distinct has no Func<T, T, bool> overload:

// Does not compile
var uniqueCustomers = customers.Distinct(
    (a, b) => a.CustomerId == b.CustomerId);

Use DistinctBy(c => c.CustomerId) when possible, or write an IEqualityComparer<Customer>.

Implementing Equals without a compatible hash code

This comparer is incorrect:

public int GetHashCode(Customer customer)
{
    return customer.Name.GetHashCode(); // Wrong: equality is based on CustomerId
}

Two customers with the same ID but different names would compare equal while potentially having different hash codes. Base the hash code on the same fields used by Equals.

public int GetHashCode(Customer customer)
{
    return customer.CustomerId.GetHashCode();
}

Comparing strings with the wrong rule

"ADA" and "ada" are different under ordinal, case-sensitive comparison. If business rules treat them as the same, specify that rule:

var uniqueUsernames = usernames.Distinct(StringComparer.OrdinalIgnoreCase);

Expecting the last duplicate to win

Distinct and DistinctBy retain the first item encountered. To keep the latest item, order or group the sequence intentionally:

var latestByCustomer = customers
    .OrderByDescending(c => c.UpdatedAt)
    .DistinctBy(c => c.CustomerId);

Mutating fields used for hashing

Do not change CustomerId while a customer is stored in a HashSet<Customer> or used as a dictionary key with an ID comparer. Changing a field that determines equality or hashing can make the item difficult to find.

Comparisons

OptionBest whenExample
Distinct()The type already has the desired default equality.names.Distinct()
DistinctBy(keySelector)Uniqueness is based on one key; available in .NET 6+.customers.DistinctBy(c => c.CustomerId)
Distinct(comparer)Equality depends on multiple fields or needs a reusable domain rule.customers.Distinct(CustomerComparers.ById)
GroupBy(keySelector)You need every item in each duplicate group, counts, or a custom winner.customers.GroupBy(c => c.CustomerId)
HashSet<T>You want to build a unique collection incrementally.new HashSet<Customer>(CustomerComparers.ById)

DistinctBy is not simply a shorter name for arbitrary custom equality. It selects a key and uses equality for that key. Use a full comparer when the equivalence rule is more complex than selecting one key, such as matching both CountryCode and PhoneNumber.

Take Quiz

Cheat Sheet

// Default equality
var unique = items.Distinct();

// One uniqueness key (.NET 6+)
var uniqueById = customers.DistinctBy(c => c.CustomerId);

// Case-insensitive string keys
var uniqueEmails = customers.DistinctBy(
    c => c.Email,
    StringComparer.OrdinalIgnoreCase);

// Full custom equality rule
var uniqueCustomers = customers.Distinct(new CustomerIdComparer());

Rules for IEqualityComparer<T>:

  • Implement both Equals(T? x, T? y) and GetHashCode(T obj).
  • If two values are equal, their hash codes must match.
  • Hash-code collisions are allowed; equal hash codes do not mean values are equal.
  • Base the hash code on the same fields used by equality.
  • Distinct preserves the first occurrence.
  • Distinct executes when enumerated; use ToList() to evaluate it now.
  • Use StringComparer.OrdinalIgnoreCase when case-insensitive string identity is required.

FAQ

Why does Distinct require GetHashCode?

Distinct uses a hash-based set for efficient duplicate detection. A hash code quickly narrows the search, and Equals confirms whether values are actually equal.

Can I pass a lambda directly to C# Distinct?

Not as an equality predicate. The built-in overload accepts an IEqualityComparer<T>, which includes both equality and hashing. Use DistinctBy(x => x.Property) for one-property uniqueness in .NET 6+.

What is the difference between Distinct and DistinctBy?

Distinct compares whole elements using their equality comparer. DistinctBy extracts a key from each element and compares those keys.

Does Distinct keep the first or last duplicate?

It keeps the first element encountered for each unique value or key.

Does Distinct work with custom classes by default?

Yes, but default class equality is often reference equality unless the type overrides equality or uses a suitable value-based design. Two separately created objects may therefore remain distinct even if their properties match.

Can equal objects have different hash codes?

No. If Equals returns true, both objects must return the same hash code. Different objects are allowed to share a hash code.

When should I use GroupBy instead of DistinctBy?

Use GroupBy when you need duplicate counts, all members of a group, or logic to choose a specific representative such as the newest record.

Take Quiz

Mini Project

Description

Build a small cleanup step for imported customer data. The import contains repeated customer records, and the application must keep one record per customer ID. This demonstrates choosing a stable identity key and using LINQ without writing a verbose comparer.

Goal

Produce a list containing only the first imported customer for each CustomerId.

Requirements

Use a Customer type with an ID and name.

Create sample data containing at least two records with the same ID.

Use LINQ to retain one customer for each ID.

Materialize and print the resulting customers.

Ensure the first record for a duplicate ID is kept.

Take Quiz

Keep learning

Related questions

View more