Question
I am learning LINQ and want to understand how to use Distinct() when working with a list of objects rather than a simple list of values.
For example, suppose I have a Person class with an Id property:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
And I have the following data:
var people = new List<Person>
{
new Person { Id = 1, Name = "Test1" },
new Person { Id = 1, Name = "Test1" },
new Person { Id = 2, Name = "Test2" }
};
How can I return only the unique Person objects based on the Id property, so that the result contains only:
Person1: Id = 1, Name = "Test1"Person3: Id = 2, Name = "Test2"
Is this possible directly with LINQ?
If not, what is the best way to get distinct objects from a List<T> based on one or more of their properties?
Short Answer
By the end of this page, you will understand why Distinct() does not automatically compare object properties, how equality works for custom classes in C#, and the common ways to get unique objects by one or more properties. You will also see practical approaches using GroupBy, custom equality comparers, and DistinctBy in modern C#.
Concept
Distinct() in LINQ removes duplicate items based on equality. For simple types like int, string, or DateTime, this works as you expect because those types already define value-based equality.
For custom classes like Person, the default behavior is different. Unless you define equality yourself, C# compares object references, not property values. That means two different Person instances with the same Id and Name are still considered different objects.
var p1 = new Person { Id = 1, Name = "Test1" };
var p2 = new Person { Id = 1, Name = "Test1" };
Console.WriteLine(p1 == p2); // False
Even though the property values match, these are two separate objects in memory.
So when you call:
people.Distinct()
LINQ only removes duplicates if the objects are considered equal by:
Equals()GetHashCode()- or a supplied
IEqualityComparer<T>
Why this matters
In real applications, you often need uniqueness based on business rules, not full object identity. For example:
- unique users by
Id - unique products by
Sku - unique orders by
OrderNumber - unique records by a combination like
FirstName + LastName + DateOfBirth
That is why knowing how to define equality or choose a LINQ pattern is important.
Main ways to get distinct objects by property
There are three common approaches:
- Use
GroupBy()and take the first item from each group - Use
DistinctBy()if your .NET version supports it - Use
Distinct()with a custom comparer
Each option is valid. The best choice depends on your C#/.NET version and whether the equality rule should belong to the class itself or only to one query.
Mental Model
Think of Distinct() as a bouncer checking names on a guest list.
- For simple values like numbers, the bouncer can easily tell whether
1is the same as another1. - For objects like
Person, the bouncer does not automatically inspect theIdorNamebadge. - Instead, by default, the bouncer only checks whether it is literally the same person instance entering again.
If you want the bouncer to treat two Person objects with the same Id as duplicates, you must give the bouncer a rule such as:
- “Compare only
Id” - “Compare
IdandName”
That rule can be provided by:
- grouping by a property
- selecting a distinct key
- or writing a custom comparer
Syntax and Examples
1. Using GroupBy()
This is one of the easiest and most readable ways.
var distinctPeople = people
.GroupBy(p => p.Id)
.Select(group => group.First())
.ToList();
How it works
GroupBy(p => p.Id)creates one group for each uniqueIdSelect(group => group.First())picks onePersonfrom each groupToList()turns the result back into a list
For the sample data, the result contains:
Id = 1, Name = "Test1"
Id = 2, Name = "Test2"
2. Using DistinctBy() in modern .NET
If you are using .NET 6 or later, LINQ includes DistinctBy().
var distinctPeople = people
.DistinctBy(p => p.Id)
.ToList();
This is often the cleanest solution.
Multiple properties
You can also use more than one property:
var distinctPeople = people
.DistinctBy(p => new { p.Id, p.Name })
.ToList();
Or with tuples:
var distinctPeople = people
.DistinctBy(p => (p.Id, p.Name))
.ToList();
3. Using Distinct() with a custom comparer
If you want to keep using Distinct(), you can supply an IEqualityComparer<Person>.
public class PersonIdComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
return x.Id == y.Id;
}
public int GetHashCode(Person obj)
{
return obj.Id.GetHashCode();
}
}
Usage:
var distinctPeople = people
.Distinct(new PersonIdComparer())
.ToList();
Comparing multiple properties
public class PersonIdNameComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
return x.Id == y.Id && x.Name == y.Name;
}
public int GetHashCode(Person obj)
{
return HashCode.Combine(obj.Id, obj.Name);
}
}
4. Projecting keys only
If you only need the distinct property values, not the full objects:
var distinctIds = people
.Select(p => p.Id)
.Distinct()
.ToList();
This returns only unique Id values, not Person objects.
Step by Step Execution
Consider this example:
var people = new List<Person>
{
new Person { Id = 1, Name = "Test1" },
new Person { Id = 1, Name = "Test1" },
new Person { Id = 2, Name = "Test2" }
};
var distinctPeople = people
.GroupBy(p => p.Id)
.Select(group => group.First())
.ToList();
Step-by-step
Step 1: Start with the original list
[
{ Id = 1, Name = "Test1" },
{ Id = 1, Name = "Test1" },
{ Id = 2, Name = "Test2" }
]
Step 2: GroupBy(p => p.Id)
LINQ groups items by Id.
Group 1:
Key = 1
Items = [
{ Id = 1, Name = "Test1" },
{ Id = 1, Name = "Test1" }
]
Group 2:
Key = 2
Items = [
{ Id = 2, Name = "Test2" }
]
Step 3: Select(group => group.First())
From each group, LINQ picks the first object.
Result:
[
{ Id = 1, Name = "Test1" },
{ Id = 2, Name = "Test2" }
]
Step 4: ToList()
The query is converted into a List<Person>.
Important detail
If several objects share the same key, First() keeps the first one encountered. If you need a different rule, you can change it.
For example, choose the person with the longest name:
var distinctPeople = people
.GroupBy(p => p.Id)
.Select(group => group.OrderByDescending(p => p.Name.Length).First())
.ToList();
Real World Use Cases
Common situations where distinct-by-property is useful
Removing duplicate database results
A query or join may return repeated entities with the same ID.
var uniqueUsers = users.DistinctBy(u => u.Id).ToList();
Importing CSV or API data
A file may contain duplicate rows for the same product.
var uniqueProducts = products.GroupBy(p => p.Sku).Select(g => g.First()).ToList();
Keeping one log entry per request
You may want only one record per request ID.
var uniqueRequests = logs.DistinctBy(log => log.RequestId).ToList();
Merging data from multiple sources
When combining lists, duplicates are common.
var merged = listA.Concat(listB)
.DistinctBy(x => x.Id)
.ToList();
De-duplicating form submissions
If users accidentally submit the same entry twice, you may want to keep just one.
var uniqueEmails = submissions.DistinctBy(s => s.Email).ToList();
Real Codebase Usage
In real projects, developers usually choose one of these patterns depending on where the equality rule belongs.
Query-level uniqueness
If the rule is specific to one query, developers often use GroupBy() or DistinctBy() directly in that query.
var activeUsers = users
.Where(u => u.IsActive)
.DistinctBy(u => u.Id)
.ToList();
This is useful when the uniqueness rule is local and should not affect the entire class.
Reusable comparer objects
If the same distinct logic appears in several places, a custom comparer is common.
var comparer = new PersonIdComparer();
var result = people.Distinct(comparer).ToList();
This avoids repeating the same grouping logic everywhere.
Guarding against null values
In real code, developers often validate input before running LINQ.
if (people == null)
{
return new List<Person>();
}
return people.DistinctBy(p => p.Id).ToList();
Choosing which duplicate to keep
Sometimes duplicates are not identical. Developers then define a rule for which record wins.
Examples:
- keep the first record
- keep the newest record
- keep the most complete record
- keep the highest priority record
var latestPeople = people
.GroupBy(p => p.Id)
.Select(g => g.OrderByDescending(p => p.LastUpdated).First())
.ToList();
Validation and normalization
Before applying distinct logic, data may need cleanup.
var uniquePeople = people
.Where(p => p != null)
.Where(p => p.Name != null)
.DistinctBy(p => new { p.Id, NormalizedName = p.Name.Trim().ToLower() })
.ToList();
This helps avoid treating "Alice" and " alice " as different values when the business rule says they should match.
Common Mistakes
1. Expecting Distinct() to compare properties automatically
Broken example:
var distinctPeople = people.Distinct().ToList();
Why it fails:
- For custom classes, default equality is usually reference-based
- Two separate
Personobjects with the same data are still different
Use one of these instead:
var distinctPeople = people.GroupBy(p => p.Id).Select(g => g.First()).ToList();
or
var distinctPeople = people.DistinctBy(p => p.Id).ToList();
2. Writing Equals() without GetHashCode()
Broken example:
public override bool Equals(object obj)
{
return obj is Person other && Id == other.Id;
}
Why it is a problem:
- Hash-based operations like
Distinct()depend on both methods - If
GetHashCode()is inconsistent, results may be wrong or inefficient
Fix:
public override bool Equals(object obj)
{
return obj is Person other && Id == other.Id;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
3. Using mutable properties for equality
If equality depends on a property that changes later, collection behavior can become confusing.
var person = new Person { Id = 1, Name = "A" };
If Id changes after insertion into a hash-based collection, matching may break.
Avoid using frequently changing fields as equality keys.
4. Forgetting null checks in custom comparers
Broken example:
public bool Equals(Person x, Person y)
{
return x.Id == y.Id;
}
If x or y is null, this throws an exception.
Safer version:
public bool Equals(Person x, Person y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
return x.Id == y.Id;
}
5. Confusing distinct objects with distinct property values
These are not the same:
var ids = people.Select(p => p.Id).Distinct().ToList();
This returns unique IDs.
var persons = people.DistinctBy(p => p.Id).ToList();
This returns Person objects.
Comparisons
| Approach | Best for | Returns | Pros | Cons |
|---|---|---|---|---|
Distinct() | Types with built-in equality or custom comparer | Original items | Simple when equality is already defined | Does not work by property automatically |
Distinct(new Comparer()) | Reusable custom equality logic | Original items | Flexible and explicit | Requires extra comparer code |
GroupBy(...).Select(g => g.First()) | Distinct by property in any LINQ version | Original items | Easy to understand, widely available | Slightly more verbose |
DistinctBy(...) | Modern .NET, distinct by key | Original items | Clean and readable | Requires .NET 6+ |
Select(...).Distinct() | Distinct scalar values only | Keys or projected values | Great when you only need the property values | Does not return the original object |
Distinct() vs GroupBy()
people.Distinct(new PersonIdComparer())
- good when you want a reusable equality rule
- better if the same logic appears often
people.GroupBy(p => p.Id).Select(g => g.First())
- good when the rule is only needed once
- often easier for beginners to read
GroupBy() vs DistinctBy()
people.GroupBy(p => p.Id).Select(g => g.First())
- works in older LINQ code
- lets you choose which item to keep from each group
people.DistinctBy(p => p.Id)
- shorter and clearer
- ideal when you just want the first item per key
Cheat Sheet
// Distinct objects by one property (.NET 6+)
var result = people.DistinctBy(p => p.Id).ToList();
// Distinct objects by multiple properties (.NET 6+)
var result2 = people.DistinctBy(p => new { p.Id, p.Name }).ToList();
// Distinct objects by property (works in older versions)
var result3 = people.GroupBy(p => p.Id)
.Select(g => g.First())
.ToList();
// Distinct property values only
var ids = people.Select(p => p.Id).Distinct().ToList();
// Distinct with custom comparer
var result4 = people.Distinct(new PersonIdComparer()).ToList();
Rules to remember
Distinct()uses equality, not arbitrary property comparison- Custom classes usually need a comparer or overridden equality methods
GroupBy(...).Select(g => g.First())is a common fallbackDistinctBy()is the cleanest option in modern .NET- Use anonymous objects or tuples for multiple properties
Good key choices
- stable IDs
- unique business keys
- normalized text values when needed
Be careful with
- mutable equality fields
- missing
GetHashCode() - null handling in comparers
- assuming
Distinct()inspects object properties automatically
FAQ
Can I use Distinct() directly on a property in C#?
Not directly on the object unless equality is defined for that object. For property-based uniqueness, use DistinctBy(), GroupBy(), or a custom comparer.
What is the easiest way to get distinct objects by one property?
If you are using .NET 6 or later, use:
people.DistinctBy(p => p.Id)
Otherwise, use:
people.GroupBy(p => p.Id).Select(g => g.First())
How do I get distinct objects by multiple properties?
Use a composite key:
people.DistinctBy(p => new { p.Id, p.Name })
or:
people.GroupBy(p => new { p.Id, p.Name }).Select(g => g.First())
Why does Distinct() not remove duplicate objects with the same values?
Because custom class instances are usually compared by reference unless you define value-based equality.
Should I override Equals() and GetHashCode() in my model class?
Only if that equality rule truly represents the object's general identity everywhere in your application. If the rule is query-specific, prefer DistinctBy() or a custom comparer.
Which duplicate does DistinctBy() keep?
It keeps the first item encountered for each key.
Is GroupBy() slower than DistinctBy()?
For most application code, both are fine. Choose the most readable option first unless performance measurements show a real issue.
Mini Project
Description
Build a small C# console example that cleans a list of duplicate people records. This demonstrates how to keep only one Person per Id, which is a common task when importing data, reading API responses, or merging collections.
Goal
Create a program that removes duplicate Person objects based on Id and prints the unique results.
Requirements
- Define a
Personclass withIdandNameproperties. - Create a list containing duplicate
Personrecords. - Produce a unique list based on
Id. - Print the final distinct people to the console.
Keep learning
Related questions
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
Learn the differences between AddTransient, AddScoped, and AddSingleton in ASP.NET Core DI with examples and practical usage.
Best Way to Repeat a Character in C#: Building Repeated Strings Efficiently
Learn the best way to repeat a character in C#, compare StringBuilder, string concatenation, and simpler built-in options.
C# Access Modifiers and static: public, private, protected, and Defaults
Learn how C# public, private, protected, and default access control visibility, and how static differs from instance members.