19 SORTING
19 排序
In this chapter, 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.
在本章中,我们将讨论 ASP.NET Core Web API 中的排序。排序是每个 API 都应该实现的常用机制。由于 LINQ 的灵活性以及与 EF Core 的良好集成,在 ASP.NET Core 中实现它并不困难。
So, let’s talk a bit about sorting.
那么,让我们谈谈排序。
19.1 What is Sorting?
19.1 什么是排序?
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 are we going into the 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.
然而,我们感兴趣的是我们如何让我们的 API 按照我们想要的方式对结果进行排序。
Let’s say we want our API to sort employees by their name in ascending order, and then by their age.
假设我们希望 API 先按员工姓名升序排序,然后再按年龄对员工进行排序。
To do that, our API call needs to look something like this:
为此,我们的 API 调用需要如下所示:
https://localhost:5001/api/companies/companyId/employees?orderBy=name,age desc
Our API needs to consider all the parameters and sort our results accordingly. In our case, this means sorting results by their name; then, if there are employees with the same name, sorting them by the age property.
我们的 API 需要考虑所有参数并相应地对结果进行排序。在我们的例子中,这意味着按名称对结果进行排序;然后,如果存在同名的员工,则按 age 属性对他们进行排序。
So, these are our employees for the IT_Solutions Ltd company:
那么,这些是我们 IT_Solutions Ltd 公司的员工:
For the sake of demonstrating this example (sorting by name and then by age), we are going to add one more Jana McLeaf to our database with the age of 27. You can add whatever you want to test the results:
为了演示此示例(先按姓名排序,然后按年龄排序),我们将向数据库中再添加一个年龄为 27 的 Jana McLeaf。您可以添加任何您想要测试结果的内容:
https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees
Great, now we have the required data to test our functionality properly.
太好了,现在我们有了正确测试我们的功能所需的数据。
And of course, like with all other functionalities we have 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.
让我们看看实现它的方法。
19.2 How to Implement Sorting in ASP.NET Core Web API
19.2 如何在 ASP.NET Core Web API 中实现排序
As with everything else so far, first, we need to extend our RequestParameters class to be able to send requests with the orderBy clause in them:
与到目前为止的其他所有内容一样,首先,我们需要扩展 RequestParameters 类,以便能够发送包含 orderBy 子句的请求:
public 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; } }
As you can see, 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.
如您所见,我们添加的唯一内容是 OrderBy 属性,并将其添加到 RequestParameters 类中,因为我们可以将其重新用于其他实体。我们希望按名称对结果进行排序,即使请求中没有明确说明。
That said, let’s modify the EmployeeParameters class to enable the default sorting condition for Employee if none was stated:
也就是说,让我们修改 EmployeeParameters 类,以启用 Employee 的默认排序条件(如果未说明):
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’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 the 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 RepositoryEmployeeExtensions class:
需要注意的一点是,我们将使用 System.Linq.Dynamic.Core NuGet 包动态创建动态 OrderBy 查询。因此,请随意将其安装在 Repository 项目中,并在 RepositoryEmployeeExtensions 类中添加 using 指令:
using System.Linq.Dynamic.Core;
Now, we can add the new extension method Sort in our RepositoryEmployeeExtensions class:
现在,我们可以在 RepositoryEmployeeExtensions 类中添加新的扩展方法 Sort:
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 == 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.
好了,这里发生了很多事情,所以让我们一步一步来,看看我们到底做了什么。
19.3 Implementation – Step by Step
19.3 实施 – 分步
First, let start with the method definition. It has two arguments — one for the list of employees as IQueryable
首先,让我们从方法定义开始。它有两个参数 — 一个用于 IQueryable 的员工列表,另一个用于排序查询。如果我们发送如下请求:
https://localhost:5001/api/companies/companyId/employees?or derBy=name,age desc,
our orderByQueryString will be name,age desc.
我们的 orderByQueryString 将是 name,age desc。
We begin by executing some basic check against the orderByQueryString. If it is null or empty, we just return the same collection ordered by name.
我们首先对 orderByQueryString 执行一些基本检查。如果为 null 或为空,我们只返回按名称排序的相同集合。
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:
我们还使用了一些反射来准备表示 Employee 类属性的 PropertyInfo 对象列表。我们需要它们能够检查通过查询字符串接收的字段是否存在于 Employee 类中:
var propertyInfos = typeof(Employee).GetProperties(BindingFlags.Public | BindingFlags.Instance);
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:
如果找不到这样的属性,我们将跳过 foreach 循环中的步骤,并转到列表中的下一个参数:
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:
如果我们找到了该属性,则返回该属性,并另外检查我们的参数是否在字符串末尾包含 “desc”。我们使用它来决定我们应该如何排序我们的财产:
var direction = param.EndsWith(" desc") ? "descending" : "ascending";
We use the StringBuilder to build our query with each loop:
我们使用 StringBuilder 构建包含每个循环的查询:
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.
此时,orderQuery 变量应包含“Name ascending, DateOfBirth descending”字符串。这意味着它将首先按 Name 升序对结果进行排序,然后按 DateOfBirth 降序排序。
The standard LINQ query for this would be:
对此的标准 LINQ 查询为:
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 to modify the GetEmployeesAsync repository method:
完成此作后,我们所要做的就是修改 GetEmployeesAsync 存储库方法:
public async Task<PagedList<Employee>> GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges) { var employees = await FindByCondition(e => e.CompanyId.Equals(companyId), trackChanges) .FilterEmployees(employeeParameters.MinAge, employeeParameters.MaxAge).Search(employeeParameters.SearchTerm) .Sort(employeeParameters.OrderBy) .ToListAsync(); return PagedList<Employee> .ToPagedList(employees, employeeParameters.PageNumber, employeeParameters.PageSize); }
And that’s it! We can test this functionality now.
就是这样!我们现在可以测试此功能。
19.4 Testing Our Implementation
19.4 测试我们的实现
First, let’s try out the query we’ve been using as an example:
首先,让我们尝试一下我们一直用作示例的查询:
https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78- 2D54A9991870/employees?orderBy=name,age desc
And this is the result:
结果如下:
We can see that this list is sorted by Name ascending. Since we have two Jana’s, they were sorted by Age descending.
我们可以看到,这个列表是按 Name 升序排序的。由于我们有两个 Jana,因此它们按 Age 降序排序。
We have prepared additional requests which you can use to test this functionality with Postman. So, feel free to do it.
我们准备了其他请求,您可以使用这些请求来通过 Postman 测试此功能。所以,请随意去做。
19.5 Improving the Sorting Functionality
19.5 改进排序功能
Right now, sorting only works with the Employee entity, but what about the Company? It is obvious that we have to change something in our implementation if we don’t want to repeat our code while implementing sorting for the Company entity.
目前,排序仅适用于 Employee 实体,但 Company 呢?很明显,如果我们不想在为 Company 实体实现排序时重复我们的代码,我们必须在实现中更改某些内容。
That said, let’s modify the Sort extension method:
也就是说,让我们修改 Sort 扩展方法:
public static IQueryable<Employee> Sort(this IQueryable<Employee> employees, string orderByQueryString) { if (string.IsNullOrWhiteSpace(orderByQueryString)) return employees.OrderBy(e => e.Name); var orderQuery = OrderQueryBuilder.CreateOrderQuery<Employee>(orderByQueryString); if (string.IsNullOrWhiteSpace(orderQuery)) return employees.OrderBy(e => e.Name); return employees.OrderBy(orderQuery); }
So, we are extracting a logic that can be reused in the CreateOrderQuery
因此,我们正在提取可在 CreateOrderQuery 方法中重复使用的逻辑。但当然,我们必须创建该方法。
Let’s create a Utility folder in the Extensions folder with the new class OrderQueryBuilder:
让我们在 Extensions 文件夹中使用新类 OrderQueryBuilder 创建一个 Utility 文件夹:
Now, let’s modify that class:
现在,让我们修改该类:
public static class OrderQueryBuilder { public static string CreateOrderQuery<T>(string orderByQueryString) { 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 direction = param.EndsWith(" desc") ? "descending" : "ascending"; orderQueryBuilder.Append($"{objectProperty.Name.ToString()} {direction}, "); } var orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); return orderQuery; } }
And there we go. Not too many changes, but we did a great job here. You can test this solution with the prepared requests in Postman and you'll get the same result for sure:
好了。没有太多的变化,但我们在这里做得很好。您可以在 Postman 中使用准备好的请求测试此解决方案,您肯定会得到相同的结果:
But now, this functionality is reusable.
但现在,此功能是可重用的。