Updated on
To sort a list of objects by one of their properties in C#, we either build a sorted copy with LINQ’s OrderBy() or reorder the list we already have with List<T>.Sort().
OrderBy() names the property inline, chains into ThenBy() for a second key, and leaves the original list alone. List<T>.Sort() rearranges the list in place and needs to be told how to compare two objects, which we supply as an IComparable<T> implementation on the type, an IComparer<T> class, or a Comparison<T> delegate.
The sections below show each approach, along with the two differences that decide between them: whether the original list survives the sort, and whether elements with equal keys keep their order.
Creating a Demo Model
Let’s begin by creating a Book class we are going to work with:
public class Book
{
public required string Title { get; set; }
public required string Author { get; set; }
public int Pages { get; set; }
}
We can sort List<Book> by any property in the Book class.
To verify the functionality, let’s create some sample data in an MSTest test class, in a test project that references our project. The class keeps each list we build below in a static List<Book> field (_books, _booksSortedTitle and so on), fills them all from its constructor, and runs each check as a [TestMethod]. Because those tests share the static fields, we mark the class [DoNotParallelize]:
private static readonly Book gatsby = new()
{
Title = "The Great Gatsby",
Author = "F. Scott Fitzgerald",
Pages = 279
};
private static readonly Book lotr = new()
{
Title = "The Lord Of The Rings",
Author = "J.R.R. Tolkien",
Pages = 1216
};
private static readonly Book pride = new()
{
Title = "Pride & Prejudice",
Author = "Jane Austen",
Pages = 329
};
private static readonly Book emma = new()
{
Title = "Emma",
Author = "Jane Austen",
Pages = 1036
};
Let’s create a CreateBookData() method to keep these Book objects in an unsorted List<Book>:
private static void CreateBookData()
{
_books = new List<Book>
{
lotr,
emma,
gatsby,
pride
};
}
And, let’s create another CreateBookDataSortedByTitle() method to contain a sorted List<Book> by the Title property:
private static void CreateBookDataSortedByTitle()
{
_booksSortedTitle = new List<Book>
{
emma,
pride,
gatsby,
lotr
};
}
What Is the Difference Between List.Sort() and OrderBy()?
List<T>.Sort() reorders the list we already have. OrderBy() leaves that list alone and produces a sorted sequence, which we turn into a second list with ToList().
Sorting in place needs no extra collection, but the original order is gone afterwards. OrderBy() pays for a second list and keeps the first one intact.
The two also treat equal keys differently. OrderBy() is a stable sort, so elements with equal keys come out in the order they went in. List<T>.Sort() is unstable, and its documentation warns that “if two elements are equal, their order might not be preserved”.
So two books with the same page count may change places when we call Sort(), while OrderBy() keeps them in their original order. If we sort by one property and need ties to keep their existing order, OrderBy() is the safe choice.
Finally, OrderBy() is deferred: nothing is sorted until we enumerate the result or call ToList().
The List<T>.Sort() documentation states: “This implementation performs an unstable sort; that is, if two elements are equal, their order might not be preserved.” The OrderBy() documentation says the opposite about that method: “This method performs a stable sort”.
The figure sorts our four books by title both ways:

