DEV Community

Cover image for Dynamically sorting an IQueryable
Pierre Bouillon
Pierre Bouillon

Posted on

Dynamically sorting an IQueryable

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; }
}
Enter fullscreen mode Exit fullscreen mode

A few additional rules where implicit:

  • Sorting.PropertyName was the name of one of the property of SomeDto (the equivalent of a nameof 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);
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 */;
}
Enter fullscreen mode Exit fullscreen mode

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(),
};
Enter fullscreen mode Exit fullscreen mode

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(),
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (2)

Collapse
 
raafacachoeira profile image
Rafael Cachoeira

Nice, just a suggestion. Would you can use the open source lib "Dynamic linq - dynamic-linq.net/".
It's possible to build dynamic where clauses and complexes orderBy.

Collapse
 
pbouillon profile image
Pierre Bouillon

Interesting, I didn't knew about it, thanks for sharing!