In this article, we are going to learn about Sorting and Filtering with LINQ. Language-Integrated Query, or LINQ for short, provides a robust set of language extensions that we can use to shape query results according to our current needs. LINQ not only allows for a type-safe way to retrieve data but also a means to sort or filter queried data in the same way.

If unfamiliar with LINQ, or need a refresher on its many advantages or disadvantages, please read our LINQ Basic Concepts article.

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

Let’s see how we can use LINQ to get exactly the data we want!

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

How Do We Sort in LINQ?

First off, what do we mean by sorting? Sorting allows us to order the elements of a data set by one or more specific attributes. Additionally, we can nest the sorting to order each element in the set by whatever priority we desire.

For our sorting examples, we will be using a list of custom Shape objects. So, let’s start with our Shape definition:

public abstract record Shape
{
    public int ShapeId { get; init; }
    public string? ShapeType { get; protected init; }
    public int ShapeWidth { get; init; }
    public int ShapeHeight { get; init; }
    public bool Is3D { get; protected init; }
}

We also need to implement some derived records.

Let’s start with the Rectangle:

public record Rectangle : Shape
{
    public Rectangle() => ShapeType = nameof(Rectangle);
}

Next, we need the Square record: 

public record Square : Shape
{
    public Square() => ShapeType = nameof(Square);
}

And finally the Cone record:

public record Cone : Shape
{
    public Cone()
    {
        ShapeType = nameof(Cone);
        Is3D = true;
    }
}

Now with our Shape objects defined, we can initialize our example List<Shape?>:

var shapeList = new List<Shape?>
{
    new Cone {ShapeId = 1, Height = 3, Width = 1},
    null,
    new Square {ShapeId = 0, Height = 2, Width = 2},
    new Rectangle {ShapeId = 2, Height = 4, Width = 6},
    null,
    new Square {ShapeId = 3, Height = 5, Width = 5}
};

LINQ Sorting Methods

To sort this data we will use one of LINQ’s seven sorting operators. With the exception of Reverse(), each of the sorting methods returns an IOrderedEnumerable<T>. One of the main advantages of using the LINQ sorting operators is the fact that they do not modify the underlying collection. We can think of them simply as returning a custom “view” over the data.

Order and OrderDescending

Both Order() and OrderDescending() where added in .NET 7.0 and provide a simple way to view our data in sorted order. Each of these methods orders the objects based upon the object’s default comparer. Order() sorts the collection in ascending order (smallest to largest), and OrderDescending() orders the collection in descending order (largest to smallest).

One thing to note is that if our object does not provide a default ordering comparer or a default order can’t be computed, then both Order() and OrderDescending() will throw an InvalidOperationException. For clarity’s sake, let’s look at an example:

public sealed class SampleNoOrderer
{
    public string Id { get; } = Guid.NewGuid().ToString();
}

public void GivenObjectWithoutDefaultOrderer_WhenCallOrder_ThenThrowsInvalidOperationException()
{
    var list = Enumerable.Range(0, 10).Select(i => new SampleNoOrderer()).ToList();

    var action = () => list.Order();

    action.Enumerating().Should().Throw<InvalidOperationException>();
}

With this in mind, let’s go ahead and update our Shape record to handle this situation. The first step is to add the IComparable<T> interface to our record definition:

public abstract record Shape : IComparable<Shape>

Second, we need to implement the CompareTo() method:

public int CompareTo(Shape? other)
{
    if (ReferenceEquals(this, other)) return 0;

    return other is null ? 1 : ShapeId.CompareTo(other.ShapeId);
}

Here we define our default sort order based upon ShapeId. For a more detailed look at the IComparable<T> interface, be sure to check out our article Techniques for Sorting a List in C#.

Now that we’ve implemented the interface, let’s see Order() in action:

shapeList.Order();

Which produces the output:

Null Shape
Null Shape
Square { ShapeId = 0, ShapeType = Square, Width = 2, Height = 2, Is3D = False }
Cone { ShapeId = 1, ShapeType = Cone, Width = 1, Height = 3, Is3D = True }
Rectangle { ShapeId = 2, ShapeType = Rectangle, Width = 6, Height = 4, Is3D = False }
Square { ShapeId = 3, ShapeType = Square, Width = 5, Height = 5, Is3D = False }

