As with everything else so far, we need to extend our RequestParameters
class first to be able to send requests with the orderBy
clause:
public abstract class RequestParameters { const int maxPageSize = 50; public int PageNumber { get; set; } = 1; private int _pageSize = 10; public int PageSize { get { return _pageSize; } set { _pageSize = (value > maxPageSize) ? maxPageSize : value; } } public string? OrderBy { get; set; } }
The only thing we’ve added is the OrderBy
property and we added it to the RequestParameters
class because we can reuse it for other entities. We want to sort our results by name, even if it hasn’t been stated explicitly in the request.
That said, let’s modify the EmployeeParameters
class to enable the default sorting condition for Employee
if none was stated:
public class EmployeeParameters : RequestParameters { public EmployeeParameters() => OrderBy = "name"; public uint MinAge { get; set; } public uint MaxAge { get; set; } = int.MaxValue; public bool ValidAgeRange => MaxAge > MinAge; public string? SearchTerm { get; set; } }
Next, we will dive right into the implementation of our sorting mechanism, or rather, our ordering mechanism. One thing to note is that we’ll be using the System.Linq.Dynamic.Core
NuGet package to dynamically create our OrderBy
query on the fly. So, feel free to install it in the Persistence
project and add a using
directive in the RepositoryEmployeeExtensions
class:
using System.Linq.Dynamic.Core;
Now, we can add a new extension method Sort
in our RepositoryEmployeeExtensions
class:
public static IQueryable<Employee> Sort(this IQueryable<Employee> employees, string orderByQueryString) { if (string.IsNullOrWhiteSpace(orderByQueryString)) return employees.OrderBy(e => e.Name); var orderParams = orderByQueryString.Trim().Split(','); var propertyInfos = typeof(Employee).GetProperties(BindingFlags.Public | BindingFlags.Instance); var orderQueryBuilder = new StringBuilder(); foreach (var param in orderParams) { if (string.IsNullOrWhiteSpace(param)) continue; var propertyFromQueryName = param.Split(" ")[0]; var objectProperty = propertyInfos.FirstOrDefault(pi => pi.Name.Equals(propertyFromQueryName, StringComparison.InvariantCultureIgnoreCase)); if (objectProperty is null) continue; var direction = param.EndsWith(" desc") ? "descending" : "ascending"; orderQueryBuilder.Append($"{objectProperty.Name.ToString()} {direction}, "); } var orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); if (string.IsNullOrWhiteSpace(orderQuery)) return employees.OrderBy(e => e.Name); return employees.OrderBy(orderQuery); }
Okay, there are a lot of things going on here, so let’s take it step by step and see what exactly we’ve done.
Step By Step Explanation
First, let’s start with the method definition. It has two arguments — one for the list of employees as IQueryable<Employee>
and the other for the ordering query. If we send a request like this one: https://localhost:5001/api/companies/companyId/employees?orderBy=name,age desc
, our orderByQueryString
will be name,age desc
. We begin by executing some basic checks against the orderByQueryString
. If it is null or empty, we just return the same collection ordered by name:
if (string.IsNullOrWhiteSpace(orderByQueryString)) return employees.OrderBy(e => e.Name);
Next, we are splitting our query string to get the individual fields:
var orderParams = orderByQueryString.Trim().Split(',');
We’re also using a bit of reflection to prepare the list of PropertyInfo
objects that represent the properties of our Employee
class. We need them to be able to check if the field received through the query string exists in the Employee
class:
var propertyInfos = typeof(Employee).GetProperties(BindingFlags.Public | BindingFlags.Instance);
That prepared, we can run through all the parameters and check for their existence:
if (string.IsNullOrWhiteSpace(param)) continue; var propertyFromQueryName = param.Split(" ")[0]; var objectProperty = propertyInfos.FirstOrDefault(pi => pi.Name.Equals(propertyFromQueryName, StringComparison.InvariantCultureIgnoreCase));
If we don’t find such a property, we skip the step in the foreach loop and go to the next parameter in the list:
if (objectProperty is null) continue;
If we do find the property, we return it and check if our parameter contains “desc” at the end of the string. We use that to decide how we should order our property:
var direction = param.EndsWith(" desc") ? "descending" : "ascending";
To build our query with each loop, we use the StringBuilder
variable:
orderQueryBuilder.Append($"{objectProperty.Name.ToString()} {direction}, ");
Now that we’ve looped through all the fields, we are just removing excess commas and doing one last check to see if our query indeed has something in it:
var orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); if (string.IsNullOrWhiteSpace(orderQuery)) return employees.OrderBy(e => e.Name);
Finally, we can order our query:
return employees.OrderBy(orderQuery);
At this point, the orderQuery
variable should contain the “Name ascending, DateOfBirth descending” string. That means it will order our results first by Name
in ascending order, and then by DateOfBirth
in descending order.
The standard LINQ query for this would be:
employees.OrderBy(e => e.Name).ThenByDescending(o => o.Age);
This is a neat little trick to form a query when you don’t know in advance how you should sort. Once we have done this, all we have to do is modify this part of the GetEmployeesAsync
repository method:
var employeesQuery = FindByCondition(e => e.CompanyId.Equals(companyId), trackChanges) .FilterEmployees(employeeParameters.MinAge, employeeParameters.MaxAge) .Search(employeeParameters.SearchTerm!) .Sort(employeeParameters.OrderBy!)
We call the Sort()
method and remove the OrderBy()
method. We can test this functionality now.