In this article, we’re going to talk about sorting in ASP.NET Core Web API. Sorting is a commonly used mechanism, that every API should implement. Implementing it in ASP.NET Core is not difficult due to the flexibility of LINQ and good integration with EF Core.
If you want to follow along with the article, you can use the start branch and if you want to get the final solution or if you get stuck, switch to the final branch.
NOTE: Some degree of previous knowledge is needed to follow this article. It relies heavily on the ASP.NET Core Web API series on Code Maze, so if you are not sure how to set up the database or how the underlying architecture works, we strongly suggest you go through the series.
Let’s begin.
What is Sorting?
Sorting, in this case, refers to ordering our results in a preferred way, using our query string parameters. We are not talking about sorting algorithms nor we are going into how’s of implementing a sorting algorithm.
What we’re interested in, however, is how do we make our API sort our results the way we want it to.
Let’s say we want our API to sort owners by their name in ascending order, and then by their date of birth.
To do that, our API call needs to look something like this:
https://localhost:5001/api/owner?orderBy=name,dateOfBirth desc
Our API needs to take all the parameters into consideration and sort our results accordingly. In our case, this means sorting results by their name, and then, if there are owners with the same name, sorting them by the DateOfBirth
property.
Let’s recall how our owner data looks like in the database:
For the sake of demonstrating this example, we are going to add one more Anna Bosh to our database. You can add whatever you want besides that to test the results.
So, let’s add another Anna Bosh:
The new Anna is 10 years older and lives on a different address.
Great, now we have the required data to test our functionality properly.
And of course, like with all other functionalities we’ve implemented so far (paging, filtering, and searching), we need to implement this to work well with everything else. We should be able to get the paginated, filtered and sorted data for example.
Let’s see one way to go around implementing this.
How to Implement Sorting in ASP.NET Core Web API
First, since we want every Entity to be sortable by some criterium, we are going to add OrderBy
property to our base class QueryStringParameters
:
public abstract class QueryStringParameters { 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; } }
We won’t set any default values since different classes can have different default values.
For example, we want OwnerParameters to order our results by the property “name” by default:
public class OwnerParameters : QueryStringParameters { public OwnerParameters() { OrderBy = "name"; } public uint MinYearOfBirth { get; set; } public uint MaxYearOfBirth { get; set; } = (uint)DateTime.Now.Year; public bool ValidYearRange => MaxYearOfBirth > MinYearOfBirth; public string Name { get; set; } }
And we want our Accounts to be ordered by DateCreated
:
public class AccountParameters : QueryStringParameters { public AccountParameters() { OrderBy = "DateCreated"; } }
Next, we’re going to dive right into the implementation of our sorting mechanism, or rather, our ordering mechanism.
One thing to note is that we’ll be using System.Linq.Dynamic.Core NuGet package to dynamically create our OrderBy query on the fly. So, feel free to install it in the Repository
project and add a using directive in the OwnerRepository
class.
Let’s add a new private method ApplySort
in our OwnerRepository
class:
private void ApplySort(ref IQueryable<Owner> owners, string orderByQueryString) { if (!owners.Any()) return; if (string.IsNullOrWhiteSpace(orderByQueryString)) { owners = owners.OrderBy(x => x.Name); return; } var orderParams = orderByQueryString.Trim().Split(','); var propertyInfos = typeof(Owner).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 == null) continue; var sortingOrder = param.EndsWith(" desc") ? "descending" : "ascending"; orderQueryBuilder.Append($"{objectProperty.Name.ToString()} {sortingOrder}, "); } var orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); if (string.IsNullOrWhiteSpace(orderQuery)) { owners = owners.OrderBy(x => x.Name); return; } owners = owners.OrderBy(orderQuery); }
Okay, that’s one big ass method. We’re actually doing multiple different things here to get our results sorted, so let’s take it step-by-step and see what’ve done exactly.
Let’s dissect our method.
Implementation – Step by Step
First, let start with the method definition. It has two arguments, one for the list of owners as IQueryable<Owner>
, and other for the ordering query. If we send a request like this one https://localhost:5001/api/owner?orderBy=name,dateOfBirth desc
, our orderByQueryString
will be name,dateOfBirth desc
.
We begin our method implementation with some mandatory checks for owners and queryString. If there are no owners, we exit the method immediately. If we’ve got some owners, but the query string is empty, we want to order the owners by name:
if (!owners.Any()) return; if (string.IsNullOrWhiteSpace(orderByQueryString)) { owners = owners.OrderBy(x => x.Name); return; }
Next, we’re 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 Owner class. We need them to be able to check if the field received through the query string really exists in the Owner
class:
var propertyInfos = typeof(Owner).GetProperties(BindingFlags.Public | BindingFlags.Instance);
Having that prepared, we can actually 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 == null) continue;
If we do find the property, we return it and additionally check if our parameter contains “desc” at the end of the string. We use that to decide how we should order our property:
var sortingOrder = param.EndsWith(" desc") ? "descending" : "ascending";
We use the StringBuilder to build our query with each loop:
orderQueryBuilder.Append($"{objectProperty.Name.ToString()} {sortingOrder}, ");
Now that we’ve looped through all the fields, we are 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)) { owners = owners.OrderBy(x => x.Name); return; }
Finally, we can order our query:
owners = owners.OrderBy(orderQuery);
At this point, our orderQuery
variable should contain “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:
owners.OrderBy(x => x.Name).ThenByDescending(o => o.DateOfBirth);
A neat little trick to form a query when you don’t know how you should sort in advance.
Calling the Method
Now that we’ve seen how the method works up close, we should just call it in our GetOwners
method:
public PagedList<Owner> GetOwners(OwnerParameters ownerParameters) { var owners = FindByCondition(o => o.DateOfBirth.Year >= ownerParameters.MinYearOfBirth && o.DateOfBirth.Year <= ownerParameters.MaxYearOfBirth); SearchByName(ref owners, ownerParameters.Name); ApplySort(ref owners, ownerParameters.OrderBy); return PagedList<Owner>.ToPagedList(owners, ownerParameters.PageNumber, ownerParameters.PageSize); }
We are providing the list of owners and the OrderBy
query string.
That’s it for our implementation. Or maybe not? What if we want to use ApplySort
in the AccountRepository
? Should we actually keep this logic in our repository class?
The answer to both our questions is of course NO!
Let’s see how to make our method more generic, and implement it in such a way that it can be used by both Account
and Owner
repositories (or any other repository that comes later).
Improving the Solution
There are two main things we want to improve on. Kick the private ApplySort
method out of the OwnerRepository
, and make the ApplySort
generic.
So let’s start by defining the SortHelper
class and the ISortHelper
interface in the Helpers folder of the Entities project.
ISortHelper
should declare one method – ApplySort:
public interface ISortHelper<T> { IQueryable<T> ApplySort(IQueryable<T> entities, string orderByQueryString); }
As you can see, ISortHelper
is a generic interface and it can be applied to any type we want. We need to provide a collection of entities, and a sorting string.
Now let’s see the actual implementation:
public class SortHelper<T> : ISortHelper<T> { public IQueryable<T> ApplySort(IQueryable<T> entities, string orderByQueryString) { if (!entities.Any()) return entities; if (string.IsNullOrWhiteSpace(orderByQueryString)) { return entities; } var orderParams = orderByQueryString.Trim().Split(','); var propertyInfos = typeof(T).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 == null) continue; var sortingOrder = param.EndsWith(" desc") ? "descending" : "ascending"; orderQueryBuilder.Append($"{objectProperty.Name.ToString()} {sortingOrder}, "); } var orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); return entities.OrderBy(orderQuery); } }
This implementation gives us the ability to inject this class to our repositories and call ApplySort wherever we need it:
private ISortHelper<Owner> _sortHelper; public OwnerRepository(RepositoryContext repositoryContext, ISortHelper<Owner> sortHelper) : base(repositoryContext) { _sortHelper = sortHelper; }
We also need to extend our RepoWrapper since our repository classes are instantiated there:
public class RepositoryWrapper : IRepositoryWrapper { private RepositoryContext _repoContext; private IOwnerRepository _owner; private IAccountRepository _account; private ISortHelper<Owner> _ownerSortHelper; private ISortHelper<Account> _accountSortHelper; public IOwnerRepository Owner { get { if (_owner == null) { _owner = new OwnerRepository(_repoContext, _ownerSortHelper); } return _owner; } } public IAccountRepository Account { get { if (_account == null) { _account = new AccountRepository(_repoContext, _accountSortHelper); } return _account; } } public RepositoryWrapper(RepositoryContext repositoryContext, ISortHelper<Owner> ownerSortHelper, ISortHelper<Account> accountSortHelper) { _repoContext = repositoryContext; _ownerSortHelper = ownerSortHelper; _accountSortHelper = accountSortHelper; } public void Save() { _repoContext.SaveChanges(); } }
And now we can call it in our GetOwners method:
public PagedList<Owner> GetOwners(OwnerParameters ownerParameters) { var owners = FindByCondition(o => o.DateOfBirth.Year >= ownerParameters.MinYearOfBirth && o.DateOfBirth.Year <= ownerParameters.MaxYearOfBirth); SearchByName(ref owners, ownerParameters.Name); var sortedOwners = _sortHelper.ApplySort(owners, ownerParameters.OrderBy); return PagedList<Owner>.ToPagedList(sortedOwners, ownerParameters.PageNumber, ownerParameters.PageSize); }
Same thing with the AccountRepository
. If you are having trouble implementing it, refer to the finished project on GitHub at the top of the article.
And since we used dependency injection to inject our SortHelper
, we need to not forget to register it in our ServiceExtensions
class:
public static void ConfigureRepositoryWrapper(this IServiceCollection services) { services.AddScoped<ISortHelper<Owner>, SortHelper<Owner>>(); services.AddScoped<ISortHelper<Account>, SortHelper<Account>>(); services.AddScoped<IRepositoryWrapper, RepositoryWrapper>(); }
A lot of work, but it’s worth it, now we can apply sorting to any entity in our project, even if we add new ones in the future.
Let’s test it out.
Testing Our Implementation
Now the fun part starts, let’s put the result of all our efforts to test.
First, let’s try out the query we’ve been using as an example:
https://localhost:5001/api/owner?orderBy=name,dateOfBirth desc
The response should be:
[ { "id": "261e1685-cf26-494c-b17c-3546e65f5620", "name": "Anna Bosh", "dateOfBirth": "1974-11-14T00:00:00", "address": "27 Colored Row" }, { "id": "9c362f85-5581-4182-ac96-b7b88a74dda7", "name": "Anna Bosh", "dateOfBirth": "1964-11-14T00:00:00", "address": "24 Crescent Street" }, { "id": "24fd81f8-d58a-4bcc-9f35-dc6cd5641906", "name": "John Keen", "dateOfBirth": "1980-12-05T00:00:00", "address": "61 Wellfield Road" }, { "id": "f98e4d74-0f68-4aac-89fd-047f1aaca6b6", "name": "Martin Miller", "dateOfBirth": "1983-05-21T00:00:00", "address": "3 Edgar Buildings" }, { "id": "66774006-2371-4d5b-8518-2177bcf3f73e", "name": "Nick Somion", "dateOfBirth": "1998-12-15T00:00:00", "address": "North sunny address 102" }, { "id": "a3c1880c-674c-4d18-8f91-5d3608a2c937", "name": "Sam Query", "dateOfBirth": "1990-04-22T00:00:00", "address": "91 Western Roads" } ]
As you can see, the list is sorted by Name
ascending, and since we have two Anna’s, they were sorted by DateOfBirth
, descending.
Now try reversing the ordering in the query:
https://localhost:5001/api/owner?orderBy=name desc,dateOfBirth
Now the result should be:
[ { "id": "a3c1880c-674c-4d18-8f91-5d3608a2c937", "name": "Sam Query", "dateOfBirth": "1990-04-22T00:00:00", "address": "91 Western Roads" }, { "id": "66774006-2371-4d5b-8518-2177bcf3f73e", "name": "Nick Somion", "dateOfBirth": "1998-12-15T00:00:00", "address": "North sunny address 102" }, { "id": "f98e4d74-0f68-4aac-89fd-047f1aaca6b6", "name": "Martin Miller", "dateOfBirth": "1983-05-21T00:00:00", "address": "3 Edgar Buildings" }, { "id": "24fd81f8-d58a-4bcc-9f35-dc6cd5641906", "name": "John Keen", "dateOfBirth": "1980-12-05T00:00:00", "address": "61 Wellfield Road" }, { "id": "9c362f85-5581-4182-ac96-b7b88a74dda7", "name": "Anna Bosh", "dateOfBirth": "1964-11-14T00:00:00", "address": "24 Crescent Street" }, { "id": "261e1685-cf26-494c-b17c-3546e65f5620", "name": "Anna Bosh", "dateOfBirth": "1974-11-14T00:00:00", "address": "27 Colored Row" } ]
Works like a charm!
Now, you can try different invalid queries like:
https://localhost:5001/api/owner?orderBy=age
https://localhost:5001/api/owner?orderBy=
https://localhost:5001/api/owner?orderBy=name desc&dateOfBirth
and see the results for yourself.
One more thing we should try is if the solution works when combined with paging, filtering, and searching.
https://localhost:5001/api/owner?pageNumber=1&pageSize=5&orderBy=dateOfBirth asc&minYearOfBirth=1960&maxYearOfBirth=1980&name=Anna
Can you guess what’s the result of this query? If you’ve guessed, leave us a comment with what you think the result is.
That’s it, let’s summarize.
Conclusion
As you have seen, even the trivial sort like this requires a certain amount of business logic, some reflection, validation, and even a bit of dynamic query building. But once you implement it, your API becomes really versatile and flexible.
This implementation is as simple as it gets, but there are certain things we can do to improve it. We can implement a service layer (we don’t have one to keep things simple), we can make our code generic, and not just owner-specific, and we can, of course, create an extension method ApplySort
for IQueryable
which would make this solution even more flexible.
In this article we’ve covered:
- The definition of sorting and how it works
- One way to implement sorting in ASP.NET Core Web API
- Testing our solution for valid and invalid queries
- Testing if our solution works together with paging, filtering, and searching
That’s it for sorting. If you have any questions or suggestions, please leave us a comment.
Next up, we’ll cover a very nice topic of data shaping.