Recently I had the challenge of paginating a web application I wrote as the tables displaying data were getting quite long and I needed a way to display things more cleanly. This post details how I solved the problem using C# Generics. It includes plenty of code snippets so you can follow along in your own application.

The code shown in this post was written for my client Universal Layer and is released by them under a BSD 3-Clause License.

What is pagination?

Pagination is a process of taking a collection of objects and putting them into pages. For example you might have a book which contains hundreds of pages but only fifty words per page. You cannot put an entire book on a single piece of paper and you should not attempt to do the same in computer software. Rather you should put a set amount of words on each page, create a list of the pages, and have a way to easily switch between pages. In the human world this works by binding the pages of a book together, pagination in computer software works similarly by binding the pages of data together into an easy to use object.

How to paginate in computer software

Pagination requires three pieces of information: You need a collection of data to paginate, the number of results per page (a limiter), and to get the correct data you need to request a specific page. Object-oriented languages such as C# make this easy. You can pass the data to an object's constructor and have it run the calculations on your behalf and make the results available as read only properties of the object.

The properties generated by the constructor are as follows:

  • Item Count: Number of items in our collection. When passing an IQueryable this means the number of rows in a database table.
  • Page Count: When you divide the number of items by the number of results per page you get the Page Count or number of pages.
  • Skip: Number of items to skip in our SQL query
  • Take: Number of items to select in our SQL query
  • The page of the selected results
  • Number of First Page
  • Number of Last Page
  • Number of Next Page
  • Number of Current Page
  • Number of Previous Page

Security Considerations for User Configurable Pagination

Some developers may allow users to choose the number of results per page in a table, api, etc. Be sure to set a reasonable maximum number of results per page and enforce this on your backend code. Failure to add a safe maximum limit could result in large queries that overwhelm the database server and result in a denial of service vulnerability.

Solution

I decided that the best way to go about solving this problem was to pass the necessary data to the constructor and have the constructor do all of the math and fill in prosperities.

From there getting data from the object’s properties is much easier than calculating on each controller and results in shorter code.

The final result of my efforts was a generic class, the code for it is below this paragraph.

PagedResults.cs

// This code is released by Universal Layer under a BSD 3-Clause License
// https://github.com/ulayer/PagedResults.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace Your.Application.Models
{
    public class PagedResults<T>
    {
        public int ItemCount { get; }
        public int PageCount { get; }
        
        public int Skip { get; }
        public int Take { get; }
        
        public IEnumerable<T> PageOfResults { get; }
        
        public int FirstPage { get; }
        public int LastPage { get; }
        
        public int NextPage { get; }
        public int CurrentPage { get; }
        public int PreviousPage { get; }

        public PagedResults(IQueryable<T> results, int pageNumber, int resultsPerPage)
        {
            ItemCount = results.Count();
            PageCount = (int) Math.Ceiling((double) ItemCount / resultsPerPage);
            
            Skip = (pageNumber - 1) * resultsPerPage;
            Take = resultsPerPage;
            
            PageOfResults = results.Skip(Skip).Take(Take).ToList();
            
            FirstPage = 1;
            LastPage = LastPage = PageCount == 0 ? 1 : PageCount;
            
            NextPage = Math.Min(pageNumber + 1, LastPage);
            CurrentPage = pageNumber;
            PreviousPage = Math.Max(pageNumber - 1, FirstPage);
        }
    }
}

I also worked on a few examples for this post of how you could take advantage of this class. I hope you find them useful when integrating this class into your applications.

Within a service

It is a well accepted design pattern to call EF Core from within a service. It's still possible to get an IQueryable by injecting EF Core's Context into your page but you shouldn't do this when services exist. By asking for data from a database through a service you keep your Razorpages code-behind area more organized. If you have to call several methods on your EF Core Context to get back the desired data it does not clutter your code-behind area.

Where initially we created a generic class we can now use it as a PagedResults<Customer> class to paginate data from the Customer class. The Customer class can come from anywhere, the important thing is that the collection of Customers is passed to our PagedResults<Customer> class as an IQueryable<T> (EF Core does this for you). The helpful thing about using a generic class is that we can change the type as we need paged results for new data types without having to code an additional PagedResults class to handle that new data type.

public PagedResults<Customer> GetPagedResults(int pageNumber, int resultsPerPage)
        {
            return new PagedResults<Customer>
            (_Context.Customers,
                pageNumber,
                resultsPerPage);
        }

Passing the results to a RazorPages Partial View

I came up with the following partial view for use in Razorpages. It requires that the Model of the calling Razorpages have a property called PagedResults. The partial views calls the model for data dynamically so if the data doesn't exist in the expected model your program will throw an exception. As long as PagedResults exists in the page's model you can inject the pagination anywhere in your view as <partial name="Shared/_Pagination" />.

It's important to note that this is a partial view not a full razorpage. To simplify the file Shared/_Pagination.cshtml exists but not Shared/_Pagination.cshtml.cs.

<nav aria-label="Page navigation example">
    <ul class="pagination">
        @if (@Model.PagedResults.CurrentPage != 1) // Show a link to the first page as well as previous page as long as we are not on the first page.
        {
            <li class="page-item"><a href="./@Model.PagedResults.FirstPage" class="page-link">First</a></li>
            <li class="page-item">
                <a href="./@Model.PagedResults.PreviousPage" class="page-link">
                    <span aria-hidden="true">&laquo;</span>
                    <span class="sr-only">Previous</span>
                </a></li>
        }
        
        @{ var pageCount = @Model.PagedResults.PageCount; }
        
        @for (int i = 1; i <= pageCount && i < 10; i++)
        {
            var currentPage = @Model.PagedResults.CurrentPage;
            if (pageCount > 10)
            {
                var activePage = ((currentPage - 5) + i);
                var active = activePage == currentPage ? "active" : string.Empty;
                if (activePage <= (pageCount - 1) && (activePage > 0))
                {
                    <li class="page-item @active"><a href="./@activePage" class="page-link">@activePage</a></li>
                }
            }
            else
            {
                var active = i == currentPage ? "active" : string.Empty;
                <li class="page-item @active"><a href="./@i" class="page-link">@i</a></li>
            }
        }
        
        
        
        @if (@Model.PagedResults.CurrentPage != @Model.PagedResults.LastPage)
        {
            <li class="page-item"><a href="./@Model.PagedResults.NextPage" class="page-link">
                <span aria-hidden="true">&raquo;</span>
                <span class="sr-only">Next</span>
            </a></li>
            <li class="page-item"><a href="./@Model.PagedResults.LastPage" class="page-link">Last</a></li>
        }
    </ul>
</nav>

References