RestfulApi 学习笔记——分页和排序(六)
前言
分页和排序时一些非常常规的操作,同样也有一些我们注意的点。
正文
分页
先来谈及分页。
看下前端传递的参数。
public class EmployeeDtoParameters
{
private const int MaxPageSize = 20;
public string Gender { get; set; }
public string Q { get; set; }
public int PageNumber { get; set; } = 1;
private int _pageSize = 5;
public int PageSize
{
get => _pageSize;
set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value;
}
}
第一个注意的地方就是避免一些攻击,比如说人家设置_pageSize 非常大的话,那么很有可能会让你的机器宕机,同样你也来不及反应抵挡数据爬取。
所以针对这个你可以设置一下最大数量。
然后呢,这上面有个不完善的地方在于由很多类可能都会使用到这个MaxPageSize 和_pageSize 这些,这时候我们最好去提取出来作为一个基础类,然后继承,这样统一了风格。
接着我们可以这样写分页。
public async Task<IEnumerable<Company>> GetCompaniesAsync(CompanyDtoParameters parameters)
{
if (parameters == null)
{
throw new ArgumentNullException(nameof(parameters));
}
var queryExpression = _context.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));
}
return await queryExpression.Skip(parameters.PageNumber-1).Take(parameters.PageSize);
}
利用skip和take,但是这样写不好。
为什么这么说呢? 是这样子的,我们返回分页数据的时候,一般来说,要带上总页数或者说记录数。
为了方便起见呢,后台同样要返回是否有前一页和后一页,还有前一页的地址,和后一页的地址,这样前端写起来方便。
同样的不要谈什么让前端去计算,因为这会让前端苦不堪言,而现在的前端并不是就是干ui切图的,人家还有很多思考性的问题。
在这样还有一个问题,就是以前我们返回的时候一般json数据是这样子的。
{
currentPage:1,
totalPages:20,
prePageLink:"",
nextPageLink:"",
items:[{数据},{数据}],
}
这样写是不符合restful 风格的,比如说api/companies,人家需要的是companies的资源,而不是什么currentPage和totalPages 等等信息。
这些不属于它要请求的资源,那么这些资源应该放到另外一个地方去。很多情况下放到header中,业界一般放在X-Pagination中,那么看下如何实现吧。
首先建立一个pageList 来作为非请求资源的存储,比如说currentPage、totalPages等
public class PagedList<T>: List<T>
{
public int CurrentPage { get; private set; }
public int TotalPages { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public bool HasPrevious => CurrentPage > 1;
public bool HasNext => CurrentPage < TotalPages;
public PagedList(List<T> items, int count, int pageNumber, int pageSize)
{
TotalCount = count;
PageSize = pageSize;
CurrentPage = pageNumber;
TotalPages = (int) Math.Ceiling(count / (double) pageSize);
AddRange(items);
}
public static async Task<PagedList<T>> CreateAsync(IQueryable<T> source, int pageNumber, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedList<T>(items, count, pageNumber, pageSize);
}
}
那么我们的查询这样写:
public async Task<IEnumerable<Company>> GetCompaniesAsync(CompanyDtoParameters parameters)
{
if (parameters == null)
{
throw new ArgumentNullException(nameof(parameters));
}
var queryExpression = _context.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));
}
return await PagedList<Company>.CreateAsync(queryExpression, parameters.PageNumber, parameters.PageSize);
}
这样我们返回的就是一个PagedList,现在我们的资源还是和一个附加参数在一起,比如说页数,那么这时候就看我们的action如何写了。
[HttpGet(Name = nameof(GetCompanies))]
[HttpHead]
public async Task<IActionResult> GetCompanies([FromQuery] CompanyDtoParameters parameters)
{
var companies = await _companyRepository.GetCompaniesAsync(parameters);
var paginationMetadata = new
{
totalCount = companies.TotalCount,
pageSize = companies.PageSize,
currentPage = companies.CurrentPage,
totalPages = companies.TotalPages,
previousLink = companies.HasPrevious ? CreateCompaniesResourceUri(parameters, ResourceUriType.PreviousPage) : null,
nextLink = companies.HasNext ? CreateCompaniesResourceUri(parameters, ResourceUriType.NextPage) : null
};
Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(paginationMetadata,
new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
}));
var companyDtos = _mapper.Map<CompanyDto>(companies);
return Ok(companyDtos);
}
action 的方式倒是好写,就是把一些参数json序列化,放在header 中的x-pagination 中。
这里有一个生成link的,贴一下方法。
private string CreateLinkForCompany(CompanyDtoParameters parameters, ResourceUriType resourceUri)
{
switch (resourceUri)
{
case ResourceUriType.PreviousPage:
return Url.Link(nameof(GetCompanies), new
{
pageNumber=parameters.PageNumber-1,
pageSize=parameters.PageSize,
companyName=parameters.CompanyName,
searchTerm=parameters.SearchTerm
});
case ResourceUriType.NextPage:
return Url.Link(nameof(GetCompanies), new
{
pageNumber = parameters.PageNumber + 1,
pageSize = parameters.PageSize,
companyName = parameters.CompanyName,
searchTerm = parameters.SearchTerm
});
default:
return Url.Link(nameof(GetCompanies), new
{
pageNumber = parameters.PageNumber - 1,
pageSize = parameters.PageSize,
companyName = parameters.CompanyName,
searchTerm = parameters.SearchTerm
});
}
}
这样就是一个简单的一个分页过程,同样的我们看到其中有很多的问题。
比如说paginationMetadata 应该是一个固定的类,这样统一风格。
同样的,CreateLinkForCompany也存在很多的问题,我们在创建link的时候呢,我们写入了很多的参数,比如说:
companyName = parameters.CompanyName,
searchTerm = parameters.SearchTerm
如果查询的参数变化快的话,这将是一个问题,而且每次改动都需要改动这些,这也是一个问题,暂时就不改动,后面有一个小节来解决这些问题。
排序
排序问题也是我们常见的一个业务,这个业务应该属于过于常见吧。按照restful 风格的排序呢,一般是这样子的。/api/companies?orderby=name desc这样子的。
这里排序字段name和排序规则desc写在了一起,为什么这么做呢?原因就是以后可能多个扩展,如果扩展一个,那么可能就是多两个字段,这样不好。
还有一个重大问题,我们一般在uri中写的排序字段name,不是真正的数据库字段,数据库字段是companyName,这样子的,那么我们就要有一个映射关系,ok,那么这个如何处理呢?
我们想到的就是这种:
if(orderby.startWidth('name'))
{
dbset.company.orderby(u=>u.companyName)
}
但是呢,这样有一个问题,如果排序字段的选择可以很多(比如说name或者其他一些都可以作为排序的)这样会形成一个大的逻辑段,那么应该怎么办呢?
这时候应该形成一个映射关系,使用PropertyMappingService可以做到。
这里用emplyee的查询举例,在这个例子中不需要知道具体的emplyee是什么,只需看到如何映射即可。
public class PropertyMappingService : IPropertyMappingService
{
private readonly Dictionary<string, PropertyMappingValue> _companyPropertyMapping =
new Dictionary<string, PropertyMappingValue>(StringComparer.OrdinalIgnoreCase)
{
{"Id", new PropertyMappingValue(new List<string>{"Id"}) },
{"CompanyName", new PropertyMappingValue(new List<string>{"Name"}) },
{"Country", new PropertyMappingValue(new List<string>{"Country"}) },
{"Industry", new PropertyMappingValue(new List<string>{ "Industry"})},
{"Product", new PropertyMappingValue(new List<string>{"Product"})},
{"Introduction", new PropertyMappingValue(new List<string>{"Introduction"})}
};
private readonly Dictionary<string, PropertyMappingValue> _employeePropertyMapping =
new Dictionary<string, PropertyMappingValue>(StringComparer.OrdinalIgnoreCase)
{
{"Id", new PropertyMappingValue(new List<string>{"Id"}) },
{"CompanyId", new PropertyMappingValue(new List<string>{"CompanyId"}) },
{"EmployeeNo", new PropertyMappingValue(new List<string>{"EmployeeNo"}) },
{"Name", new PropertyMappingValue(new List<string>{"FirstName", "LastName"})},
{"GenderDisplay", new PropertyMappingValue(new List<string>{"Gender"})},
{"Age", new PropertyMappingValue(new List<string>{"DateOfBirth"}, true)}
};
private readonly IList<IPropertyMapping> _propertyMappings = new List<IPropertyMapping>();
public PropertyMappingService()
{
_propertyMappings.Add(new PropertyMapping<EmployeeDto, Employee>(_employeePropertyMapping));
_propertyMappings.Add(new PropertyMapping<CompanyDto, Company>(_companyPropertyMapping));
}
public Dictionary<string, PropertyMappingValue> GetPropertyMapping<TSource, TDestination>()
{
var matchingMapping = _propertyMappings.OfType<PropertyMapping<TSource, TDestination>>();
var propertyMappings = matchingMapping.ToList();
if (propertyMappings.Count == 1)
{
return propertyMappings.First().MappingDictionary;
}
throw new Exception($"无法找到唯一的映射关系:{typeof(TSource)}, {typeof(TDestination)}");
}
public bool ValidMappingExistsFor<TSource, TDestination>(string fields)
{
var propertyMapping = GetPropertyMapping<TSource, TDestination>();
if (string.IsNullOrWhiteSpace(fields))
{
return true;
}
var fieldAfterSplit = fields.Split(",");
foreach (var field in fieldAfterSplit)
{
var trimmedField = field.Trim();
var indexOfFirstSpace = trimmedField.IndexOf(" ", StringComparison.Ordinal);
var propertyName = indexOfFirstSpace == -1 ? trimmedField
: trimmedField.Remove(indexOfFirstSpace);
if (!propertyMapping.ContainsKey(propertyName))
{
return false;
}
}
return true;
}
}
这样就可以形成一个映射关系。比如说Name 表示两个字段firstname 还有lastname这样,这个其实自己也可以实现。
好的,来解释一下代码吧。
首先实例化了一个数组:private readonly IList _propertyMappings = new List();
private readonly Dictionary<string, PropertyMappingValue> _employeePropertyMapping =
new Dictionary<string, PropertyMappingValue>(StringComparer.OrdinalIgnoreCase)
{
{"Id", new PropertyMappingValue(new List<string>{"Id"}) },
{"CompanyId", new PropertyMappingValue(new List<string>{"CompanyId"}) },
{"EmployeeNo", new PropertyMappingValue(new List<string>{"EmployeeNo"}) },
{"Name", new PropertyMappingValue(new List<string>{"FirstName", "LastName"})},
{"GenderDisplay", new PropertyMappingValue(new List<string>{"Gender"})},
{"Age", new PropertyMappingValue(new List<string>{"DateOfBirth"}, true)}
};
上面就是我们写的映射字典。
来看下PropertyMappingValue 这个是啥,这个其实就是自己定义的一个东西:
public class PropertyMappingValue
{
public IEnumerable<string> DestinationProperties { get; set; }
public bool Revert { get; set; }
public PropertyMappingValue(IEnumerable<string> destinationProperties, bool revert = false)
{
DestinationProperties = destinationProperties
?? throw new ArgumentNullException(nameof(destinationProperties));
Revert = revert;
}
}
接下来就是加进去:
public PropertyMappingService()
{
_propertyMappings.Add(new PropertyMapping<EmployeeDto, Employee>(_employeePropertyMapping));
_propertyMappings.Add(new PropertyMapping<CompanyDto, Company>(_companyPropertyMapping));
}
然后把这几个添加到里面。
public Dictionary<string, PropertyMappingValue> GetPropertyMapping<TSource, TDestination>()
{
var matchingMapping = _propertyMappings.OfType<PropertyMapping<TSource, TDestination>>();
if (matchingMapping.Count() == 1)
{
return matchingMapping.First().MappingDictionary;
}
throw new Exception($"无法找到唯一的映射关系:{typeof(TSource)}, {typeof(TDestination)}");
}
上面这个可以说是关键了,_propertyMappings通过OfType,找到PropertyMapping<TSource, TDestination>类型集合。
如果是PropertyMapping<EmployeeDto, Employee>也就是new PropertyMapping<EmployeeDto, Employee>的一个集合。
然后判断是否添加进去了,如果形成一个1对1的关系,那么就返回我们添加进去的,也就是new PropertyMapping<EmployeeDto, Employee>(_employeePropertyMapping)
看下PropertyMapping 是啥?
public class PropertyMapping<TSource, TDestination>: IPropertyMapping
{
public Dictionary<string,PropertyMappingValue> MappingDictionary { get; private set; }
public PropertyMapping(Dictionary<string, PropertyMappingValue> mappingDictionary)
{
MappingDictionary = mappingDictionary
?? throw new ArgumentNullException(nameof(mappingDictionary));
}
}
其实就是做一层封装而已,为的就是OfType。在这里非常关键的就是ofType,https://www.cnblogs.com/macT/p/12069362.html 可以看下这个,挺好理解的。
那么我们拿到一个映射关系后如何处理呢?来看查询方法。
public async Task<IEnumerable<Employee>> GetEmployeesAsync(Guid companyId,
EmployeeDtoParameters parameters)
{
if (companyId == Guid.Empty)
{
throw new ArgumentNullException(nameof(companyId));
}
var items = _context.Employees.Where(x => x.CompanyId == companyId);
if (!string.IsNullOrWhiteSpace(parameters.Gender))
{
parameters.Gender = parameters.Gender.Trim();
var gender = Enum.Parse<Gender>(parameters.Gender);
items = items.Where(x => x.Gender == gender);
}
if (!string.IsNullOrWhiteSpace(parameters.Q))
{
parameters.Q = parameters.Q.Trim();
items = items.Where(x => x.EmployeeNo.Contains(parameters.Q)
|| x.FirstName.Contains(parameters.Q)
|| x.LastName.Contains(parameters.Q));
}
var mappingDictionary = _propertyMappingService.GetPropertyMapping<EmployeeDto, Employee>();
items = items.ApplySort(parameters.OrderBy, mappingDictionary);
return await items.ToListAsync();
}
我们拿到一个映射关系后就可以进行排序了,看下ApplySort。
public static IQueryable<T> ApplySort<T>(
this IQueryable<T> source,
string orderBy,
Dictionary<string, PropertyMappingValue> mappingDictionary)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
if (mappingDictionary == null)
{
throw new ArgumentNullException(nameof(mappingDictionary));
}
if (string.IsNullOrWhiteSpace(orderBy))
{
return source;
}
var orderByAfterSplit = orderBy.Split(",");
foreach (var orderByClause in orderByAfterSplit.Reverse())
{
var trimmedOrderByClause = orderByClause.Trim();
var orderDescending = trimmedOrderByClause.EndsWith(" desc");
var indexOfFirstSpace = trimmedOrderByClause.IndexOf(" ", StringComparison.Ordinal);
var propertyName = indexOfFirstSpace == -1
? trimmedOrderByClause
: trimmedOrderByClause.Remove(indexOfFirstSpace);
if (!mappingDictionary.ContainsKey(propertyName))
{
throw new ArgumentNullException($"没有找到Key为{propertyName}的映射");
}
var propertyMappingValue = mappingDictionary[propertyName];
if (propertyMappingValue == null)
{
throw new ArgumentNullException(nameof(propertyMappingValue));
}
foreach (var destinationProperty in propertyMappingValue.DestinationProperties.Reverse())
{
if (propertyMappingValue.Revert)
{
orderDescending = !orderDescending;
}
source = source.OrderBy(destinationProperty +
(orderDescending ? " descending" : " ascending"));
}
}
return source;
}
这个还是比较好理解的,思路就是用,切割orderby,然后判断是否包含desc,然后赋值给orderDescending 表示是否降序。
同样如果包含 desc,去除掉相应的desc,保留前面的字段。然后就去找我们的映射字典,接下来就是通过orderby的拼接来完成。
在这里我们发现这个orderby传入的是一个字符串,而我们ef又没有,是的,这是通过扩展程序来的,用库。using System.Linq.Dynamic.Core;,安装即可。
是的至此就基本结束了,对了我们依然需要依赖注入,因为可能以后我们有更优雅的方式,注入方式如下:
services.AddTransient<IPropertyMappingService, PropertyMappingService>();
排序的思想很简单,值得学习的是一个映射方式,为什么添加一个包装类。
结
下一节其他请求方式
RestfulApi 学习笔记——分页和排序(六)的更多相关文章
- Effective STL 学习笔记 31:排序算法
Effective STL 学习笔记 31:排序算法 */--> div.org-src-container { font-size: 85%; font-family: monospace; ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十六章:实例化和截头锥体裁切
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十六章:实例化和截头锥体裁切 代码工程地址: https://git ...
- UNP学习笔记(第二十六章 线程)
线程有时称为轻权进程(lightweight process) 同一进程内的所有线程共享相同的全局内存.这使得线程之间易于共享信息,然后这样也会带来同步的问题 同一进程内的所有线程处理共享全局变量外还 ...
- Dynamic CRM 2013学习笔记(二十六)报表设计:Reporting Service报表 动态参数、参数多选全选、动态列、动态显示行字体颜色
上次介绍过CRM里开始报表的一些注意事项:Dynamic CRM 2013学习笔记(十五)报表入门.开发工具及注意事项,本文继续介绍报表里的一些动态效果:动态显示参数,参数是从数据库里查询出来的:参数 ...
- Dynamic CRM 2013学习笔记(四十六)简单审批流的实现
前面介绍过自定义审批流: Dynamic CRM 2013学习笔记(十九)自定义审批流1 - 效果演示 Dynamic CRM 2013学习笔记(二十一)自定义审批流2 - 配置按钮 Dynamic ...
- 【Java学习笔记之二十六】深入理解Java匿名内部类
在[Java学习笔记之二十五]初步认知Java内部类中对匿名内部类做了一个简单的介绍,但是内部类还存在很多其他细节问题,所以就衍生出这篇博客.在这篇博客中你可以了解到匿名内部类的使用.匿名内部类要注意 ...
- MySQL实战45讲学习笔记:第十六讲
一.今日内容概要 在你开发应用的时候,一定会经常碰到需要根据指定的字段排序来显示结果的需求.还是以我们前面举例用过的市民表为例,假设你要查询城市是“杭州”的所有人名字,并且按照姓名排序返回前 1000 ...
- Java学习笔记之JNDI(六)
JNDI 是什么 JNDI是 Java 命名与目录接口(Java Naming and Directory Interface),在J2EE规范中是重要的规范之一,不少专家认为,没有透彻理解JNDI的 ...
- tableau入门学习笔记--分页功能
最近在使用tableau来制作报表,对于tableau也是第一次接触并使用,每天学习些新的功能来记录在博客里,给他人方便,也给自己方便 tableau分页功能 很多时候由于工作表过长而出现拖拽条,如果 ...
- Hadoop学习笔记: 全排序
在Hadoop中实现全排序有如下三种方法: 1. 只使用一个reducer 2. 自定义partitioner 3. 使用TotalOrderPartitioner 其中第一种方法显然违背了mapre ...
随机推荐
- URL(网址)的组成
URL(Uniform Resource Locator,统一资源定位器)就是通常所说的"网址".它是用来标识互联网上资源(如网页.图片.文件等)的唯一地址.URL由协议(如htt ...
- Nginx的负载均衡策略(4+2)
Nginx的负载均衡策略主要包括以下几种: 轮询(Round Robin):每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除.这是Nginx的默认策略,适合服务器配置 ...
- STM32 SPI DMA 源码解析及总结
一 前言 最近在调试stm32的SPI时候i,遇到了一个非常诡异的问题.中间花费了不少时间才把问题搞定.这中间暴露的问题值得反思.借此机会,还是梳理一下stm32的SPI的代码做一个总结吧. 二 初始 ...
- 一个简单的RTMP服务器实现 --- RTMP复杂握手(Complex Handshake)
PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明 本文作为本人csdn blog的主站的备份.(Bl ...
- 使用zxing来生成二维码
使用zxing来生成二维码 二维码已经成为了现代生活中不可或缺的一部分,无论是商业还是个人使用,二维码都有着广泛的应用.而在二维码的生成过程中,zxing是一款非常优秀的开源库,它提供了一系列的API ...
- 建民的Java小课堂
Java Java快问快答: 1.JAVA的基本运行单位是类还是方法? 很明显是类 2.类由什么组成? 由特性和行为的对象组成 3.变量的类型,相互之间可以转换吗,浮点数? 答案是可以 int i=9 ...
- CYQ.Data 操作 Json 性能测试:对比 Newtonsoft.Json
前言: 在 CYQ.Data 版本更新的这么多年,中间过程的版本都在完善各种功能. 基于需要支持或兼容的代码越多,很多时候,常规思维,都把相关功能完成,就结束了. 实现过程中,无法避免的会用到大量的反 ...
- MySQL(初识数据库)
一 存储数据的演变过程 随意的存在一个文件中.数据格式也是千差万别的完全取决于我们自己 软件开发目录规范 限制了存储数据的具体位置 ''' bin conf core lib db readme.tx ...
- 《Go程序设计语言》学习笔记之结构体
<Go程序设计语言>学习笔记之结构体 一. 环境 Centos8.5, go1.17.5 linux/amd64 二. 概念 结构体是将零个或者多个任意类型的命名变量组合在一起的聚合数据类 ...
- archlinux virtualbox使用文件共享 主机arch,客机windows8.1 windows10
参照 https://www.cnblogs.com/cuitang/p/11263008.html 1.安装virtualbox增强功能VBoxGuestAdditions.iso (1)从virt ...