For completeness, let’s check out OrderDescending():

shapeList.OrderDescending();

And check the result again:

Square { ShapeId = 3, ShapeType = Square, Width = 5, Height = 5, Is3D = False }
Rectangle { ShapeId = 2, ShapeType = Rectangle, Width = 6, Height = 4, Is3D = False }
Cone { ShapeId = 1, ShapeType = Cone, Width = 1, Height = 3, Is3D = True }
Square { ShapeId = 0, ShapeType = Square, Width = 2, Height = 2, Is3D = False }
Null Shape
Null Shape

Here we see that the order of the returned IOrderedEnumerable<Shape?> is, as we would expect, the exact reverse of the one returned when calling Order() on our list.

OrderBy and OrderByDescending

OrderBy() and OrderByDescending() order data based upon a provided key selection lambda matching the signature Func<TKey, TSource>. The collection will be sorted according to the default ordering of the selected key (i.e. the property selected by the lambda). Just as with Order() and OrderDescending(), OrderBy() and OrderByDescending() will throw an exception if the selected key does not define a default ordering.

Let’s take a look at OrderBy() in action:

shapeList.OrderBy(s => s?.ShapeType);

Which yields:

Null Shape
Null Shape
Cone { ShapeId = 1, ShapeType = Cone, Width = 1, Height = 3, Is3D = True }
Rectangle { ShapeId = 2, ShapeType = Rectangle, Width = 6, Height = 4, Is3D = False }
Square { ShapeId = 0, ShapeType = Square, Width = 2, Height = 2, Is3D = False }
Square { ShapeId = 3, ShapeType = Square, Width = 5, Height = 5, Is3D = False }

To accomplish the same but in descending order, we can use OrderByDescending():

shapeList.OrderByDescending(s => s?.ShapeType);

After we do that, we can inspect the output:

Square { ShapeId = 0, ShapeType = Square, Width = 2, Height = 2, Is3D = False }
Square { ShapeId = 3, ShapeType = Square, Width = 5, Height = 5, Is3D = False }
Rectangle { ShapeId = 2, ShapeType = Rectangle, Width = 6, Height = 4, Is3D = False }
Cone { ShapeId = 1, ShapeType = Cone, Width = 1, Height = 3, Is3D = True }
Null Shape
Null Shape

ThenBy and ThenByDescending

We can use ThenBy() and ThenByDescending() on any IOrderedEnumerable<T> to produce a subsequent ordering of a collection.

As with both OrderBy() and OrderByDescending(), we pass a key selection lambda into ThenBy() or ThenByDescending() which is then used for ordering:

shapeList.OrderBy(s => s?.Is3D).ThenBy(s => s?.ShapeId);

Here we first order by whether or not the shape is 3D. We next order by the ShapeId:

Null Shape
Null Shape
Square { ShapeId = 0, ShapeType = Square, Width = 2, Height = 2, Is3D = False }
Rectangle { ShapeId = 2, ShapeType = Rectangle, Width = 6, Height = 4, Is3D = False }
Square { ShapeId = 3, ShapeType = Square, Width = 5, Height = 5, Is3D = False }
Cone { ShapeId = 1, ShapeType = Cone, Width = 1, Height = 3, Is3D = True }

We also have the option of reversing the sort ordering by calling ThenByDescending():

shapeList.OrderBy(sl => sl?.Is3D).ThenByDescending(sl => sl?.ShapeId);

Notice here how the Cone object remains at the end of the list, but the middle shapes are now listed in descending order by ShapeId:

Null Shape
Null Shape
Square { ShapeId = 3, ShapeType = Square, Width = 5, Height = 5, Is3D = False }
Rectangle { ShapeId = 2, ShapeType = Rectangle, Width = 6, Height = 4, Is3D = False }
Square { ShapeId = 0, ShapeType = Square, Width = 2, Height = 2, Is3D = False }
Cone { ShapeId = 1, ShapeType = Cone, Width = 1, Height = 3, Is3D = True }

Reverse

The Reverse() method does exactly what we would expect, returning an IEnumerable<T> of the collection in reverse order:

