Fork me on GitHub

ASP NET Core --- HTTP 翻页、过滤、排序

参照 草根专栏- 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;
        }
    }
}
View Code

 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);
        }
    }
}
View Code

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);
            }
        }
View Code
        [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;
        }
    }
}
View Code
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);
        }

    }
}
View Code

 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);
        }
posted @ 2018-09-05 15:45  精进的小陈  阅读(712)  评论(0编辑  收藏  举报