Paging in ASP.NET Core 2.0 Web API






3.77/5 (9 votes)
How to implement paging in ASP.NET Core Web Api. Continue reading...
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 onPagedList
returns a POCOPagingHeader
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 code200 (OK)
. The output model contains:- Paging information that is essentially the
PagingHeader
POCO and contains properties likeTotalItems
,PageNumber
,PageSize
andTotalPages
. - 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 inStartup
. - List of movies. As discussed in the previous post (CRUD), we map the domain model to an output model (
MovieInfo
in this case).
- Paging information that is essentially the