In this article, we are going to look at different ways to sort a List by a property in the object.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in.

Creating a Demo Model

Let’s begin by creating a Book class we are going to work with:

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
public class Book
{
    public string Title { get; set; }
    public 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:

private static readonly Book gatsby = new Book
{
    Title = "The Great Gatsby",
    Author = "F. Scott Fitzgerald",
    Pages = 279
};

private static readonly Book lotr = new Book
{
    Title = "The Lord Of The Rings",
    Author = "J.R.R. Tolkien",
    Pages = 1216
};

private static readonly Book pride = new Book
{
    Title = "Pride & Prejudice",
    Author = "Jane Austen",
    Pages = 329
};

private static readonly Book emma = new Book
{
    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>
    {
        gatsby,
        lotr,
        pride,
        emma
    };
}

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
    };
}

Using the OrderBy() Method in LINQ

The most common way of sorting lists is by using the OrderBy() LINQ method to create a new, sorted copy of the original list. This creates a new list with the elements sorted by using a key. 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:

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);

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 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 interface:

public class Book : IComparable<Book>
{
    public string Title { get; set; }
    public 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, the Book class can invoke the Sort() method provided in the List<T> class to order the objects according to 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 to sort the List<Book> by Title:

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);

IComaparer<T> sorts the list in place and doesn’t create a new copy of the original list.

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 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 invoke the instance of the Comparison delegate any longer.

Performance of Different Sorting Methods

Let’s see how these different ways of sorting a list of objects by a property perform against each other.

First, let’s write a method to generate a random number that we will use as the value for the PageNumber property:

public static int GenerateNumber(int min, int max)
{
    return _random.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.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 create a collection that holds a list of Book objects with randomly assigned property values:

public IEnumerable<List<Book>> GenerateBooks()
{
    var data = new List<List<Book>>();
    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);
    }

    data.Add(books);

    return data;
}

We will simulate the sorting scenario on 1000 books. These Book objects contain the property values generated from GenerateNumber() and GenerateString() methods.

Let’s assess the performance results of the sorting methods:

|                        Method |      Mean |    Error |   StdDev |
|------------------------------ |----------:|---------:|---------:|
|          SortByTitleUsingLinq | 371.55 us | 3.830 us | 3.395 us |
|        SortByTitleIComparable |  22.79 us | 0.108 us | 0.101 us |
|          SortByTitleIComparer | 246.08 us | 0.533 us | 0.445 us |
| SortByTitleComparisonDelegate | 249.68 us | 2.495 us | 2.212 us |

We can see that the intrinsic sorting using IComparable implementation is the fastest way to sort a list of objects by far, taking 22.79 us. Whereas, using LINQ takes the longest with 371.55 us.

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 and IComparer interfaces, and using the Comparison delegate.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!