17 FILTERING
17 过滤
In this chapter, we are going to cover filtering in ASP.NET Core Web API. We’ll learn what filtering is, how it’s different from searching, and how to implement it in a real-world project.
在本章中,我们将介绍 ASP.NET Core Web API 中的筛选。我们将了解什么是筛选,它与搜索有何不同,以及如何在实际项目中实现它。
While not critical as paging, filtering is still an important part of a flexible REST API, so we need to know how to implement it in our API projects.
虽然过滤不像分页那样重要,但它仍然是灵活的 REST API 的重要组成部分,因此我们需要知道如何在我们的 API 项目中实现它。
Filtering helps us get the exact result set we want instead of all the results without any criteria.
筛选可以帮助我们获得所需的确切结果集,而不是没有任何条件的所有结果。
17.1 What is Filtering?
17.1 什么是过滤?
Filtering is a mechanism to retrieve results by providing some kind of criterion. We can write many kinds of filters to get results by type of class property, value range, date range, or anything else.
筛选是一种通过提供某种标准来检索结果的机制。我们可以编写多种过滤器来按类属性类型、值范围、日期范围或其他任何内容来获取结果。
When implementing filtering, you are always restricted by the predefined set of options you can set in your request. For example, you can send a date value to request an employee, but you won’t have much success.
实施筛选时,您始终受到可在请求中设置的预定义选项集的限制。例如,您可以发送日期值来请求员工,但不会有太大的成功。
On the front end, filtering is usually implemented as checkboxes, radio buttons, or dropdowns. This kind of implementation limits you to only those options that are available to create a valid filter.
在前端,筛选通常实现为复选框、单选按钮或下拉列表。这种实现将您限制为仅可用于创建有效过滤器的那些选项。
Take for example a car-selling website. When filtering the cars you want, you would ideally want to select:
以一个汽车销售网站为例。在筛选所需的汽车时,理想情况下需要选择:
• Car manufacturer as a category from a list or a dropdown
汽车制造商作为列表或下拉列表中的类别
• Car model from a list or a dropdown
来自列表或下拉列表的汽车模型
• Is it new or used with radio buttons
它是新的还是与单选按钮一起使用的
• The city where the seller is as a dropdown
卖家所在的城市作为下拉列表
• The price of the car is an input field (numeric)
汽车的价格是一个输入字段 (数字)
• ......
You get the point. So, the request would look something like this:
你明白了。因此,请求将如下所示:
Or even like this:
或者甚至像这样:
Now that we know what filtering is, let’s see how it’s different from searching.
现在我们知道了什么是筛选,让我们看看它与搜索有什么不同。
17.2 How is Filtering Different from Searching?
17.2 过滤与搜索有何不同?
When searching for results, we usually have only one input and that’s the one you use to search for anything within a website.
在搜索结果中,我们通常只有一个输入,那就是您用来搜索网站内任何内容的输入。
So in other words, you send a string to the API and the API is responsible for using that string to find any results that match it.
换句话说,您向 API 发送一个字符串,API 负责使用该字符串查找与它匹配的任何结果。
On our car website, we would use the search field to find the “Ford Expedition” car model and we would get all the results that match the car name “Ford Expedition.” Thus, this search would return every “Ford Expedition” car available.
在我们的汽车网站上,我们将使用搜索字段查找“Ford Expedition”汽车模型,我们将获得与汽车名称“Ford Expedition”匹配的所有结果。因此,此搜索将返回所有可用的“Ford Expedition”汽车。
We can also improve the search by implementing search terms like Google does, for example. If the user enters the Ford Expedition without quotes in the search field, we would return both what’s relevant to Ford and Expedition. But if the user puts quotes around it, we would search the entire term “Ford Expedition” in our database.
例如,我们还可以通过像 Google 一样实施搜索词来改进搜索。如果用户在搜索字段中输入 Ford Expedition,但不包含引号,我们将同时返回与 Ford 和 Expedition 相关的内容。但是,如果用户用引号括起来,我们会在我们的数据库中搜索整个术语 “Ford Expedition”。
It makes a better user experience. Example:
它可以提供更好的用户体验。示例:
https://bestcarswebsite.com/sale/search?name=fordfocus
Using search doesn’t mean we can’t use filters with it. It makes perfect sense to use filtering and searching together, so we need to take that into account when writing our source code.
使用 search 并不意味着我们不能对它使用 filter。同时使用 filtering 和 search 非常有意义,因此我们在编写源代码时需要考虑到这一点。
But enough theory.
但理论已经足够了。
Let’s implement some filters.
让我们实现一些过滤器。
17.3 How to Implement Filtering in ASP.NET Core Web API
17.3 如何在 ASP.NET Core Web API 中实现过滤
We have the Age property in our Employee class. Let’s say we want to find out which employees are between the ages of 26 and 29. We also want to be able to enter just the starting age — and not the ending one — and vice versa.
我们的 Employee 类中有 Age 属性。假设我们想要了解哪些员工的年龄在 26 到 29 岁之间。我们还希望能够只输入起始年龄 — 而不是结束年龄 — 反之亦然。
We would need a query like this one:
我们需要一个这样的查询:
https://localhost:5001/api/companies/companyId/employees?minAge=26&maxAge=29
But, we want to be able to do this too:
但是,我们也希望能够做到这一点:
https://localhost:5001/api/companies/companyId/employees?minAge=26
Or like this:
或者像这样:
https://localhost:5001/api/companies/companyId/employees?maxAge=29
Okay, we have a specification. Let’s see how to implement it.
好的,我们有一个规范。让我们看看如何实现它。
We’ve already implemented paging in our controller, so we have the necessary infrastructure to extend it with the filtering functionality. We’ve used the EmployeeParameters class, which inherits from the RequestParameters class, to define the query parameters for our paging request.
我们已经在控制器中实现了分页,因此我们拥有必要的基础设施来使用过滤功能来扩展它。我们使用了 EmployeeParameters 类(继承自 RequestParameters 类)来定义分页请求的查询参数。
Let’s extend the EmployeeParameters class:
让我们扩展 EmployeeParameters 类:
namespace Shared.RequestFeatures;
public class EmployeeParameters : RequestParameters
{
public uint MinAge { get; set; }
public uint MaxAge { get; set; } = int.MaxValue;
public bool ValidAgeRange => MaxAge > MinAge;
}
We’ve added two unsigned int properties (to avoid negative year values):MinAge and MaxAge.
我们添加了两个 unsigned int 属性(以避免负年份值):MinAge 和 MaxAge。
Since the default uint value is 0, we don’t need to explicitly define it; 0 is okay in this case. For MaxAge, we want to set it to the max int value. If we don’t get it through the query params, we have something to work with. It doesn’t matter if someone sets the age to 300 through the params; it won’t affect the results.
由于默认的 uint 值为 0,因此我们不需要显式定义它;在这种情况下,0 是可以的。对于 MaxAge,我们希望将其设置为 max int 值。如果我们没有通过 query params 获取它,我们有一些东西可以使用。如果有人通过 params 将 age 设置为 300 并不重要;它不会影响结果。
We’ve also added a simple validation property – ValidAgeRange. Its purpose is to tell us if the max-age is indeed greater than the min-age. If it’s not, we want to let the API user know that he/she is doing something wrong.
我们还添加了一个简单的验证属性 – ValidAgeRange。它的目的是告诉我们 max-age 是否确实大于 min-age。如果不是,我们想让 API 用户知道他/她做错了什么。
Okay, now that we have our parameters ready, we can modify the GetEmployeesAsync service method by adding a validation check as a first statement:
好了,现在我们已经准备好了参数,我们可以通过添加验证检查作为第一个语句来修改 GetEmployeesAsync 服务方法:
public async Task<(IEnumerable<EmployeeDto> employees, MetaData metaData)> GetEmployeesAsync
(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges)
{
if (!employeeParameters.ValidAgeRange)
throw new MaxAgeRangeBadRequestException();
await CheckIfCompanyExists(companyId, trackChanges);
var employeesWithMetaData = await _repository.Employee
.GetEmployeesAsync(companyId, employeeParameters, trackChanges);
var employeesDto = _mapper.Map<IEnumerable<EmployeeDto>>(employeesWithMetaData);
return (employees: employeesDto, metaData: employeesWithMetaData.MetaData);
}
We’ve added our validation check and a BadRequest response if the validation fails.
我们添加了验证检查和验证失败时的 BadRequest 响应。
But we don’t have this custom exception class so, we have to create it in the Entities/Exceptions class:
但是我们没有这个自定义异常类,因此,我们必须在 Entities/Exceptions 类中创建它:
namespace Entities.Exceptions;
public sealed class MaxAgeRangeBadRequestException : BadRequestException
{
public MaxAgeRangeBadRequestException()
: base("Max age can't be less than min age.")
{
}
}
That should do it.
那应该可以。
After the service class modification and creation of our custom exception class, let’s get to the implementation in our EmployeeRepository class:
在修改服务类并创建自定义异常类之后,让我们开始 EmployeeRepository 类中的实现:
public async Task<PagedList<Employee>> GetEmployeesAsync(Guid companyId,
EmployeeParameters employeeParameters, bool trackChanges)
{
var employees = await FindByCondition(e => e.CompanyId.Equals(companyId) &&
(e.Age >= employeeParameters.MinAge && e.Age <= employeeParameters.MaxAge), trackChanges)
.OrderBy(e => e.Name)
.ToListAsync();
return PagedList<Employee>
.ToPagedList(employees, employeeParameters.PageNumber, employeeParameters.PageSize);
}
Actually, at this point, the implementation is rather simple too.
实际上,在这一点上,实现也相当简单。
We are using the FindByCondition method to find all the employees with an Age between the MaxAge and the MinAge.
我们使用 FindByCondition 方法查找 Age 介于 MaxAge 和 MinAge 之间的所有员工。
Let’s try it out.
让我们试一试。
17.4 Sending and Testing a Query
17.4 发送和测试查询
Let’s send a first request with only a MinAge parameter:
让我们发送第一个只有一个 MinAge 参数的请求:
https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees?minAge=32
Next, let’s send one with only a MaxAge parameter:
接下来,让我们发送一个仅包含 MaxAge 参数的 Cookie:
https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees?maxAge=26
After that, we can combine those two:
之后,我们可以将这两者合并:
https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78- 2D54A9991870/employees?minAge=26&maxAge=30
And finally, we can test the filter with the paging:
最后,我们可以使用分页来测试过滤器:
https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees?pageNumber=1&pageSize=4&minAge=32&maxAge=35
Excellent. The filter is implemented and we can move on to the searching part.
非常好。过滤器已实现,我们可以继续搜索部分。