Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
May 30, 2022 09:15 pm GMT

Conditionally 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; }}

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);

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!


Original Link: https://dev.to/pbouillon/conditionally-sorting-an-iqueryable-2aig

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To