ASP .NET Core Web API_09_ 翻页过滤排序

翻页

  1. Query String
    http://localhost:5000/api/posts?pageIndex=1&pageSize=10&orderBy=id
  2. 使用抽象父类 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(ref T field, T value, [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer.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;
        }
    }
  1. 子类继承
 public class PostParameters:QueryParameters
    {
    }
  1. HTTP Get 传参
 [HttpGet]
  public async Task Get(PostParameters postParameters)
        {
            var posts = await _postRepository.GetAllPostsAsync(postParameters);
            var postResources = _mapper.Map, IEnumerable>(posts);
            return Ok(postResources);
        }
  1. 修改Repositroy

        public async Task> GetAllPostsAsync(PostParameters postParameters)
        {
            var query = _applicationContext.Posts.OrderBy(x => x.Id);
            return await query
                .Skip(postParameters.PageIndex * postParameters.PageSize)
                .Take(postParameters.PageSize)
                .ToListAsync();            
        }

返回翻页元数据

  1. 如果将数据和翻页元数据一起返回:

    *
    metadata

    * 响应的body不再符合Accept Header了(不是资源的application/json), 这是一种新的media type.

    * 违反REST约束, API消费者不知道如何通过application/json这个类型来解释响应的数据.

  2. 翻页数据不是资源表述的一部分, 应使用自定义Header (X-Pagination).
  3. 存放翻页数据的类: PaginatedList可以继承于List.
 public class PaginatedList:List 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 data)
        {
            PageIndex = pageIndex;
            PageSize = pageSize;
            TotalItemsCount = totalItemsCount;
            AddRange(data);
        }
    }

修改Repository

public async Task> 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(postParameters.PageIndex, postParameters.PageSize, count, data);
 }

修改controller

[HttpGet]
public async Task Get(PostParameters postParameters)
{
            var postList = await _postRepository.GetAllPostsAsync(postParameters);
            var postResources = _mapper.Map, IEnumerable>(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);
}
ASP .NET Core Web API_09_ 翻页过滤排序_第1张图片
OK

生成前后页的URI

  1. 枚举UriType
 public enum PaginationResourceUriType
    {
        CurrentPage,
        PreviousPage,
        NextPage
    }
  1. 注册UrlHelper
//注册UrlHelper
services.AddSingleton();
services.AddScoped(factory =>
{
      var actionContext = factory.GetService().ActionContext;
      return new IUrlHelper(actionContext);
});
  1. 创建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);
            }
        }
  1. 修改Get方法
   [HttpGet(Name ="GetPosts")]
        public async Task Get(PostParameters postParameters)
        {
            var postList = await _postRepository.GetAllPostsAsync(postParameters);
            var postResources = _mapper.Map, IEnumerable>(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);
        }
ASP .NET Core Web API_09_ 翻页过滤排序_第2张图片
成功

过滤和搜索

  • 过滤: 对集合资源附加一些条件, 筛选出结果.
    http://localhost:5000/api/posts?title=China
    条件应用于Resource Model.
    过滤属性可以放在QueryParameters的子类里.
 public class PostParameters:QueryParameters
    {
        public string Title { get; set; }
    }

修改Repository

 public async Task> 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(postParameters.PageIndex, postParameters.PageSize, count, data);
        }
ASP .NET Core Web API_09_ 翻页过滤排序_第3张图片
success
  • 搜索: 使用关键字对集合资源进行模糊搜索.
    http://localhost/api/posts?searchTerm=hin

排序

  1. 问题
    翻页需要排序.
    让资源按照资源的某个属性或多个属性进行正向或反向的排序.
    Resource Model的一个属性可能会映射到Entity Model的多个属性上
    Resource Model上的正序可能在Entity Model上就是倒序的
    需要支持多属性的排序
    复用
  2. 安装System.Linq.Dynamic.Core
  3. 排序异常返回400BadRequest
  4. 排序思路
  • PropertyMappingContainer
    * PropertyMapping (PostPropertyMapping)
    * MappedProperty


    ASP .NET Core Web API_09_ 翻页过滤排序_第4张图片
    排序

MappedProperty

public class MappedProperty
    {
        public string Name { get; set; }
        public bool Revert { get; set; }
    }

PropertyMapping

 public interface IPropertyMapping
    {
        Dictionary> MappingDictionary { get; }
    }
  public abstract class PropertyMapping:IPropertyMapping where TDestination:IEntity
    {
        //可能映射多个Entity中属性,所以使用List
        public Dictionary>  MappingDictionary { get; }

        protected PropertyMapping(Dictionary> mappingDictionary)
        {
            MappingDictionary = mappingDictionary;
            MappingDictionary[nameof(IEntity.Id)] = new List
            {
                new MappedProperty{Name=nameof(IEntity.Id),Revert =false}
            };
        }
    }
  • PostPropertyMapping

    public class PostPropertyMapping : PropertyMapping
    {
        public PostPropertyMapping() : base(new Dictionary>(StringComparer.OrdinalIgnoreCase)
        {
            [nameof(PostResource.Title)] = new List { new MappedProperty { Name=nameof(Post.Title),Revert=false}},
            [nameof(PostResource.Body)] = new List { new MappedProperty { Name=nameof(Post.Body),Revert=false}},
            [nameof(PostResource.Author)] = new List { new MappedProperty { Name=nameof(Post.Author),Revert=false}},
        })
        {
        }
    

PropertyMappingContainer

public interface IPropertyMappingContainer
    {
        void Register() where T : IPropertyMapping, new();
        IPropertyMapping Resolve() where TDestination : IEntity;
        bool ValidateMappingExistsFor(string fields) where TDestination : IEntity;
    }
 public class PropertyMappingContainer : IPropertyMappingContainer
    {
        protected internal readonly IList PropertyMappings = new List();

        public void Register() where T : IPropertyMapping, new()
        {
            if (PropertyMappings.All(x=>x.GetType()!=typeof(T)))
            {
                PropertyMappings.Add(new T());
            }
        }

        //注册
        public IPropertyMapping Resolve() where TDestination : IEntity
        {
            var matchingMapping = PropertyMappings.OfType>().ToList();
            if (matchingMapping.Count ==1)
            {
                return matchingMapping.First();
            }
            throw new Exception($"Cannot find property mapping instance for {typeof(TSource)},{typeof(TDestination)}");
        }

        //验证
        public bool ValidateMappingExistsFor(string fields) where TDestination : IEntity
        {
            var propertyMapping = Resolve();
            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();
//3.注册单例容器
services.AddSingleton(propertyMappingContainer);

QueryableExtensions

public static class QueryableExtensions
    {
        public static IQueryable ApplySort(this IQueryable 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 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 ToDynamicQueryable
          (this IQueryable source, string fields, Dictionary> mappingDictionary)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            if (mappingDictionary == null)
            {
                throw new ArgumentNullException(nameof(mappingDictionary));
            }

            if (string.IsNullOrWhiteSpace(fields))
            {
                return (IQueryable)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)source.Select(selectClause);
        }
    }

修改Repository

 public async Task> 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());

            var count = await query.CountAsync();
            var data = await query 
                .Skip(postParameters.PageIndex * postParameters.PageSize)
                .Take(postParameters.PageSize)
                .ToListAsync();

            return new PaginatedList(postParameters.PageIndex, postParameters.PageSize, count, data);
        }
ASP .NET Core Web API_09_ 翻页过滤排序_第5张图片
排序成功

ASP .NET Core Web API_09_ 翻页过滤排序_第6张图片
多字段排序

你可能感兴趣的:(ASP .NET Core Web API_09_ 翻页过滤排序)