Almost every medium to large site requires some sort of paging through lists of information. The advantage of paging is that you only need to bring back a limited result set. The user might not care about the results on the last page so only bring back the pages requested and reduce the overall imapact on your database.
I use this method with front end frameworks such as Vue, Angular, React.js. Rather than paging with numbers and using a next/previous link I prefer going with a 'load more' approach on scroll. However this backend technique can be used in both scenarios
Create a PagedList class:
public class PagedList<T> : List<T>
{
public int CurrentPage { get; private set; }
public int TotalPages { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public bool HasPrevious
{
get { return (CurrentPage > 1); }
}
public bool HasNext
{
get { return (CurrentPage < TotalPages); }
}
public PagedList(List<T> items, int count, int pageNumber, int pageSize)
{
TotalCount = count;
PageSize = pageSize;
CurrentPage = pageNumber;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
AddRange(items);
}
public static async Task<PagedList<T>> CreateAsync(
IQueryable<T> source,
int pageNumber,
int pageSize
)
{
var count = await source.CountAsync();
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedList<T>(items, count, pageNumber, pageSize);
}
}
Create a PageMetaData class. This will be sent in the API response
public class PageMetaData
{
public int CurrentPage { get; private set; }
public int TotalPages { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public int? NextPage => CurrentPage+1;
public bool HasPrevious
{
get
{
return (CurrentPage > 1);
}
}
public bool HasNext
{
get
{
return (CurrentPage < TotalPages);
}
}
}
Create a ResourceParameters class. Resource Parameters can be passed to your Api as query parameters
public class ResourceParameters
{
const int maxPageSize = 20;
public int PageNumber { get; set; } = 1;
private int _pageSize = 10;
public int PageSize
{
get { return _pageSize; }
set { _pageSize = (value > maxPageSize) ? maxPageSize : value; }
}
}
Service to query the database:
public async Task <PagedList<Post> GetPosts(ResourceParameters resourceParameters)
{
var collection = ApplicationDbContext.Posts
.OrderByDescending(x = >x.DateCreated)
.AsQueryable();
var posts = await PagedList<Post>.CreateAsync(collection, resourceParameters.PageNumber, resourceParameters.PageSize);
return posts;
}
If using AutoMapper, this PageMetaData can me mapped with a profile
public class PostProfile : Profile
{
public PostProfile()
{
CreateMap<Post, PostViewModel>();
CreateMap<PagedList<Post>, PagedList<PostViewModel>>();
CreateMap<PagedList<Post>, PageMetaData>();
}
}
An example API call with the paging properties passed as query parameters
[HttpGet()]
public async Task <IActionResult> GetPosts([FromQuery] ResourceParameters resourceParameters)
{
var postsPagedList = await _unitOfWork.Posts.GetPosts(resourceParameters);
var posts = _mapper.Map <IEnumerable<PostViewModel>> (postsPagedList);
var pageMetaData = _mapper.Map<PageMetaData>(postsPagedList);
var response = new PostListResult() {
Posts = posts,
PageMetaData = pageMetaData
};
return Ok(response);
}