Problem
How to implement paging in ASP.NET Core Web API.
Solution
In an empty project, update the Startup class to add services and middleware for MVC.
- public void ConfigureServices(
- IServiceCollection services)
- {
- services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
- services.AddScoped<IUrlHelper>(factory =>
- {
- var actionContext = factory.GetService<IActionContextAccessor>()
- .ActionContext;
- return new UrlHelper(actionContext);
- });
-
- services.AddSingleton<IMovieService, MovieService>();
-
- services.AddMvc();
- }
-
- public void Configure(
- IApplicationBuilder app,
- IHostingEnvironment env)
- {
- app.UseDeveloperExceptionPage();
- app.UseMvcWithDefaultRoute();
- }
Add models to hold link and paging data.
- public class PagingParams
- {
- public int PageNumber { get; set; } = 1;
- public int PageSize { get; set; } = 5;
- }
-
- public class LinkInfo
- {
- public string Href { get; set; }
- public string Rel { get; set; }
- public string Method { get; set; }
- }
- public class PagingHeader
- {
- public PagingHeader(
- int totalItems, int pageNumber, int pageSize, int totalPages)
- {
- this.TotalItems = totalItems;
- this.PageNumber = pageNumber;
- this.PageSize = pageSize;
- this.TotalPages = totalPages;
- }
-
- public int TotalItems { get; }
- public int PageNumber { get; }
- public int PageSize { get; }
- public int TotalPages { get; }
-
- public string ToJson() => JsonConvert.SerializeObject(this,
- new JsonSerializerSettings {
- ContractResolver = new
- CamelCasePropertyNamesContractResolver() });
-
- }
Create a type to hold the paged list.
- public class PagedList<T>
- {
- public PagedList(IQueryable<T> source, int pageNumber, int pageSize)
- {
- this.TotalItems = source.Count();
- this.PageNumber = pageNumber;
- this.PageSize = pageSize;
- this.List = source
- .Skip(pageSize * (pageNumber - 1))
- .Take(pageSize)
- .ToList();
- }
-
- public int TotalItems { get; }
- public int PageNumber { get; }
- public int PageSize { get; }
- public List<T> List { get; }
- public int TotalPages =>
- (int)Math.Ceiling(this.TotalItems / (double)this.PageSize);
- public bool HasPreviousPage => this.PageNumber > 1;
- public bool HasNextPage => this.PageNumber < this.TotalPages;
- public int NextPageNumber =>
- this.HasNextPage ? this.PageNumber + 1 : this.TotalPages;
- public int PreviousPageNumber =>
- this.HasPreviousPage ? this.PageNumber - 1 : 1;
-
- public PagingHeader GetHeader()
- {
- return new PagingHeader(
- this.TotalItems, this.PageNumber,
- this.PageSize, this.TotalPages);
- }
- }
Add a service and domain model.
- public interface IMovieService
- {
- PagedList<Movie> GetMovies(PagingParams pagingParams);
- }
-
- public class MovieService : IMovieService
- {
- public PagedList<Movie> GetMovies(PagingParams pagingParams)
- {
- var query = this.movies.AsQueryable();
- return new PagedList<Movie>(
- query, pagingParams.PageNumber, pagingParams.PageSize);
- }
- }
-
- public class Movie
- {
- public int Id { get; set; }
- public string Title { get; set; }
- public int ReleaseYear { get; set; }
- public string Summary { get; set; }
- }
Add output models (to send data via API).
- public class MovieOutputModel
- {
- public PagingHeader Paging { get; set; }
- public List<LinkInfo> Links { get; set; }
- public List<MovieInfo> Items { get; set; }
- }
-
- public class MovieInfo
- {
- public int Id { get; set; }
- public string Title { get; set; }
- public int ReleaseYear { get; set; }
- public string Summary { get; set; }
- public DateTime LastReadAt { get; set; }
- }
Add a controller for the API with service injected via constructor.
- [Route("movies")]
- public class MoviesController : Controller
- {
- private readonly IMovieService service;
- private readonly IUrlHelper urlHelper;
-
- public MoviesController(IMovieService service, IUrlHelper urlHelper)
- {
- this.service = service;
- this.urlHelper = urlHelper;
- }
-
- [HttpGet(Name = "GetMovies")]
- public IActionResult Get(PagingParams pagingParams)
- {
- var model = service.GetMovies(pagingParams);
-
- Response.Headers.Add("X-Pagination", model.GetHeader().ToJson());
-
- var outputModel = new MovieOutputModel
- {
- Paging = model.GetHeader(),
- Links = GetLinks(model),
- Items = model.List.Select(m => ToMovieInfo(m)).ToList(),
- };
- return Ok(outputModel);
- }
-
- private List<LinkInfo> GetLinks(PagedList<Movie> list)
- {
- var links = new List<LinkInfo>();
-
- if (list.HasPreviousPage)
- links.Add(CreateLink("GetMovies", list.PreviousPageNumber,
- list.PageSize, "previousPage", "GET"));
-
- links.Add(CreateLink("GetMovies", list.PageNumber,
- list.PageSize, "self", "GET"));
-
- if (list.HasNextPage)
- links.Add(CreateLink("GetMovies", list.NextPageNumber,
- list.PageSize, "nextPage", "GET"));
-
- return links;
- }
-
- private LinkInfo CreateLink(
- string routeName, int pageNumber, int pageSize,
- string rel, string method)
- {
- return new LinkInfo
- {
- Href = urlHelper.Link(routeName,
- new { PageNumber = pageNumber, PageSize = pageSize }),
- Rel = rel,
- Method = method
- };
- }
- }
Output
Discussion
Let’s walk through the sample code step-by-step.
- Paging information, i.e., page number and page size, is usually received via query parameters. The POCO PagingParams simply holds this information and passes it to service (or repository).
- Service will then wrap the results (a list) in another custom type PagedList so that it can hold the paging metadata along with the original list. GetHeader() method on PagedList returns a POCO PagingHeader which is used later to populate X-Pagination
- Back in the controller, we add the pagination header to HTTP response. This header can be read by the client and looks like.
- We build our output model MovieOutputModel and return status code 200 (OK). The output model contains,
- Paging information that is essentially the PagingHeader POCO and contains properties like TotalItems, PageNumber, PageSize, and TotalPages.
- Links to the current, next and previous pages. These are created with the help of framework provided IUrlHelper interface which was registered in the service container in Startup.
- List of movies. As discussed in the previous post (CRUD), we mapped the domain model to an output model (MovieInfo in this case).
Source Code
GitHub