使用ASP.NET Core 3.x 构建 RESTful API P17 P18 P19 过滤和搜索 查询参数
使用ASP.NET Core 3.x 构建 RESTful API P17 P18 P19 过滤和搜索 查询参数
博客园文章Id:12676569
理论
如何给API传递数据
- 数据可以通过多种方式来传递给API.
- 在 Asp .Net Core中 Binding source Attributes 会高数Model的绑定引擎从哪里找到绑定.
Binding Source Attributes
Binding Source Attributes一共有六种:
- [FromBody],请求的Body
- [FromForm],请求的Body中的form数据
- [FromHeader],请求的Header
- [FromQuery],Query string 参数
- [FromRoute],当前请求中的路由数据
- [FromService],作为Action参数而注入的服务
例如 从路由模板,或者是查询字符串的参数中获取到的数据:
[ApiController]
特性 [ApiController]
-
默认情况下ASP .NET Core 会使用 Complex Object Model Binder,它会把数据从Value Providers那里提取出来,而Value Providers的顺序是定义好的.
-
但是我们构建API时通常会使用[ApiController]这个特性,为了更好的适应API它改变了上面的规则.
修改后的规则图例如下:
过滤
- 过滤集合的意思就是指根据条件限定返回的集合.
- 例如我想返回所有类型为国有企业的欧洲公司.则URI为: GET /api/companies?type=State-owned®ion=Europe
- 所以过滤就是指:我们把某个字段的名字以及想要让该字段匹配的值一起传递给API,并将这些作为返回集合的一部分.
搜索
-
针对集合进行搜索是指根据预定义的一些规则,把符合条件的数据添加到集合里
-
搜索实际上超处理过滤的范围,针对搜索,通常不会把要匹配的字段名传递过去,通常会把要搜索的值传递给API,然后API自行决定应该对哪些字段来查找该值,经常会是全文搜索.
-
例如: GET /api/companies?q=xxx
过滤 VS 搜索
过滤: 首先是一个完整的集合,然后根据条件把匹配/不匹配的数据项移除.
搜索: 首先是一个空的集合,然后根据条件把匹配/不匹配的数据往里面添加.
注意:过滤和搜索这些参数并不是资源的一部分,只允许针对资源的字段进行过滤.
编码
过滤
我们可以对GetEmployeesForCompany
方法进行如下改造:
注意:我们在使用[FromQuery]
特性时,当传递进来的参数名称与Action
方法的形参名称不一致是,我们可以手动指定匹配,例如[FromQuery(Name = "gender")] string genderDisplay
修改接口,以及接口实现,Action方法
接口:
Task<IEnumerable<Employee>> GetEmployeeAsync(Guid companyId, string genderDisplay);
接口实现:
public async Task<IEnumerable<Employee>> GetEmployeeAsync(Guid companyId,string genderDisplay)
{
if (companyId == null)
{
throw new ArgumentNullException(nameof(companyId));
}
if (string.IsNullOrWhiteSpace(genderDisplay)) //如果为空就不进行过滤
{
return await this._content.Employees
.Where(x => x.CompanyId == companyId)
.OrderBy(x => x.EmployeeNo)
.ToListAsync();
}
var genderStr = genderDisplay.Trim();
var gender = Enum.Parse<Gender>(genderStr);
return await this._content.Employees
.Where(x => x.CompanyId == companyId && x.Gender == gender)
.OrderBy(x => x.EmployeeNo)
.ToListAsync();
}
Action方法:
[HttpGet]
public async Task<ActionResult<IEnumerable<EmployeeDto>>> GetEmployeesForCompany(Guid companyId,[FromQuery(Name = "gender")] string genderDisplay)
{
if (! await this._companyRepository.CompanyExistsAsync(companyId))
{
return NotFound();
}
var employees = await this._companyRepository.GetEmployeeAsync(companyId,genderDisplay);
return Ok(this._mapper.Map<IEnumerable<EmployeeDto>>(employees));
}
调用:
搜索
我们再一次改造接口:
Task<IEnumerable<Employee>> GetEmployeeAsync(Guid companyId, string genderDisplay,string q);
修改实现类:
public async Task<IEnumerable<Employee>> GetEmployeeAsync(Guid companyId,string genderDisplay,string q)
{
if (companyId == null)
{
throw new ArgumentNullException(nameof(companyId));
}
if (string.IsNullOrWhiteSpace(genderDisplay) && string.IsNullOrWhiteSpace(q)) //如果为空就不进行过滤和搜索
{
return await this._content.Employees
.Where(x => x.CompanyId == companyId)
.OrderBy(x => x.EmployeeNo)
.ToListAsync();
}
//要么进行过滤,要么进行搜索,要么进行过滤和搜索
//代码走到此处,实际上并没有查询数据库(延迟加载机制),此处的作用就相当于拼接sql
var items = this._content.Employees.Where(x=>x.CompanyId == companyId) as IQueryable<Employee>;
if (!string.IsNullOrWhiteSpace(genderDisplay)) //处理过滤
{
genderDisplay = genderDisplay.Trim();
var gender = Enum.Parse<Gender>(genderDisplay);
//再一次拼接查询表达式,到此处仍然没有进行查询.
items = items.Where(x => x.Gender == gender);
}
if (!string.IsNullOrWhiteSpace(q)) //处理搜索(全文搜索)
{
q = q.Trim();
items = items.Where(x => x.EmployeeNo.Contains(q) //到此处仍然没有进行查询.
|| x.FirstName.Contains(q)
|| x.LastName.Contains(q));
}
return await items
.OrderBy(x => x.EmployeeNo)
.ToListAsync();
}
调用结果:
我们使API再进行进化.
[HttpGet]
public async Task<ActionResult<IEnumerable<EmployeeDto>>> GetEmployeesForCompany(Guid companyId,[FromQuery(Name = "gender")] string genderDisplay, [FromQuery]string q)
{
if (! await this._companyRepository.CompanyExistsAsync(companyId))
{
return NotFound();
}
var employees = await this._companyRepository.GetEmployeeAsync(companyId,genderDisplay,q);
return Ok(this._mapper.Map<IEnumerable<EmployeeDto>>(employees));
}
通过上述的业务进化,我们观察到当有查询需求的改签,那么我们就不得不需要修改相应Action方法的签名,这是不可取的,那么我们如何应对此类问题呢?我们可以将请求参数封装成一个类,传递给 Action,改造代码示例如下:
封装一个请求参数类,这样做的好处是,当以后有请求参数需求的改变,我们只需要针对请求参数类就行修改以及具体业务方法出根据变更的请求参数修改具体的业务即可:
using System;
namespace Routine.Api.DtoParameters
{
public class CompanyDtoParameters
{
public Guid Id { get; set; }
public string CompanyName { get; set; }
public string SearchTerm { get; set; }
}
}
修改获取所有公司的接口定义:
Task<IEnumerable<Company>> GetCompaniesAsync(CompanyDtoParameters parameters);
修改获取所有公司的实现类:
public async Task<IEnumerable<Company>> GetCompaniesAsync(CompanyDtoParameters parameters)
{
if (parameters == null)
{
throw new ArgumentNullException(nameof(parameters));
}
if (string.IsNullOrWhiteSpace(parameters.CompanyName) &&
string.IsNullOrWhiteSpace(parameters.SearchTerm))
{
//当公司名和搜索条件都为空的时候,我们返回所有的公司
return await this._content.Companies.ToListAsync();
}
//组建查询表达式
var queryExpression = this._content.Companies as IQueryable<Company>;
if (!string.IsNullOrWhiteSpace(parameters.CompanyName)) //过滤条件
{
parameters.CompanyName = parameters.CompanyName.Trim();
queryExpression = queryExpression.Where(x => x.Name == parameters.CompanyName);
}
if (!string.IsNullOrWhiteSpace(parameters.SearchTerm)) //搜索条件 (全文检索)
{
parameters.SearchTerm = parameters.SearchTerm.Trim();
queryExpression = queryExpression.Where(x => x.Name.Contains(parameters.SearchTerm) ||
x.Introduction.Contains(parameters.SearchTerm));
}
//EF core 由于拥有延迟加载的查询特性,当遇到.ToList或者for循环遍历的时候,才会真正的去查询数据库,否则都是
//拼接sql表达式树的过程.
return await queryExpression.ToListAsync();
}
运行调用:
HttpCode 415:表示请求的内容无法被串行化,因为API不支持内容类型,在请求头中没有Content-Type注明请求类型,或者值不被API所支持,就会得到415的状态码.
此时我们发现HTTPStatusCode
的值是415即不支持的媒体类型,为什么呢?因为我们在Action方法 GetCompanies
添加了一个参数 CompanyDtoParameters parameters
这个参数是一个类,这样的参数类型,就变成了复杂类型,那么 特性[ApiController]
会认为GetCompanies
的绑定源在请求的Body
中,但是此接口是HttpGet
方式请求,请求的参数并不来自与请求的Body
,而是来自于查询字符串
所以会报415的错误,这个时候需要我们手动的绑定请求参数的源,修改方法签名如下:
public async Task<ActionResult<IEnumerable<CompanyDto>>> GetCompanies([FromQuery]CompanyDtoParameters parameters
在参数的前面明确声明请求参数的来源在[FromQuery]
中,再运行程序调试观察:
这个时候就正确了.