Dynamically sorting an IQueryable
Tags: dotnet, csharp, programming, refactorit
Published: 2022-05-30 21:15:19

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:

```csharp
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 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:

```csharp
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:

```csharp
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.

```csharp
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](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/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:

```csharp
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:

```csharp
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](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression) might come handy in such cases.

Regarding the `PropertyName`, we can apply our logic and directly return our ternary operator:

```csharp
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:

```csharp
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`](https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.orderby?view=net-6.0#system-linq-enumerable-orderby-2%28system-collections-generic-ienumerable%28%28-0%29%29-system-func%28%28-0-1%29%29%29=) 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:

```csharp
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](https://docs.microsoft.com/en-us/dotnet/csharp/how-to/compare-strings) to handle cases and locals.

Happy coding!