shapeList.AsEnumerable().Reverse();

One thing to note here in our sample code is the addition of the call to AsEnumerable(). Because shapeList is a List<T> which defines a Reverse() method, we need to “convert” our list to an IEnumerable in order to call Enumerable.Reverse().

Now let’s take a look at the output:

Square { ShapeId = 3, ShapeType = Square, Width = 5, Height = 5, Is3D = False }
Null Shape
Rectangle { ShapeId = 2, ShapeType = Rectangle, Width = 6, Height = 4, Is3D = False }
Square { ShapeId = 0, ShapeType = Square, Width = 2, Height = 2, Is3D = False }
Null Shape
Cone { ShapeId = 1, ShapeType = Cone, Width = 1, Height = 3, Is3D = True }

How Do We Filter in LINQ?

Filtering extracts elements from the dataset based on specified criteria. LINQ accomplishes filtering with two filtering methods. Both of which return an IEnumerable<T>.

Where

The Where() method is a powerful extension that allows us to filter the collection based on the criteria we specify:

shapeList.Where(s => s?.Height < 4);

Which produces the following output:

Cone { ShapeId = 1, ShapeType = Cone, Width = 1, Height = 3, Is3D = True }
Square { ShapeId = 0, ShapeType = Square, Width = 2, Height = 2, Is3D = False }

In this specific example, we filter out all Shape objects with Height >= 4. Or phrased in reverse, we return an IEnumerable<Shape?> containing only Shape objects with Height < 4.

The Where() method accepts a parameter of type Func<TSource, Boolean>. This means we can pass in either a lambda function or a method matching this signature. In our previous example, we passed in the lambda function: s => s?.Height < 4). Now, let’s take a look at calling it with a method.

First, let’s define our method:

public static bool FilterIs3DAndWidthLessThan3(Shape? s) => s is {Is3D: true, Width: < 3};

Second, let’s see it in action:

shapeList.Where(LinqFilteringMethods.FilterIs3DAndWidthLessThan3);

And the result:

Cone { ShapeId = 1, ShapeType = Cone, Width = 1, Height = 3, Is3D = True }

OfType

OfType<TResult>() is a unique extension method that filters the collection based on a specified type. It returns an IEnumerable<TResult>. All elements in the collection that are not of type TResult are removed from the result set.

Accordingly, this can be useful to ensure that all the elements in the collection are of the specified type:

shapeList.OfType<Square>();

Which produces:

Square { ShapeId = 0, ShapeType = Square, Width = 2, Height = 2, Is3D = False }
Square { ShapeId = 3, ShapeType = Square, Width = 5, Height = 5, Is3D = False }

Using Extension Methods For Sorting and Filtering With LINQ

Custom static methods – extension methods – allow us to customize or add functionality to how we sort or filter IEnumerable<TSource> objects. LINQ itself is a collection of extension methods defined in the Enumerable class.

To learn more about extension methods, please read our C# Intermediate – Static Members, Constants, and Extension Methods article.

Let’s create an extension method to filter out null items from an IEnumerable<T?>:

public static IEnumerable<T> FilterNotNull<T>(this IEnumerable<T?> source)
{
    using var enumerator = source.GetEnumerator();
    while (enumerator.MoveNext())
    {
        if (enumerator.Current is null) continue;
            
        yield return enumerator.Current;
    }
}

This method will lazily enumerate the source collection yield returning each non-null item.

Now let’s see the method in practice:

shapeList.FilterNotNull();

As we see here, we call our FilterNotNull() method just like we called the other LINQ sorting and filtering methods, which returns:

Cone { ShapeId = 1, ShapeType = Cone, Width = 1, Height = 3, Is3D = True }
Square { ShapeId = 0, ShapeType = Square, Width = 2, Height = 2, Is3D = False }
Rectangle { ShapeId = 2, ShapeType = Rectangle, Width = 6, Height = 4, Is3D = False }
Square { ShapeId = 3, ShapeType = Square, Width = 5, Height = 5, Is3D = False }

Conclusion

In this article, we covered a few of the excellent ways to implement Sorting and Filtering with LINQ. We hope that using this information you will be able to sort and filter your data with LINQ to get exactly what you need out of each query!

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