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<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;
        }
    }
  1. 子类继承
 public class PostParameters:QueryParameters
    {
    }
  1. 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);
        }
  1. 修改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();            
        }

返回翻页元数据

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

    *
    metadata

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

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

  2. 翻页数据不是资源表述的一部分, 应使用自定义Header (X-Pagination).
  3. 存放翻页数据的类: 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

  1. 枚举UriType
 public enum PaginationResourceUriType
    {
        CurrentPage,
        PreviousPage,
        NextPage
    }
  1. 注册UrlHelper
//注册UrlHelper
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddScoped<IUrlHelper>(factory =>
{
      var actionContext = factory.GetService<IActionContextAccessor>().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<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

排序

  1. 问题
    翻页需要排序.
    让资源按照资源的某个属性或多个属性进行正向或反向的排序.
    Resource Model的一个属性可能会映射到Entity Model的多个属性上
    Resource Model上的正序可能在Entity Model上就是倒序的
    需要支持多属性的排序
    复用
  2. 安装System.Linq.Dynamic.Core
  3. 排序异常返回400BadRequest
  4. 排序思路
  • 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);
        }
排序成功

多字段排序
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容

  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,704评论 2 59
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,638评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,867评论 25 707
  • 今天早上,突然想起:我可以拿儿子的领读机在车上上下班的路程中跟读呀,结果在来回的路程中,跟读了两遍儿子的读经内容。...
    紫彤阅读 118评论 0 1
  • 感时 文/舟亮 萋萋芳草沿河染,艳艳桃花满树燃。 新燕未归春尚远,早生华发正当年。
    舟亮阅读 162评论 0 1