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.
Let’s see how we can use LINQ to get exactly the data we want!
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.
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!