ASP .NET Core Web Api + Angular

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

2018-10-22  本文已影响45人  xtddw

翻页

  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);
        }
成功

过滤和搜索

 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

排序

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

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}
            };
        }
    }

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

猜你喜欢

热点阅读