For a sorted copy, use LINQ: people.OrderBy(p => p.Name).ToList(), with OrderByDescending for descending order and .ThenBy(p => p.Age) for a tie-breaker. To sort the list in place, pass List.Sort a comparison: people.Sort((a, b) => a.Name.CompareTo(b.Name)). OrderBy is stable; List.Sort is not.
A List<Person> doesn't know how one Person compares to another, so you tell it which property to sort by. C# gives you two ways. LINQ's OrderBy takes a key selector lambda and returns a new, sorted sequence. List<T>.Sort takes a comparison lambda and reorders the list itself. This page covers both, descending order and multiple keys, string comparison and null values, IComparable<T> for a type's built-in order, and sorting by a property whose name arrives as a string, which is what the "dynamic LINQ OrderBy" question asks. The examples use records (C# 9) and collection expressions (C# 12) and run on .NET 10. Each one runs on this page: hit Run, then edit the code and run it again.
1LINQ OrderBy and OrderByDescendingRecommended
OrderBy(p => p.Name) reads the key from each element and sorts by it. The key can be any comparable type: string, int, decimal, DateTime, DateOnly, an enum. It returns an IOrderedEnumerable<T> and leaves the source alone, so call .ToList() when you want a list, and assign it back when you want the variable to hold the sorted version.
Output
Prints Ada, Barbara, Grace, Linus and the first record, Person { Name = Ada, Age = 36, Height = 1.65, Joined = 11/02/2019 } (the date uses the current culture; this sandbox runs the invariant culture). The source still reads Grace, Ada, Linus, Barbara. Then Linus, Ada, Grace, Barbara youngest first, Linus, Barbara, Grace, Ada tallest first, the join dates from 2018-01-09 Barbara to 2023-06-30 Linus, and Linus once the sorted copy is assigned back. The key selector is any lambda, so OrderBy(p => p.Address.City) sorts by a nested property and OrderBy(p => p.Name.Length) by a computed one. In query syntax the same sort is from p in people orderby p.Name select p.
2Sort in place: List<T>.Sort with a lambda
List<T>.Sort reorders the list you call it on and allocates no new list. It takes a Comparison<T>, a lambda that receives two elements and returns a negative number, zero or a positive number. You rarely compute that number yourself: every built-in comparable type has a CompareTo method that returns it. For a comparer you pass around, Comparer<T>.Create wraps the same lambda in an IComparer<T>.
Output
Prints Linus, Ada, Grace, Barbara by age, Barbara, Grace, Ada, Linus with a and b swapped, Ada, Barbara, Grace, Linus by name and Linus, Ada, Grace, Barbara with the comparer. The last two lines are the catch: with Sort the two 30-year-olds came out as third, second, first, while OrderBy kept them as third, first, second. List<T>.Sort and Array.Sort use an unstable introsort, so elements with equal keys can change places. OrderBy is documented as stable. When the existing order of ties matters, use OrderBy or add a tie-breaker key.
3Multiple properties: ThenBy and ThenByDescending
ThenBy adds a tie-breaker: the second key is only looked at when the first keys are equal. Each key gets its own direction, so OrderByDescending followed by ThenBy sorts the first key high to low and the second low to high. The mistake to avoid is chaining a second OrderBy, which throws away the first sort. With List.Sort, compare the first key and fall through to the next one when the result is zero.
Output
The first block prints 90 alice, 90 dana, 70 bob, 70 carol. The second prints blue 90 dana, blue 70 bob, red 90 alice, red 70 carol. The double OrderBy prints red alice, blue bob, red carol, blue dana: sorted by name only, with the teams mixed up. The in-place version gives dana, bob, alice, carol, the same order as the second block. For several keys in a comparison lambda, a tuple also works: (a.Team, b.Score).CompareTo((b.Team, a.Score)) compares team ascending, then score descending, because b and a swap places in the second slot.
4String comparison and null values
Sorting by a string property uses the current culture by default, so the result can change from one machine to the next. Pass a StringComparer as the second argument of OrderBy to choose: Ordinal compares character codes, OrdinalIgnoreCase ignores case as well. null keys are fine: the default comparer puts them first. A null element is a different problem, because the key selector dereferences it.
Output
Prints Alice, bob, carol, Dave with the default comparer, Alice, Dave, bob, carol with Ordinal and Alice, bob, carol, Dave with OrdinalIgnoreCase. The null city and the null ranks sort first: null, Austin, Berlin, Oslo and null, null, 1, 3. Sorting on c.City is null first moves them to the end, Austin, Berlin, Oslo, null, because false sorts before true. The null element throws NullReferenceException: Object reference not set to an instance of an object., and with c?.Name it sorts first: null, Alice, bob, carol, Dave. For a List.Sort lambda, string.Compare(a, b, StringComparison.OrdinalIgnoreCase) does the same job as the comparer.
5IComparable<T>: a natural order, and the subtraction bug
If a type has one obvious order, such as version numbers or money, implement IComparable<T>. Then list.Sort() with no arguments, Order() and OrderDescending() (.NET 7+), Max() and SortedSet<T> all use it without being told how. Records don't implement it on their own. When you write a comparison by hand, return CompareTo's result; never subtract the two values.
Output
Prints 1.2, 1.10, 2.0, 2.0, 1.10, 1.2 and 2.0: numeric order, so 1.10 comes after 1.2, which sorting the strings would get wrong. Sorting Account, which has no natural order, compiles and then throws InvalidOperationException: Failed to compare two elements in the array. with the inner message At least one object must implement IComparable. The subtracting comparison sorts the accounts as C, A, B, putting two billion before minus two billion, because 2_000_000_000 - -2_000_000_000 wraps to -294967296 in int arithmetic. CompareTo gives the correct B, C, A.
6Sort by a property name in a string (dynamic OrderBy)
When the sort column comes from a grid header or a ?sortBy= parameter, you have the property name as a string. The safest answer is a dictionary that maps the allowed names to key selectors. Reflection reaches any property, but only for in-memory sequences. For an IQueryable<T>, such as an Entity Framework query, build an expression tree for x => x.Property and call Queryable.OrderBy with it, so the provider can translate the sort into SQL.
Output
Prints Grace, Linus, Ada by city, Grace, Ada, Linus by age descending through reflection, then Ada, Grace, Linus and Grace, Ada, Linus from the expression tree. An unknown name throws ArgumentException: Instance property 'Salary' is not defined for type 'Person' (Parameter 'propertyName'), so check the name against a list before you build the tree from user input. For a ThenBy on a dynamic key, pass ThenBy or ThenByDescending as the method name and the ordered query as the source. If you need string expressions such as "Name desc, Age", the System.Linq.Dynamic.Core NuGet package parses them for you.
7Which should you use?
| Approach | Result | Best for |
|---|---|---|
| list.OrderBy(p => p.Name).ToList() | New list, stable | The default: readable, keeps the source, keeps ties in order |
| OrderByDescending(...) / ThenBy(...) | New list, stable | Descending order and sorting by several properties |
| list.Sort((a, b) => a.Age.CompareTo(b.Age)) | In place, unstable | Large lists where you do not need the old order or a copy |
| OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase) | New list, stable | Strings that must sort the same on every machine |
| IComparable<T> + list.Sort() / Order() | Either | Types with one obvious order (versions, money) |
| Expression tree + Queryable.OrderBy | IQueryable | Sorting a database query by a column name from input |
| (a, b) => a.Age - b.Age | Avoid | Nothing: it overflows; use CompareTo |
Frequently asked questions
Why doesn't OrderBy sort my list?
Because it returns a new sequence instead of changing the list. A bare people.OrderBy(p => p.Name); compiles, and people keeps its old order. Assign the result (people = people.OrderBy(p => p.Name).ToList();) or use people.Sort(...) to sort in place. The returned sequence is also deferred: it sorts when you enumerate it, so an item added to the list after the OrderBy call but before the foreach is included in the result.
How do I sort a list of objects in descending order?
With LINQ, use OrderByDescending(p => p.Age), and ThenByDescending for later keys. With List.Sort, swap the arguments in the comparison: people.Sort((a, b) => b.Age.CompareTo(a.Age)). Sorting ascending and then calling people.Reverse() also works, but it reverses the order of equal elements too.
Is List.Sort stable in C#?
No. List<T>.Sort and Array.Sort use introsort, which is not stable: two elements that compare equal can end up in either order. LINQ OrderBy, OrderByDescending, ThenBy and ThenByDescending are documented as stable. If you need a stable in-place sort, sort with OrderBy and copy the result back, or add a tie-breaker key such as an id so that no two elements compare equal.
How do I sort an array of objects by a property?
Use Array.Sort(arr, (a, b) => a.Age.CompareTo(b.Age)) to sort the array in place, or arr.OrderBy(p => p.Age).ToArray() for a sorted copy. Array.Sort also takes an IComparer<T>, so a Comparer<Person>.Create(...) comparer works with both arrays and lists.
What does "Failed to compare two elements in the array" mean?
List<T>.Sort() or Array.Sort() was called without a comparer on a type that does not implement IComparable<T> or IComparable. The exception is InvalidOperationException, and its inner exception says At least one object must implement IComparable. Pass a comparison (list.Sort((a, b) => a.Name.CompareTo(b.Name))), use OrderBy with a key, or implement IComparable<T> on the type. The same exception wraps any exception thrown inside your own comparison, such as a NullReferenceException on a null element.
How do I sort by a property name passed as a string?
Map the allowed names to key selectors in a Dictionary<string, Func<T, object>> and call OrderBy(selector). For in-memory lists you can also use reflection: var prop = typeof(T).GetProperty(name); list.OrderBy(x => prop.GetValue(x)). For an IQueryable<T> (Entity Framework and other LINQ providers), build the key selector with Expression.Property and Expression.Lambda and call Queryable.OrderBy through Expression.Call, as in the dynamic OrderBy example, so the sort runs in the database. Never pass an unchecked name from user input straight into a query.