翻页
- Query String
http://localhost:5000/api/posts?pageIndex=1&pageSize=10&orderBy=id
- 使用抽象父类 QueryParameters, 包含常见参数:
PageIndex
PageSize
OrderBy
…
public abstract class QueryParameters : INotifyPropertyChanged
{
private const int DefaultPageSize = 10;
private const int DefaultMaxPageSize = 100;
private int _pageIndex;
public int PageIndex
{
get { return _pageIndex; }
set { _pageIndex = value >= 0 ? value : 0; }
}
private int _pageSize;
public virtual int PageSize
{
get { return _pageSize; }
set => SetField(ref _pageSize, value);
}
private string _orderBy;
public string OrderBy
{
get { return _orderBy; }
set { _orderBy = value ?? nameof(IEntity.Id); }
}
private int _maxPageSize = DefaultMaxPageSize;
protected internal virtual int MaxPageSize
{
get { return _maxPageSize; }
set => SetField(ref _maxPageSize, value);
}
public string Fields { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
field = value;
OnPropertyChanged(propertyName);
if (propertyName == nameof(PageSize) || propertyName == nameof(MaxPageSize))
{
SetPageSize();
}
return true;
}
private void SetPageSize()
{
if (_maxPageSize<=0)
{
_maxPageSize = DefaultMaxPageSize;
}
if (_pageSize<=0)
{
_pageSize = DefaultPageSize;
}
_pageSize = _pageSize > _maxPageSize ? _maxPageSize : _pageSize;
}
}
- 子类继承
public class PostParameters:QueryParameters
{
}
- HTTP Get 传参
[HttpGet]
public async Task<IActionResult> Get(PostParameters postParameters)
{
var posts = await _postRepository.GetAllPostsAsync(postParameters);
var postResources = _mapper.Map<IEnumerable<Post>, IEnumerable<PostResource>>(posts);
return Ok(postResources);
}
- 修改Repositroy
public async Task<IEnumerable<Post>> GetAllPostsAsync(PostParameters postParameters)
{
var query = _applicationContext.Posts.OrderBy(x => x.Id);
return await query
.Skip(postParameters.PageIndex * postParameters.PageSize)
.Take(postParameters.PageSize)
.ToListAsync();
}
返回翻页元数据
-
如果将数据和翻页元数据一起返回:
* metadata
* 响应的body不再符合Accept Header了(不是资源的application/json), 这是一种新的media type.
* 违反REST约束, API消费者不知道如何通过application/json这个类型来解释响应的数据.
- 翻页数据不是资源表述的一部分, 应使用自定义Header (
X-Pagination
). - 存放翻页数据的类: PaginatedList<T>可以继承于List<T>.
public class PaginatedList<T>:List<T> where T:class
{
public int PageSize { get; set; }
public int PageIndex { get; set; }
private int _totalItemsCount;
public int TotalItemsCount
{
get { return _totalItemsCount; }
set { _totalItemsCount = value; }
}
public int PageCount => TotalItemsCount / PageSize + (TotalItemsCount % PageSize > 0 ? 1 : 0);
public bool HasPrevious => PageIndex > 0;
public bool HasNext => PageIndex < PageCount - 1;
public PaginatedList(int pageIndex,int pageSize,int totalItemsCount,IEnumerable<T> data)
{
PageIndex = pageIndex;
PageSize = pageSize;
TotalItemsCount = totalItemsCount;
AddRange(data);
}
}
修改Repository
public async Task<PaginatedList<Post>> GetAllPostsAsync(PostParameters postParameters)
{
var query = _applicationContext.Posts.OrderBy(x => x.Id);
var count = await query.CountAsync();
var data = await query
.Skip(postParameters.PageIndex * postParameters.PageSize)
.Take(postParameters.PageSize)
.ToListAsync();
return new PaginatedList<Post>(postParameters.PageIndex, postParameters.PageSize, count, data);
}
修改controller
[HttpGet]
public async Task<IActionResult> Get(PostParameters postParameters)
{
var postList = await _postRepository.GetAllPostsAsync(postParameters);
var postResources = _mapper.Map<IEnumerable<Post>, IEnumerable<PostResource>>(postList);
var meta = new
{
postList.PageSize,
postList.PageIndex,
postList.TotalItemsCount,
postList.PageCount
};
Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(meta, new JsonSerializerSettings
{
//使得命名符合驼峰命名法
ContractResolver = new CamelCasePropertyNamesContractResolver()
}));
return Ok(postResources);
}
OK
生成前后页的URI
- 枚举UriType
public enum PaginationResourceUriType
{
CurrentPage,
PreviousPage,
NextPage
}
- 注册UrlHelper
//注册UrlHelper
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddScoped<IUrlHelper>(factory =>
{
var actionContext = factory.GetService<IActionContextAccessor>().ActionContext;
return new IUrlHelper(actionContext);
});
- 创建CreatePostUri()方法
private string CreatePostUri(PostParameters parameters,PaginationResourceUriType uriType)
{
switch(uriType)
{
case PaginationResourceUriType.PreviousPage:
var previousParameters = new
{
pageIndex = parameters.PageIndex - 1,
pagesize = parameters.PageSize,
orderBy = parameters.OrderBy,
fields = parameters.Fields
};
return _urlHelper.Link("GetPosts", previousParameters);
case PaginationResourceUriType.NextPage:
var nextParameters = new
{
pageIndex = parameters.PageIndex + 1,
pagesize = parameters.PageSize,
orderBy = parameters.OrderBy,
fields = parameters.Fields
};
return _urlHelper.Link("GetPosts", nextParameters);
default:
var currentParameters = new
{
pageIndex = parameters.PageIndex,
pagesize = parameters.PageSize,
orderBy = parameters.OrderBy,
fields = parameters.Fields
};
return _urlHelper.Link("GetPosts", currentParameters);
}
}
- 修改Get方法
[HttpGet(Name ="GetPosts")]
public async Task<IActionResult> Get(PostParameters postParameters)
{
var postList = await _postRepository.GetAllPostsAsync(postParameters);
var postResources = _mapper.Map<IEnumerable<Post>, IEnumerable<PostResource>>(postList);
var previousPageLink = postList.HasPrevious ? CreatePostUri(postParameters, PaginationResourceUriType.PreviousPage) : null;
var nextPageLink = postList.HasNext ? CreatePostUri(postParameters, PaginationResourceUriType.NextPage) : null;
var meta = new
{
postList.PageSize,
postList.PageIndex,
postList.TotalItemsCount,
postList.PageCount,
previousPageLink,
nextPageLink
};
Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(meta, new JsonSerializerSettings
{
//使得命名符合驼峰命名法
ContractResolver = new CamelCasePropertyNamesContractResolver()
}));
return Ok(postResources);
}
成功
过滤和搜索
- 过滤: 对集合资源附加一些条件, 筛选出结果.
http://localhost:5000/api/posts?title=China
条件应用于Resource Model.
过滤属性可以放在QueryParameters的子类里.
public class PostParameters:QueryParameters
{
public string Title { get; set; }
}
修改Repository
public async Task<PaginatedList<Post>> GetAllPostsAsync(PostParameters postParameters)
{
var query = _applicationContext.Posts.AsQueryable();
if (!string.IsNullOrEmpty(postParameters.Title))
{
var title = postParameters.Title.ToLowerInvariant();
query = query.Where(x => x.Title.ToLowerInvariant()==title);
}
query = query.OrderBy(x => x.Id);
var count = await query.CountAsync();
var data = await query
.Skip(postParameters.PageIndex * postParameters.PageSize)
.Take(postParameters.PageSize)
.ToListAsync();
return new PaginatedList<Post>(postParameters.PageIndex, postParameters.PageSize, count, data);
}
success
- 搜索: 使用关键字对集合资源进行模糊搜索.
http://localhost/api/posts?searchTerm=hin
排序
- 问题
翻页需要排序.
让资源按照资源的某个属性或多个属性进行正向或反向的排序.
Resource Model的一个属性可能会映射到Entity Model的多个属性上
Resource Model上的正序可能在Entity Model上就是倒序的
需要支持多属性的排序
复用 - 安装
System.Linq.Dynamic.Core
- 排序异常返回
400
BadRequest - 排序思路
-
PropertyMappingContainer
* PropertyMapping (PostPropertyMapping)
* MappedProperty
排序
MappedProperty
public class MappedProperty
{
public string Name { get; set; }
public bool Revert { get; set; }
}
PropertyMapping
public interface IPropertyMapping
{
Dictionary<string, List<MappedProperty>> MappingDictionary { get; }
}
public abstract class PropertyMapping<TSource,TDestination>:IPropertyMapping where TDestination:IEntity
{
//可能映射多个Entity中属性,所以使用List<MappedProperty>
public Dictionary<string,List<MappedProperty>> MappingDictionary { get; }
protected PropertyMapping(Dictionary<string,List<MappedProperty>> mappingDictionary)
{
MappingDictionary = mappingDictionary;
MappingDictionary[nameof(IEntity.Id)] = new List<MappedProperty>
{
new MappedProperty{Name=nameof(IEntity.Id),Revert =false}
};
}
}
- PostPropertyMapping
public class PostPropertyMapping : PropertyMapping<PostResource, Post>
{
public PostPropertyMapping() : base(new Dictionary<string, List<MappedProperty>>(StringComparer.OrdinalIgnoreCase)
{
[nameof(PostResource.Title)] = new List<MappedProperty> { new MappedProperty { Name=nameof(Post.Title),Revert=false}},
[nameof(PostResource.Body)] = new List<MappedProperty> { new MappedProperty { Name=nameof(Post.Body),Revert=false}},
[nameof(PostResource.Author)] = new List<MappedProperty> { new MappedProperty { Name=nameof(Post.Author),Revert=false}},
})
{
}
PropertyMappingContainer
public interface IPropertyMappingContainer
{
void Register<T>() where T : IPropertyMapping, new();
IPropertyMapping Resolve<TSource, TDestination>() where TDestination : IEntity;
bool ValidateMappingExistsFor<TSource, TDestination>(string fields) where TDestination : IEntity;
}
public class PropertyMappingContainer : IPropertyMappingContainer
{
protected internal readonly IList<IPropertyMapping> PropertyMappings = new List<IPropertyMapping>();
public void Register<T>() where T : IPropertyMapping, new()
{
if (PropertyMappings.All(x=>x.GetType()!=typeof(T)))
{
PropertyMappings.Add(new T());
}
}
//注册
public IPropertyMapping Resolve<TSource, TDestination>() where TDestination : IEntity
{
var matchingMapping = PropertyMappings.OfType<PropertyMapping<TSource, TDestination>>().ToList();
if (matchingMapping.Count ==1)
{
return matchingMapping.First();
}
throw new Exception($"Cannot find property mapping instance for {typeof(TSource)},{typeof(TDestination)}");
}
//验证
public bool ValidateMappingExistsFor<TSource, TDestination>(string fields) where TDestination : IEntity
{
var propertyMapping = Resolve<TSource, TDestination>();
if (string.IsNullOrWhiteSpace(fields))
{
return false;
}
var fieldsAfterSplit = fields.Split(',');
foreach (var field in fieldsAfterSplit)
{
var trimedField = field.Trim();
var indexOfFirstSpace = trimedField.IndexOf(" ", StringComparison.Ordinal);
var propertyName = indexOfFirstSpace == -1 ? trimedField : trimedField.Remove(indexOfFirstSpace);
if (string.IsNullOrWhiteSpace(propertyName))
{
continue;
}
if (!propertyMapping.MappingDictionary.ContainsKey(propertyName))
{
return false;
}
}
return true;
}
}
注册服务
//注册排序服务
//1.新建一个容器
var propertyMappingContainer = new PropertyMappingContainer();
//2.把PostPropertyMapping注册
propertyMappingContainer.Register<PostPropertyMapping>();
//3.注册单例容器
services.AddSingleton<IPropertyMappingContainer>(propertyMappingContainer);
QueryableExtensions
public static class QueryableExtensions
{
public static IQueryable<T> ApplySort<T>(this IQueryable<T> source, string orderBy, IPropertyMapping propertyMapping)
{
if (source ==null)
throw new ArgumentNullException(nameof(source));
if (propertyMapping == null)
throw new ArgumentNullException(nameof(propertyMapping));
var mappingDictionary = propertyMapping.MappingDictionary;
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 trimedOrderByClause = orderByClause.Trim();
var orderDescending = trimedOrderByClause.EndsWith(" desc");
var indexOfFirstSpace = trimedOrderByClause.IndexOf(" ", StringComparison.Ordinal);
var propertyName = indexOfFirstSpace == -1 ? trimedOrderByClause : trimedOrderByClause.Remove(indexOfFirstSpace);
if (string.IsNullOrEmpty(propertyName))
continue;
if (!mappingDictionary.TryGetValue(propertyName,out List<MappedProperty> mappedProperties))
throw new ArgumentNullException($"Key mapping for {propertyName} is missing");
if (mappedProperties == null)
throw new ArgumentNullException(propertyName);
mappedProperties.Reverse();
foreach (var destinationProperty in mappedProperties)
{
if (destinationProperty.Revert)
{
orderDescending = !orderDescending;
}
source = source.OrderBy(destinationProperty.Name + (orderDescending ? " descending" : " ascending"));
//OrderBy =====>>>>> System.Linq.Dynamic.Core;
}
}
return source;
}
public static IQueryable<object> ToDynamicQueryable<TSource>
(this IQueryable<TSource> source, string fields, Dictionary<string, List<MappedProperty>> mappingDictionary)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
if (mappingDictionary == null)
{
throw new ArgumentNullException(nameof(mappingDictionary));
}
if (string.IsNullOrWhiteSpace(fields))
{
return (IQueryable<object>)source;
}
fields = fields.ToLower();
var fieldsAfterSplit = fields.Split(',').ToList();
if (!fieldsAfterSplit.Contains("id", StringComparer.InvariantCultureIgnoreCase))
{
fieldsAfterSplit.Add("id");
}
var selectClause = "new (";
foreach (var field in fieldsAfterSplit)
{
var propertyName = field.Trim();
if (string.IsNullOrEmpty(propertyName))
{
continue;
}
var key = mappingDictionary.Keys.SingleOrDefault(k => String.CompareOrdinal(k.ToLower(), propertyName.ToLower()) == 0);
if (string.IsNullOrEmpty(key))
{
throw new ArgumentException($"Key mapping for {propertyName} is missing");
}
var mappedProperties = mappingDictionary[key];
if (mappedProperties == null)
{
throw new ArgumentNullException(key);
}
foreach (var destinationProperty in mappedProperties)
{
selectClause += $" {destinationProperty.Name},";
}
}
selectClause = selectClause.Substring(0, selectClause.Length - 1) + ")";
return (IQueryable<object>)source.Select(selectClause);
}
}
修改Repository
public async Task<PaginatedList<Post>> GetAllPostsAsync(PostParameters postParameters)
{
var query = _applicationContext.Posts.AsQueryable();
if (!string.IsNullOrEmpty(postParameters.Title))
{
var title = postParameters.Title.ToLowerInvariant();
query = query.Where(x => x.Title.ToLowerInvariant()==title);
}
//调用排序
//query = query.OrderBy(x => x.Id);
query = query.ApplySort(postParameters.OrderBy, _propertyMappingContainer.Resolve<PostResource, Post>());
var count = await query.CountAsync();
var data = await query
.Skip(postParameters.PageIndex * postParameters.PageSize)
.Take(postParameters.PageSize)
.ToListAsync();
return new PaginatedList<Post>(postParameters.PageIndex, postParameters.PageSize, count, data);
}
排序成功
多字段排序
网友评论