美文网首页ASP .NET Core Web Api + Angular
ASP .NET Core Web API_09_ 翻页过滤排序

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

作者: xtddw | 来源:发表于2018-10-22 16:39 被阅读45次

    翻页

    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);
            }
    
    排序成功
    多字段排序

    相关文章

      网友评论

        本文标题:ASP .NET Core Web API_09_ 翻页过滤排序

        本文链接:https://www.haomeiwen.com/subject/kmugzftx.html