Problem
How to implement paging in ASP.NET Core Web API.
Solution
In an empty project, update 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 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 hold this information and pass to service (or repository). - Service will then wrap the results (a list) in another custom type
PagedList
, so that it can hold 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 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 map the domain model to an output model (
MovieInfo
in this case).