Click here to Skip to main content
15,121,505 members
Articles / Web Development / ASP.NET
Technical Blog
Posted 31 Aug 2017

Tagged as

Stats

32.2K views
13 bookmarked

Paging in ASP.NET Core 2.0 Web API

Rate me:
Please Sign up or sign in to vote.
3.77/5 (9 votes)
31 Aug 2017CPOL1 min read
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:

C#
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:

C#
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:

C#
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:

C#
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):

C#
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:

C#
[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:

Image 1

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:

Image 2

  • 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).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author


Comments and Discussions

 
QuestionPagingParams Pin
Member 1117365224-Jan-18 18:23
MemberMember 1117365224-Jan-18 18:23 
GeneralMy vote of 5 Pin
E. Scott McFadden1-Sep-17 10:12
professionalE. Scott McFadden1-Sep-17 10:12 
Once again, you have a solid article that is very useful.
GeneralRe: My vote of 5 Pin
User 10432641-Sep-17 12:27
MemberUser 10432641-Sep-17 12:27 
QuestionWhy TotalPages is required Pin
Mou_kol31-Aug-17 23:56
MemberMou_kol31-Aug-17 23:56 
AnswerRe: Why TotalPages is required Pin
User 10432641-Sep-17 0:41
MemberUser 10432641-Sep-17 0:41 
GeneralRe: Why TotalPages is required Pin
Mou_kol1-Sep-17 4:23
MemberMou_kol1-Sep-17 4:23 
GeneralRe: Why TotalPages is required Pin
User 10432641-Sep-17 4:25
MemberUser 10432641-Sep-17 4:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.