The sortedList on the left is a new object, and _books still holds the books in their original order. On the right, _books itself now holds them sorted.
Using the OrderBy() Method in LINQ
A common way of sorting lists is by using the OrderBy() LINQ method to create a new, sorted copy of the original list. This returns a sequence sorted by a key, which ToList() turns into a new list. This key should be a property of the object.
Let’s look at a scenario to sort the List<Book> by the Title property by creating a method in a Sort class:
public List<Book> SortByTitleUsingLinq(List<Book> originalList)
{
return originalList.OrderBy(x => x.Title).ToList();
}
This method takes a List<Book> as argument and returns a new List<Book> that’s sorted according to the Title property.
Now, on executing the method, we get a List<Book> sorted by Title:
var sort = new Sort(); var sortedList = sort.SortByTitleUsingLinq(_books); CollectionAssert.AreEqual(_booksSortedTitle, sortedList);
As we can see now the presorted list is equal to our sorted list.
Sorting by Multiple Properties
So, we have looked at sorting a list of objects by using one property. However, we can also sort the list by using multiple properties.
We often do something similar in SQL with an ORDER BY clause:
ORDER BY Author, Pages
Let’s say we want the same list to be sorted by Author and Pages properties.
We can do so by using ThenBy() method in addition to OrderBy():
public List<Book> SortByAuthorAndPagesUsingLinq(List<Book> originalList)
{
return originalList.OrderBy(x => x.Author).ThenBy(x => x.Pages).ToList();
}
This method returns a new List<Book> sorted, first by the Author property and then by the Pages property.
Let’s create another sample sorted list:
private static void CreateBookDataSortedByAuthorPages()
{
_booksSortedAuthorPages = new List<Book>
{
gatsby,
lotr,
pride,
emma
};
}
This list contains Book objects sorted by multiple properties. We can verify this by executing the method:
var sort = new Sort(); var sortedList = sort.SortByAuthorAndPagesUsingLinq(_books); CollectionAssert.AreEqual(_booksSortedAuthorPages, sortedList);
Both Jane Austen books share the first key, so ThenBy() decides their order: Pride & Prejudice (329 pages) comes before Emma (1,036 pages). Our unsorted list holds Emma ahead of Pride & Prejudice, so this check fails if ThenBy() does nothing.
Sorting in Descending Order
OrderByDescending() takes the same key selector as OrderBy() and sorts from the highest value to the lowest, and ThenByDescending() does the same for a second key:
public List<Book> SortByPagesDescendingUsingLinq(List<Book> originalList)
{
return originalList.OrderByDescending(x => x.Pages).ToList();
}
List<T>.Sort() has no descending overload. To sort in place from the highest page count to the lowest, we swap the operands in the comparison, and check the result against a _booksSortedPagesDescending list that holds lotr, emma, pride and gatsby:
_books.Sort((x, y) => y.Pages.CompareTo(x.Pages)); CollectionAssert.AreEqual(_booksSortedPagesDescending, _books);
Here y comes first and is compared to x. The same swap turns any ascending comparison in this article into a descending one, whether it lives in a lambda, a Comparison<T> method or an IComparer<T> class.
To sort a sequence by its elements rather than by a property, LINQ has Order() and OrderDescending(), which we cover along with the rest of LINQ’s sorting operators.
Using the IComparable<T> Interface
The IComparable<T> interface allows us to sort a list of complex types without using LINQ.
To do that, we need to implement the IComparable<T> interface in the Book class and implement the CompareTo() method. This CompareTo() method returns an integer whose value can be:
- Less than zero – meaning the compared instance comes after the current instance in the sort order
- Zero – meaning both the compared and the current instances have the same position in the sort order
- Greater than zero – meaning the compared instance comes before the current instance in the sort order
Let’s modify the Book class to implement the IComparable<T> interface:
public class Book : IComparable<Book>
{
public required string Title { get; set; }
public required string Author { get; set; }
public int Pages { get; set; }
public int CompareTo(Book? other)
{
if (other == null)
return 1;
if (this.Pages > other.Pages)
return 1;
else if (this.Pages < other.Pages)
return -1;
else
return 0;
}
}
Now, calling Sort() without arguments on a List<Book> uses this CompareTo() method to order the objects according to the Pages property.
Let’s create another sample List<Book> sorted by Pages to verify this functionality:
private static void CreateBookDataSortedByPages()
{
_booksSortedPages = new List<Book>
{
gatsby,
pride,
emma,
lotr
};
}
When we invoke the Sort() method on the original unsorted List<Book>, it is sorted without needing to create a separate copy of the original list:
_books.Sort(); CollectionAssert.AreEqual(_booksSortedPages, _books);
Using the IComparer<T> Interface
We can also use IComparer<T> interface to have our comparison implementations and hence, sort the list according to our custom implementations.
The IComparer<T> and IComparable<T> interfaces work differently.
We use IComparable<T> for intrinsic comparison, i.e. comparing another instance to the current instance. IComparer<T>, on the other hand, provides us with a mechanism to compare two different objects.
Let’s implement the IComparer<T> interface to sort the List<Book> by Title:
public class SortBookByTitle : IComparer<Book>
{
public int Compare(Book? x, Book? y)
{
if (x == null)
return -1;
if (y == null)
return 1;
return x.Title.CompareTo(y.Title);
}
}
Now, we need to pass an instance of the SortBookByTitle class as an argument to the Sort() method:
_books.Sort(new SortBookByTitle()); CollectionAssert.AreEqual(_booksSortedTitle, _books);
IComparer<T> sorts the list in place and doesn’t create a new copy of the original list.
We go further into writing a reusable comparer class in a separate article.
Using the Comparison<T> Delegate
An overload of the Sort() method expects Comparison<T> delegate as an argument. We can use this in addition to the other approaches to sort the list.
Let’s start by creating a method in our Sort class that matches the signature of the Comparison delegate:
public static int CompareBooks(Book x, Book y)
{
return x.Title.CompareTo(y.Title);
}
We can then create an instance of the Comparison delegate and pass CompareBooks as an argument. Hence, the delegate points to our new method.
If we pass this delegate instance as an argument in the Sort() method, we can sort the List<Book> by Title:
var bookComparer = new Comparison<Book>(Sort.CompareBooks); _books.Sort(bookComparer); CollectionAssert.AreEqual(_booksSortedTitle, _books);
Using lambda expressions, we can further simplify this approach:
_books.Sort((x, y) => x.Title.CompareTo(y.Title)); CollectionAssert.AreEqual(_booksSortedTitle, _books);
So, with lambda expressions, we don’t need to declare a new comparison method or explicitly create the instance of the Comparison delegate any longer.
How Do the Sorting Approaches Compare in Performance?
LINQ and List<T>.Sort() do the work differently. OrderBy() extracts the key from each object once and sorts an index array by those keys. The three in-place approaches hand a comparison to the introspective sort inside Array.Sort, which calls it for every pair of elements it compares.
The type of the key matters more than the approach. Comparing two int page counts is a single integer comparison. Comparing two string titles with CompareTo() is a culture-aware comparison and costs far more.
Allocation is the other difference. OrderBy().ToList() allocates a copy of the elements, a key array, an index array and the new list, while the in-place approaches allocate at most a comparer object and a delegate.
So a numeric key beats a string key by a wide margin. On the same key, OrderBy() finishes ahead of the in-place sorts.
At a thousand objects, none of this is visible to a user. It matters with hundreds of thousands of objects, or inside a loop.
First, let’s create a static DataGenerator class, in a file that starts with using System.Text;, and write a method in it to generate a random number that we will use as the value for the Pages property:
public static int GenerateNumber(int min, int max)
{
return Random.Shared.Next(min, max);
}
The method GenerateNumber() takes two integer values as input. Using the Random class, we generate a number between these integers.
Now, let’s write a method to generate a random string that we will use as the value for Title and Author properties:
public static string GenerateString(int size)
{
var builder = new StringBuilder(size);
char start = 'a';
for (int i = 0; i < size; i++)
{
var text = (char)Random.Shared.Next(start, start + 26);
builder.Append(text);
}
return builder.ToString();
}
To keep it simple, we will generate lower-case text of a specified size.
Next, let’s add the BenchmarkDotNet package and create a SortBenchmarks class, in a file that starts with using BenchmarkDotNet.Attributes;, with a method that returns a list of Book objects with randomly assigned property values:
public static List<Book> GenerateBooks()
{
var books = new List<Book>();
for (int i = 0; i < 1000; i++)
{
var book = new Book
{
Title = DataGenerator.GenerateString(10),
Author = DataGenerator.GenerateString(15),
Pages = DataGenerator.GenerateNumber(100, 1000)
};
books.Add(book);
}
return books;
}
We will simulate the sorting scenario on 1000 books. These Book objects contain the property values generated from GenerateNumber() and GenerateString() methods.
Three of the approaches sort the list in place. If every benchmark invocation received the same list, each invocation after the first would sort a list that is already sorted. So every invocation gets a fresh, unsorted copy of the generated books, which we keep in two List<Book> fields of the class, _originalList and _books:
[GlobalSetup]
public void GlobalSetup()
{
_originalList = GenerateBooks();
}
[IterationSetup]
public void IterationSetup()
{
_books = new List<Book>(_originalList);
}
With [IterationSetup] in place, BenchmarkDotNet runs one invocation per iteration, which is the InvocationCount=1 line in the results.
Book.CompareTo() compares Pages, so the benchmark that calls Sort() without arguments sorts by page count. We name it SortByPagesIComparable and add a LINQ sort by Pages, so each in-place approach has a LINQ sort on the same key to compare with. Each row of the results is a [Benchmark] method that runs one sort on _books, and the class carries [MemoryDiagnoser] for the Allocated column. Program.cs runs it with BenchmarkRunner.Run<SortBenchmarks>(), after using BenchmarkDotNet.Running;, in a Release build.
Let’s assess the performance results of the sorting methods:
BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6466/22H2/2022Update) AMD Ryzen 5 3600 3.60GHz, 1 CPU, 12 logical and 6 physical cores .NET SDK 10.0.302 [Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3 Job-CNUJVU : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3 InvocationCount=1 UnrollFactor=1 | Method | Mean | Error | StdDev | Allocated | |------------------------------- |---------:|---------:|---------:|----------:| | SortByTitleUsingLinq | 547.8 us | 9.17 us | 7.16 us | 28368 B | | SortByPagesUsingLinq | 108.4 us | 0.98 us | 0.87 us | 24336 B | | SortByPagesDescendingUsingLinq | 111.5 us | 1.98 us | 1.54 us | 24336 B | | SortByPagesIComparable | 142.6 us | 1.94 us | 1.62 us | - | | SortByTitleIComparer | 811.5 us | 13.33 us | 13.10 us | 88 B | | SortByTitleComparisonDelegate | 782.1 us | 13.17 us | 12.32 us | 64 B |
The key decides most of the cost. Sorting by the int page count with LINQ takes 108.4 us, while the same LINQ call by the string title takes 547.8 us, about five times longer.
On the same key, the LINQ copy takes 24 to 33 percent less time than sorting in place. By Pages, OrderBy() takes 108.4 us against 142.6 us for the IComparable<T> sort. By Title, it takes 547.8 us against 811.5 us for IComparer<T> and 782.1 us for the Comparison<T> delegate, and those two in-place forms land within 4 percent of each other. Sorting in descending order costs about the same as ascending.
Memory goes the other way. Each LINQ sort allocates about 24 to 28 KB for 1,000 books, while the in-place sorts allocate 88 bytes at most, and nothing at all for IComparable<T>.
Which Approach Should We Use to Sort a List by a Property?
Start with OrderBy(). It names the property inline, it chains into ThenBy() for a second key, it is stable, and it leaves the original list alone.
Reach for List<T>.Sort() when we want to avoid allocating a second list, we own the list, and nothing else depends on its original order.
With Sort(), a lambda passed straight in is the shortest way to express the comparison and the right default for a one-off sort.
A Comparison<T> variable is the same lambda with a name, which is worth having when several call sites share it.
IComparer<T> suits reuse beyond sorting. It is a small class, and other framework types accept one: List<T>.BinarySearch(), SortedSet<T> and SortedDictionary<TKey, TValue> all take a comparer.
IComparable<T> is the one to be careful with, because implementing it declares a single natural order for that type everywhere in the codebase. Implement it when one property genuinely is the type’s identity for sorting, and use a comparer for every other order.
The table puts the six forms side by side:
| Approach | Sorts by | The original list | Stable | Reach for it when |
|---|---|---|---|---|
list.OrderBy(x => x.Title) | any property, ascending | untouched; a new list after ToList() | Yes | the default, and whenever the original order still matters |
list.OrderByDescending(x => x.Pages) | any property, descending | untouched | Yes | the same, reversed |
list.OrderBy(...).ThenBy(...) | two or more properties in order | untouched | Yes | ties on the first key need a second key |
list.Sort() | the type's own IComparable<T> order | reordered in place | No | one property really is the type's natural order |
list.Sort(new SortBookByTitle()) | whatever the IComparer<T> says | reordered in place | No | the same comparison is reused in more than one place |
list.Sort((x, y) => x.Title.CompareTo(y.Title)) | whatever the lambda says | reordered in place | No | a one-off in-place sort |
Only the three LINQ rows are stable and keep the original list untouched. For a closer look at how IComparable, IComparer and the Comparison delegate differ, we have a separate article.
Conclusion
In this article, we learned about sorting a list based on the property of an object. We saw different ways of doing so, using the LINQ OrderBy() method, implementation of IComparable<T> and IComparer<T> interfaces, and using the Comparison delegate.
OrderBy() works on more than lists, and the same call handles sorting a dictionary by its values. When the question is which elements a list holds rather than their order, Remove Duplicates From a List in C# covers it.
Tested with .NET 10.0.10 and BenchmarkDotNet 0.15.8.
