Starting from .NET 6, a new DistinctBy LINQ operator is available:
public static IEnumerable<TSource> DistinctBy<TSource,TKey> ( this IEnumerable<TSource> source, Func<TSource,TKey> keySelector);
Returns distinct elements from a sequence according to a specified key selector function.
Usage example:
List<Item> distinctList = listWithDuplicates .DistinctBy(i => i.Id) .ToList();
There is also an overload that has an IEqualityComparer<TKey> parameter.
Update in-place: In case creating a new List<T> is not desirable, here is a RemoveDuplicates extension method for the List<T> class:
/// <summary> /// Removes all the elements that are duplicates of previous elements, /// according to a specified key selector function. /// </summary> /// <returns> /// The number of elements removed. /// </returns> public static int RemoveDuplicates<TSource, TKey>( this List<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> keyComparer = null) { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(keySelector); HashSet<TKey> hashSet = new(keyComparer); return source.RemoveAll(item => !hashSet.Add(keySelector(item))); }
This method is efficient (O(n)) but also a bit dangerous, because it is based on the potentially corruptive List<T>.RemoveAll method¹. In case the keySelector lambda succeeds for some elements and then fails for another element, the partially modified List<T> will neither be restored to its initial state, nor it will be in a state recognizable as the result of successful individual Removes. Instead it will transition to a corrupted state that includes duplicate occurrences of existing elements. So in case the keySelector lambda is not fail-proof, the RemoveDuplicates method should be invoked in a try block that has a catch block where the potentially corrupted list is discarded.
Alternatively you could substitute the dangerous built-in RemoveAll with a safe custom implementation, that offers predictable behavior.
¹ For all .NET versions and platforms, including the latest .NET 7. I have submitted a proposal on GitHub to document the corruptive behavior of the List<T>.RemoveAll method, and the feedback that I received was that neither the behavior should be documented, nor the implementation should be fixed.