Use LINQ: list = list.Distinct().ToList();. Distinct() returns each value once, keeping the first occurrence, and ToList() turns the result back into a List<T>. To dedupe objects by a property, use list.DistinctBy(p => p.Id).ToList() (.NET 6+). Plain Distinct() on your own classes removes nothing unless they are records or override Equals and GetHashCode.
List<T> has no RemoveDuplicates method. The work is done by LINQ's Distinct and DistinctBy, which build a new sequence, or by a HashSet<T>, which can also clean the list in place. The harder part is deciding what "duplicate" means. For numbers and strings it is obvious. For your own objects, C# compares references unless you tell it otherwise, which is why Distinct() so often seems to do nothing. The examples cover both, plus how to pass Distinct a rule of your own. Each example runs on this page: hit Run, then edit the code and run it again.
1Distinct().ToList()Recommended
Distinct() works on any IEnumerable<T>: lists, arrays, query results. It is lazy, so nothing happens until you enumerate it, and ToList() (or ToArray()) does that and gives you a new collection. The original list is never changed; assign the result back if you want to replace it. Strings compare case-sensitively by default, and the overload that takes an IEqualityComparer<T> such as StringComparer.OrdinalIgnoreCase changes that.
Output
Prints 3, 1, 2, 5: each value at the position where it first appeared. The counts are 7 before the reassignment and 4 after it. Chaining Order() gives 70, 88, 95. The tags print all five, csharp, dotnet, CSharp, linq, DotNet, until the comparer makes it csharp, dotnet, linq, keeping the first spelling of each. The array line prints the cat and hat bat. On order: the documentation calls the result of Distinct an unordered sequence, but LINQ to Objects yields each item the first time it meets it, so the original order of first occurrences is what you get in practice, as the output shows. If that order is part of your program's contract, the HashSet loop in the next section guarantees it by construction.
2HashSet<T>, and removing duplicates in place
A HashSet<T> stores each value once, and Distinct() uses one internally. Use a set directly when you want the unique values for lookups later, or when the list itself has to change, for example because other code holds a reference to it. The key detail is that HashSet<T>.Add returns false for a value that is already there. Inside List<T>.RemoveAll, that removes every repeat in one pass and keeps the first of each.
Output
The set has 4 values and contains 5 (True). Adding 5 again returns False, adding 8 returns True. RemoveAll prints removed 3: 3, 1, 2, 5, the same result as Distinct() but in the original list. It calls the predicate once per element, front to back. The loop version prints Ana, Ben, Cleo and is the one to copy when you need to do more than skip a repeat, such as logging it. SortedSet<T> prints 1, 4, 9. Both duplicate checks print True. The example never prints a HashSet itself: its enumeration order is not defined, so don't count on it matching the list. ToHashSet() needs .NET Framework 4.7.2 or .NET Core 2.0; on older versions use new HashSet<T>(list).
3Distinct by a property with DistinctBy
Most real lists hold objects, and "duplicate" means "same email" or "same Id", not "every field equal". DistinctBy(keySelector), added in .NET 6, keeps the first item for each key. For a key made of several properties, return a tuple: tuples compare by value. A second argument sets the comparer for the key. Before .NET 6, including .NET Framework, there is no DistinctBy; group by the key and take the first item of each group instead.
Output
The first loop prints three people: Person { First = Ana, Last = Lopez, Email = [email protected] }, then Ben and Cleo. The second Ana is dropped because an earlier item had the same key. The tuple key gives 3, since only the two Ana Lopez rows match on both names. The email count is 3 too: [email protected] and [email protected] are the same key once case is ignored. The GroupBy version prints Ana, Ben, the first person with each last name. It builds every group before returning anything, so on large lists DistinctBy is the cheaper of the two. To keep the last item per key instead, see section 6.
4Why Distinct() ignores duplicate objects
Distinct() with no arguments uses EqualityComparer<T>.Default. For a class, that means Equals and GetHashCode inherited from object, which compare references: two objects with identical data are still two different objects. Records (C# 9) and value tuples generate value equality for you. For a class you can't turn into a record, implement IEquatable<T> and override GetHashCode so that equal objects produce equal hash codes.
Output
The class list prints 3: nothing was removed. The same object added twice prints 1, which shows what reference equality actually checks. The record and tuple lists both print 2, and the Sku class prints A-1, B-2, because its Equals ignores case. Overriding Equals alone is not enough. Distinct compares hash codes first and only calls Equals when they match, so a class that overrides Equals but not GetHashCode still keeps all its duplicates (the FAQ has the run). Custom equality applies everywhere the type is compared, in Contains, HashSet and Dictionary keys. When you only want different equality for one dedupe, pass a key or a comparer instead (sections 3 and 5).
5Distinct with a lambda: a custom comparer
Distinct has no overload that takes a lambda. It takes an IEqualityComparer<T>, an object with an Equals and a GetHashCode method. If the rule is "same key", DistinctBy from section 3 is simpler. A comparer is worth writing when you want to reuse the rule, because the same object works with HashSet, Dictionary, Union, Except and Contains. Since .NET 8, EqualityComparer<T>.Create builds one from lambdas; on older versions, write a small class.
Output
The name comparer prints Mug, Lamp, Pan: "mug" matches "Mug" ignoring case. The category comparer prints Mug, Lamp, one product per category. The HashSet built with it holds 2 items, and Contains prints True for a rug that has never been in the list, because only the category is compared. The last line is NotSupportedException: Specified method is not supported.: Create with only the equality lambda gives a comparer whose GetHashCode throws, and Distinct needs it. The hash code must agree with Equals: hash the same fields, with the same case rules. An old Stack Overflow workaround returns 0 for every hash code. That gives correct results, but every item lands in one bucket and gets compared with every kept item, so a large list slows down badly.
6Find the duplicates, or keep the last one
Sometimes the duplicates are the point: report them, drop every value that repeats, or keep the newest row per key rather than the first. GroupBy handles all three, since each group holds every item with the same key, in source order. CountBy (.NET 9) counts per key without building the groups.
Output
The groups print [email protected] x3, [email protected] x2, and CountBy prints [email protected]: 3 and [email protected]: 2, sorted by key because its order isn't documented. The only value that appears once is [email protected]. g.Last() keeps the latest status per order, Order { Id = 1, Status = shipped } and Order { Id = 2, Status = pending }, listed in the order each Id first appeared. The reverse trick keeps the same items but orders them by where each Id appeared last: Order { Id = 2, Status = pending } | Order { Id = 1, Status = shipped }. The AsEnumerable() matters: on a List<T>, Reverse() is the list's own method, which reverses in place and returns nothing. More on grouping in group by with LINQ.
7Which should you use?
| Method | Order of the result | Duplicate means | Best for |
|---|---|---|---|
| list.Distinct().ToList() | First occurrences | Default equality | Numbers, strings, records: the default |
| list.DistinctBy(x => x.Key) | First occurrences | Same key (one or more properties) | Objects by Id, email, name (.NET 6+) |
| list.RemoveAll(x => !seen.Add(x)) | First occurrences, in place | Default equality or a comparer | Changing the list itself |
| list.ToHashSet() | Not defined | Default equality or a comparer | Unique values for fast lookups |
| new SortedSet<T>(list) | Sorted by value | Compares equal when sorting | Unique and sorted in one step |
| list.Distinct(comparer) | First occurrences | Your IEqualityComparer<T> | A reusable rule, e.g. ignore case |
| GroupBy(x => x.Key).Select(g => g.First()) | First occurrences | Same key | Before .NET 6, or g.Last() to keep the last |
Frequently asked questions
Does Distinct() keep the original order of the list?
In practice, yes. LINQ to Objects yields each item the first time it sees it, so [3, 1, 3, 2, 1, 5, 2] becomes 3, 1, 2, 5. The documentation describes the result as an unordered sequence, though, so if the order is a requirement, use a HashSet<T> with a loop or list.RemoveAll(x => !seen.Add(x)), which keep the first occurrence by construction. A HashSet<T> on its own does not keep any order.
How do I remove duplicates by a property, like distinct by Id?
On .NET 6 and later, use list.DistinctBy(p => p.Id).ToList(); it keeps the first item for each Id. For several properties, return a tuple: DistinctBy(p => (p.First, p.Last)). On older versions, including .NET Framework, use list.GroupBy(p => p.Id).Select(g => g.First()).ToList(), which gives the same items.
How do I use Distinct with a lambda expression?
Distinct itself only takes an IEqualityComparer<T>. If two items are duplicates when one property matches, DistinctBy(x => x.Property) is the lambda version. For any other rule, .NET 8 added EqualityComparer<T>.Create((a, b) => ..., x => ...), which takes an equality lambda and a hash-code lambda. Pass both: without the hash-code lambda, Distinct throws NotSupportedException: Specified method is not supported. On older versions, write a small class that implements IEqualityComparer<T>.
Why doesn't Distinct() remove duplicate objects from my list?
Because your class compares by reference: two new objects with the same data are not equal to the default comparer. Make the type a record, which compares by value, or implement IEquatable<T> and override GetHashCode as well as Equals. Overriding only Equals does not help: in a run here, a class whose Equals returned True for two items still came out of Distinct() with all 3 items, because the hash codes differed. Or skip equality and use DistinctBy on a key.
How do I remove duplicates from a List<T> without creating a new list?
Use RemoveAll with a HashSet<T>: var seen = new HashSet<int>(); list.RemoveAll(x => !seen.Add(x));. Add returns false for a value already in the set, so every repeat is removed and the first of each stays. RemoveAll returns the number removed, 3 for [3, 1, 3, 2, 1, 5, 2]. Pass a comparer to the set, such as StringComparer.OrdinalIgnoreCase, to change what counts as a repeat.
How do I check whether a list contains duplicates?
Compare the counts: list.Count != list.Distinct().Count() or list.Count != list.ToHashSet().Count is True when something repeats. To see which values repeat, group them: list.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key). On .NET 9 and later, list.CountBy(x => x) gives each value with its count.
How do I remove duplicate strings ignoring case?
Pass a string comparer: list.Distinct(StringComparer.OrdinalIgnoreCase).ToList(). The first spelling wins, so ["Apple", "apple", "APPLE", "pear"] becomes Apple, pear. The same comparer works with DistinctBy(p => p.Email, StringComparer.OrdinalIgnoreCase) and new HashSet<string>(list, StringComparer.OrdinalIgnoreCase). Use OrdinalIgnoreCase for identifiers such as emails and tags; CurrentCultureIgnoreCase is for text shown to people.