Dynamically sorting an IQueryable
Pierre Bouillon
Posted on May 30, 2022
A couple of days ago, I faced a situation where an incoming request may require its result to be sorted.
The model was similar to the following one:
public record Sorting(string PropertyName, string Order);
public class MyRequest : IRequest<SomeDto>
{
// Some fields
public Sorting? Sort { get; init; }
}
A few additional rules where implicit:
-
Sorting.PropertyName
was the name of one of the property ofSomeDto
(the equivalent of anameof
on them) -
Sorting.Order
would be either"asc"
or"desc"
Using EF Core, conditionally sorting the resulting request has been easy but making it cleaner wasn't so much.
I finally came to a solution that I think is clean enough and wanted to share it with whom it may help someday!
Context
Our example will be based on a very simple model with only two fields and the same kind of sorting DTO I had:
public record User(string FirstName, string LastName);
public record Sorting(string PropertyName, string Order);
In our example,
Sorting
will not be null but in a real case don't forget to verify it!
As for our data, they may be something like a list or a DbSet with a query containing the incoming strings:
var users = new List<User>
{
new("Pierre", "Bouillon"),
new("Thomas", "Anderson"),
new("Tom", "Scott"),
new("Keanu", "Reeves"),
new("Edward", "Snowden"),
};
// Or an EF Core query:
var users = Context.Users.Where(user => /* ... */);
var sort = new Sorting("FirstName", "asc");
We're all set!
Naive approach
Firstly, we can easily write a very naive approach: testing each field that might require sorting and then the order before returning the sorted query.
if (sort.PropertyName == nameof(User.FirstName))
{
if (sort.Order == "asc")
{
return users.OrderBy(user => user.FirstName);
}
else
{
return users.OrderByDescending(user => user.FirstName);
}
}
if (sort.PropertyName == nameof(User.LastName))
{
if (sort.Order == "asc")
{
return users.OrderBy(user => user.LastName);
}
else
{
return users.OrderByDescending(user => user.LastName);
}
}
throw new ArgumentException();
That's a bit verbose, especially checking each time for the order when there is only two values: either asc
or something else, we could do better.
Using ternaries
The ternary operator (also referred as conditional operator in the C# reference) is not always easy to read and should be employed with care but I find it pretty explicit there.
Let's refine our checking on the order with it:
if (sort.PropertyName == nameof(User.FirstName))
{
return sort.Order == "asc"
? users.OrderBy(user => user.FirstName)
: users.OrderByDescending(user => user.FirstName);
}
if (sort.PropertyName == nameof(User.LastName))
{
return sort.Order == "asc"
? users.OrderBy(user => user.LastName)
: users.OrderByDescending(user => user.LastName);
}
throw new ArgumentException();
That's a little bit better, however, we can notice a pattern there, our code seems to be a repetition of the following structure:
if (sort.PropertyName == /* a property */)
{
return /* users sorted with the appropriate order */;
}
There must be a way to directly return the appropriate request based on the PropertyName
...
Using switch expression
Introduced in C# 8.0, switch expressions might come handy in such cases.
Regarding the PropertyName
, we can apply our logic and directly return our ternary operator:
return sort.PropertyName switch
{
nameof(User.FirstName) => sort.Order == "asc"
? users.OrderBy(user => user.FirstName)
: users.OrderByDescending(user => user.FirstName),
nameof(User.LastName) => sort.Order == "asc"
? users.OrderBy(user => user.FirstName)
: users.OrderByDescending(user => user.FirstName),
_ => throw new ArgumentException(),
};
That's way more concise but now that the enclosing if
statements have been refactored, we can noticed that there is a lot of repeated code:
return sort.PropertyName switch
{
nameof(/* property */) => sort.Order == "asc"
? users.OrderBy(user => user./* property */)
: users.OrderByDescending(user => user./* property */),
nameof(/* property */) => sort.Order == "asc"
? users.OrderBy(user => user./* property */)
: users.OrderByDescending(user => user./* property */),
_ => throw new ArgumentException(),
};
We might want to look for another way of splitting our sorting.
Using expressions
Taking a step back, we can see that sorting our users
require us to extract two information:
- The property name
- The order
In our previous logic, the evaluation of the order has been done within the evaluation of the property name and it led to code duplication at this place.
If we take a close look at the Enumerable.OrderBy
method, we can see that it is taking the key as a function as parameter.
Let's once again refine our code so that we extract the key before applying the order:
Expression<Func<User, string>> sortBy = sort.PropertyName switch
{
nameof(User.FirstName) => user => user.FirstName,
nameof(User.LastName) => user => user.LastName,
_ => throw new ArgumentException(),
};
return sort.Order == "asc"
? users.OrderBy(sortBy)
: users.OrderByDescending(sortBy);
That's way better!
Beside, we just have to add another arm to the switch statement if we wanted to sort on a new property which would be fairly simple.
On a side note, we were just comparing the order to "asc"
without any other verification. In your code, you might want to take a closer look at the best way to test this value to handle cases and locals.
Happy coding!
Posted on May 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.