参照 草根专栏- ASP.NET Core + Ng6 实战:https://v.qq.com/x/page/v07647j3zkq.html
翻页, 过滤, 排序等 – 如何传递参数?
Query String
- http://localhost:5000/api/country?pageIndex=12&pageSize=10&orderBy=id
使用抽象父类 QueryParameters, 包含常见参数:
- PageIndex, PageSize, OrderBy …
一、翻页:
1、在Core.Entity 中添加 QueryParameters.cs 类
namespace BlogDemo.Core.Entities { public abstract class QueryParameters : INotifyPropertyChanged { private const int DefaultPageSize = 10; private const int DefaultMaxPageSize = 100; private int _pageIndex; public int PageIndex { get => _pageIndex; set => _pageIndex = value >= 0 ? value : 0; } private int _pageSize = DefaultPageSize; public virtual int PageSize { get => _pageSize; set => SetField(ref _pageSize, value); } private string _orderBy; public string OrderBy { get => _orderBy; set => _orderBy = value ?? nameof(IEntity.Id); } private int _maxPageSize = DefaultMaxPageSize; protected internal virtual int MaxPageSize { get => _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; } } }
2、在BlogDemo.Core.Entities 中添加 PostParameters.cs 类
namespace BlogDemo.Core.Entities { public class PostParameters:QueryParameters { public string Title { get; set; } } }
3、 修改 BlogDemo.Infrastructure.Repositories 文件夹 的 PostRepository类 中的 方法
public async Task<PaginatedList<Post>> GetPostsAsync(PostParameters parameters) { var Query = _myContext.Posts.AsQueryable(); Query = Query.OrderBy(x => x.Id); var count = await Query.CountAsync(); var data = await Query.Skip(parameters.PageIndex * parameters.PageSize).Take(parameters.PageSize).ToListAsync(); return new PaginatedList<Post>(parameters.PageIndex, parameters.PageSize,count,data); }
4、修改Controller中的Action
public async Task<PaginatedList<Post>> GetPostsAsync(PostParameters parameters) { var Query = _myContext.Posts.AsQueryable(); Query = Query.OrderBy(x => x.Id); var count = await Query.CountAsync(); var data = await Query.Skip(parameters.PageIndex * parameters.PageSize).Take(parameters.PageSize).ToListAsync(); return new PaginatedList<Post>(parameters.PageIndex, parameters.PageSize,count,data); }
二、返回翻页元数据
- 如果将数据和翻页元数据一起返回:
响应的body不再符合Accept Header了(不是资源的application/json), 这是一种新的media type.
违反REST约束, API消费者不知道如何通过application/json这个类型来解释响应的数据.
- 翻页数据不是资源表述的一部分, 应使用自定义Header (“X-Pagination”).
- 存放翻页数据的类: PaginatedList<T>可以继承于List<T>.
1、添加存放翻页数据的类:PaginatedList<T>可以继承于List<T>:
namespace BlogDemo.Core.Entities { 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 => _totalItemsCount; set => _totalItemsCount = value >= 0 ? value : 0; } 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); } } }
2、修改PostRepository..cs 中的Get方法
public async Task<PaginatedList<Post>> GetPostsAsync(PostParameters parameters) { var Query = _myContext.Posts.AsQueryable(); Query = Query.OrderBy(x => x.Id); var count = await Query.CountAsync(); var data = await Query.Skip(parameters.PageIndex * parameters.PageSize).Take(parameters.PageSize).ToListAsync(); return new PaginatedList<Post>(parameters.PageIndex, parameters.PageSize,count,data); }
3、修改Controller中的Action
public async Task<IActionResult> Get(PostParameters parameters) { var posts = await _postRepository.GetPostsAsync(parameters); var postDto=_mapper.Map<IEnumerable<Post>,IEnumerable<PostDTO>>(posts); var meta = new { PageSize = posts.PageSize, PageIndex = posts.PageIndex, TotalItemCount = posts.TotalItemsCount, PageCount = posts.PageCount, }; Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(meta, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(postDto); }
4、PostMan测试
三、生成前后页的URI
1、ConfiguraServices注册IUrlHelper,IActionContextAccessor
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>(); services.AddScoped<IUrlHelper>(factory => { var actionContext = factory.GetService<IActionContextAccessor>().ActionContext; return new UrlHelper(actionContext); });
2、Controller 编写方法返回URL
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); } }
[HttpGet(Name = "GetPosts")]
public async Task<IActionResult> Get(PostParameters parameters) { var posts = await _postRepository.GetPostsAsync(parameters); var postDto=_mapper.Map<IEnumerable<Post>,IEnumerable<PostDTO>>(posts); var previousPageLink = posts.HasPrevious ? CreatePostUri(parameters, PaginationResourceUriType.PreviousPage) : null; var nextPageLink = posts.HasNext ? CreatePostUri(parameters, PaginationResourceUriType.NextPage) : null; var meta = new { PageSize = posts.PageSize, PageIndex = posts.PageIndex, TotalItemCount = posts.TotalItemsCount, PageCount = posts.PageCount, previousPageLink, nextPageLink }; Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(meta, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(postDto); }
3、Postman测试
四、 过滤和搜索
过滤: 对集合资源附加一些条件, 筛选出结果.
1、 在PostParameters.cs类中,添加过滤字段;
public class PostParameters:QueryParameters { public string Title { get; set; } }
2、修改 PostRepository.cs 中的方法:
public async Task<PaginatedList<Post>> GetPostsAsync(PostParameters parameters) { var Query = _myContext.Posts.AsQueryable(); if (!string.IsNullOrEmpty(parameters.Title)) { var title = parameters.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(parameters.PageIndex * parameters.PageSize).Take(parameters.PageSize).ToListAsync(); return new PaginatedList<Post>(parameters.PageIndex, parameters.PageSize,count,data); }
3、Postman测试
五、排序
- 翻页需要排序.
- 让资源按照资源的某个属性或多个属性进行正向或反向的排序.
- Resource Model的一个属性可能会映射到Entity Model的多个属性上
- Resource Model上的正序可能在Entity Model上就是倒序的
- 需要支持多属性的排序
- 复用
1、在 BlogDemo.Infrastructure nuget包 添加 System.Linq.Dynamic.Core
2、添加映射属性、属性映射、容器等类;
namespace BlogDemo.Infrastructure.Services { public class MappedProperty { public string Name { get; set; } public bool Revert { get; set; } } }
public abstract class PropertyMapping<TSource, TDestination> : IPropertyMapping where TDestination : IEntity { 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} }; } }
namespace BlogDemo.Infrastructure.Services { public interface IPropertyMapping { Dictionary<string, List<MappedProperty>> MappingDictionary { get; } } }
namespace BlogDemo.Infrastructure.Services { 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; } }
namespace BlogDemo.Infrastructure.Services { 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 true; } var fieldsAfterSplit = fields.Split(','); foreach (var field in fieldsAfterSplit) { var trimmedField = field.Trim(); var indexOfFirstSpace = trimmedField.IndexOf(" ", StringComparison.Ordinal); var propertyName = indexOfFirstSpace == -1 ? trimmedField : trimmedField.Remove(indexOfFirstSpace); if (string.IsNullOrWhiteSpace(propertyName)) { continue; } if (!propertyMapping.MappingDictionary.ContainsKey(propertyName)) { return false; } } return true; } } }
namespace BlogDemo.Infrastructure.Extensions { 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 trimmedOrderByClause = orderByClause.Trim(); var orderDescending = trimmedOrderByClause.EndsWith(" desc"); var indexOfFirstSpace = trimmedOrderByClause.IndexOf(" ", StringComparison.Ordinal); var propertyName = indexOfFirstSpace == -1 ? trimmedOrderByClause : trimmedOrderByClause.Remove(indexOfFirstSpace); if (string.IsNullOrEmpty(propertyName)) { continue; } if (!mappingDictionary.TryGetValue(propertyName, out List<MappedProperty> mappedProperties)) { throw new ArgumentException($"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")); } } 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); } } }
3、在ConfigureServices中注入
public void ConfigureServices(IServiceCollection services) { //排序 var propertyMappingContainer = new PropertyMappingContainer(); propertyMappingContainer.Register<PostPropertyMapping>(); services.AddSingleton<IPropertyMappingContainer>(propertyMappingContainer); }
4、修改 PostRepository.cs 中的方法:
public async Task<PaginatedList<Post>> GetPostsAsync(PostParameters parameters) { var Query = _myContext.Posts.AsQueryable(); if (!string.IsNullOrEmpty(parameters.Title)) { var title = parameters.Title.ToLowerInvariant(); Query = Query.Where(x => x.Title.ToLowerInvariant()==title); } Query = Query.ApplySort(parameters.OrderBy, _propertyMappingContainer.Resolve<PostDTO, Post>()); var count = await Query.CountAsync(); var data = await Query.Skip(parameters.PageIndex * parameters.PageSize).Take(parameters.PageSize).ToListAsync(); return new PaginatedList<Post>(parameters.PageIndex, parameters.PageSize,count,data); }