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.
Check this,
if you provide an invalid single column in OrderBy you get an error stating that the entities.OrderBy(orderQuery) does not work with empty value.
(tenantId is not a valid column in owners entity)
Exception: System.ArgumentException
Message: Value cannot be empty. (Parameter ‘ordering’)
Works properly. Thank you Vladimir for this excellent article.
Did everything as instructed. Why is there a validation error before it even gets to the sorting code?
“title”: “One or more validation errors occurred.”,
“status”: 400,
“errors”: {
“Name”: [
“The Name field is required.”
]
Based on the JSON you provided, it seems that the [ApiController] attribute took over the request and validate it. After .NET 6, if you don’t want any property to be required, you can make it nullable:
public string? Name { get; set; }
If you do this, I think you won’t get this error. Of course, I would suggest you to read more about the ApiController attribute here https://code-maze.com/apicontroller-attribute-in-asp-net-core-web-api/ just to be sure what it does (if you are not familiar with it).
Thank you for responding. I had already done that except for OwnerParameters.cs, after making Name nullable there, it started working. I didn’t think it got validation from that class.
Does this way OrderBy work in .net> 5.0?
I have an exception:
you need to install System.Linq.Dynamic.Core from nuget
Hi, first of all thanks for the tutorial.
I had a question, why this line of code:
Isn’t this an unecessary roundtrip to database?
Regards
Hello, how do you get the PropertyInfos[] of the related class(es) of a parent class like in One-To-Many, One-To-One, Many-To-Many scenerio for sorting and apply them to the dynamic linq?
What I miss here is an option to also sort via navigation properties like
name.firstname, name.lastname
( assuming name is navigation property ).
Hi, thank you for your site and every articles, they are really helpful. I was wondering if you had a solution in your book or site page for sorting from Dto to Entity.
Example : My grid receives a Dto from API. The sort and filtering in querystring are from Dto properties. In Api, I need to translate EmployeeDto.CompleteName to Employee.Name for sorting.
Do you have an clean solution or should do I need to translate before applying sort?
Thank you
Hi ,
I just came back to this great article and I started to wonder why you have done it as service and not as extension method on IQueryable ?
Hey Lukasz, there was no particular reason for doing that except for demonstration purposes. Doing it via the extension method is indeed a viable choice for this problem and you should definitely try it out.
We’ve implemented it as an extension method in our book Ultimate ASP.NET Core Web API.
That said, this is not wrong, but many find it more intuitive as an extension method so we’ve covered that approach as well.
This web page shows var orderParams = orderByQueryString.OrderBy.Trim().Split(‘,’); on the implement SortHelper class but it is correct on the GitHub, it should be var orderParams = orderByQueryString.Trim().Split(‘,’);
Thanks for that. It will be fixed in the article.
hi,
this quey is not working give error
entities.OrderBy(orderQuery);
Hi. The error description would be helpful. Secondly, try to download our source code and run it. It should be working – then you can compare your and our solution. Lastly, have you installed the System.Linq.Dynamic.Core library?
You should always include the include statements in you code references.
Hello Bob. Thanks for the suggestion but we are not including the using statements because Visual Studio does that for you. It will give you suggestion what using statement you require for that peace of code. To be honest, many time I just let visual studio offer me the suggestion about the using statement and only then I know what to include. Additionally, we have the source code for every article, so if there is any confusion about the using statements, you can always check the source code. Finally, in some articles we add additional paragraph to state which using statements are required but not in the code samples.
You mentioned that readers can get the source code for every article, but i still can not find it.
Can You, please tell me how to get it or provide a link.
Hi, I’ve a problem when building the generic version of ApplySort.
owners.OrderBy(x => x.Name) does not return ordered rows. No errors are displayed but simply rows come without order.
With ApplySort() locally in the owner repository all works fine.
I’m working with net core 3.1
Any idea?
Regards
Hey Jose,
There has been a mistake in the article and the source code regarding ApplySort method.
Try pulling the latest version of the code and running the examples again.
Sorry to say this, but it was a copy paste mistake on my part. 🙂
And to clarify, this is a dynamic query, so the OrderByin ApplySort works a bit differently than your standard OrderBy. Check out the definition with F12 in VS. It’s very powerful, and let’s you build complex dynamic and custom queries like this one.
Hope this helps.
Hi Vladimir,
Thank you very much for your support.
I’m unable to sort entities. I’m working with the latest github code and all works fine but the code return entities.OrderBy(orderQuery); returns the same entities without order.
I’m watching the orderQuery parameter and it is fine “Name ascending” or “Description ascending” but the records are returned in the same order as they are read.
No errors, no warnings, I’m a little lost.
I’ve even tried to call return entities.OrderBy(“Name”); with the same result.
Locally ApplySort in every repository works fine, but OrderBy in the generic version does not work. It return the same rows in the same order.
Regards
Jose
Hey Jose,
Are you using modified version of our code, or clean solution as we’ve provided it?
The problem with the code before we’ve fixed it was that we haven’t assigned the return value from the ApplySort method. Now it should be working fine, we’ve tested it locally again.
The above code for generic way is still has issue.. Is that the updated one?
Hey Aishwarya,
It has been updated since, it should work now.
I am having the same issue as well. using latest code on github. the list is returned as is without sorted. API goes through with 200 successful. no errors or whatsoever, System.Linq.Dynamic.Core library is installed
I am having the same issue as well. using latest code on github. the list is returned as is without sorted and the API goes through with 200 successful. no errors or whatsoever. The System.Linq.Dynamic.Core library is also installed. On dotnetcore 3.1
EDIT: Managed to get it working as expected. I had the wrong reference. I double checked it was not using System.Linq.Dynamic.Core. I recreated the Isorthelper and sorthelper using the latest github code. it is working!!
Hi Tham. This is really strange. I’ve just tested the code from this branch and it works as expected. As Vlada replied to Jose, the problem was fixed as you can see with this commit: https://uploads.disquscdn.com/images/8a3e8b53f2eec4e87edf9a593a16ecc86713cc0928952c6f9b0007e15801f981.png
Hey #Tham, it’s easy to mix up the default Linq library with the dynamic one. Glad you’ve managed to sort it out *no pun intended* 🙂