Implementing Server-Side Pagination using ASP.NET Core API and React.js 3

In this article, we will perform server-side pagination using ASP.NET Core API, Entity Framework Core, and the React.js front-end. We should choose the option of server-side pagination when the server returns a large amount of data. The pagination makes sure that the front end is provided with a limited set of data so that the response to the client application is as lightweight as possible. To implement the code for this article I have used the Northwind database. Scrips for the Northwind can be downloaded from this link.   

Figure 1 will provide an idea of the implementation

 


Figure 1: The Application Implementation  

For fetching a specific number of records from the table, we can use LINQ with Skip() and Take() methods. I have used the LINQ with Entity Framework Core to implement the server-side pagination.  

Once the Northwind database is created, we can see various tables in it. Some of the tables contain Null values. So I have used the query as shown in Listing 1 to create a new table based on the Employees, Customers, and Shippers table. This new table will be named as CustomersEmployeesShippers. This table will contain 830 records. (Sufficient for the example discussed in this article). Note that you can use any other SQL Server database.


Select OrderID, Customers.ContactName as CustomerName, Employees.FirstName + ' ' + Employees.LastName as EmployeeName, 
OrderDate, RequiredDate, ShippedDate, Shippers.CompanyName as ShipperName, 
Freight, ShipName, ShipAddress, ShipCity,ShipPostalCode, ShipCountry into CustomersEmployeesShippers
from Orders, Customers, Employees, Shippers
where
Customers.CustomerID=Orders.CustomerID and Employees.EmployeeID =Orders.EmployeeID and Shippers.ShipperID=Orders.ShipVia

Listing 1: The Query

Note, I have implemented the code for this article using .NET 6 and Visual Studio 2022 for Mac and Visual Studio Code (VS Code).

Step 1: Open Visual Studio and Create a new ASP.NET Cre API application, name it API_ServerPagination. In this project, add the following packages to use the Entity Framework Core Database First approach to generate Entity class. 

Microsoft.EntityFrameworkCore

Microsoft.EntityFrameworkCore.SqlServer

Microsoft.EntityFrameworkCore.Design

Microsoft.EntityFrameworkCore.Tools

Step 2: Open the Command Prompt (Oer Terminal Window) and enter the command shown in Listing 2 to generate Entity and DbContext class from the database

dotnet ef dbcontext scaffold "Data Source=127.0.0.1;Initial Catalog=Northwind;
User Id=sa;Password=MyPass@word;MultipleActiveResultSets=true" 
Microsoft.EntityFrameworkCore.SqlServer -o Models -t CustomersEmployeesShippers

Listing 2: The command to generate the Entity from the Database

Once the command is executed successfully, the Models folder will be added to the project. This folder is now having NorthwindContext.cs and CustomersEmployeesShipper.cs files. Move the connection string from the OnConfiguring() method of the NorthwindContext class from the NorthwindContext.cs file to the appsettings.json file as shown in Listing 3


 "ConnectionStrings": {
    "NorthwindConnString": "Data Source=127.0.0.1;Initial Catalog=Northwind;User Id=sa;Password=MyPass@word;MultipleActiveResultSets=true"
  }

Listing 3: The connection string in the appsettings.json file

In the Models folder, add a new class file and name it ResponseObject.cs. In this file, we will add the ResponseObject class. We will use this class to send the response to the client application. The code of the class is shown in Listing 4


 public class ResponseObject
 {
        public long TotalRecords { get; set; }
        public List<CustomersEmployeesShipper> CustomersEmployeesShipper { get; set; } 
        	= new List<CustomersEmployeesShipper>();
 }

Listing 4: The ResponseObject class

Step 3:   Let's register the CORS Service and DbContext in Dependency Container by modifying the Program.cs as shown in Listing 5         


builder.Services.AddDbContext<NorthwindContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString
    ("NorthwindConnString"));
});
builder.Services.AddCors(options =>
{
    options.AddPolicy("cors", policy =>
    {
        policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin();
    });
});
builder.Services.AddControllers().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.PropertyNamingPolicy = null;
});

Listing 5: The Registration of the CORS and DbContext in Dependency Container

We need the Cross-Origin-Resource-Sharing (CORS) service because we will be consuming the API using the React.js Front-End application. We are also making sure that the JSON response from the API will not use the Camel-Casing in JSON responses instead we want the Pascal casing response thats why we are setting the PropertyNameingPolicy to null.

We need to configure the CORS middleware in the Program.cs as shown in Listing 6


.....
app.UseCors("cors");
.....

Listing 6: The CORS Middleware

Step 4: Let's add a new empty API Controller in the Controllers folder and name it as SearchControll.cs. In this controller, we will inject the NorthwindContext class using the constructor injection.  Add the code in this controller as shown in Listing 7


using Microsoft.AspNetCore.Mvc;

namespace API_ServerPagination.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class SearchController : ControllerBase
    {
        NorthwindContext ctx;
        public SearchController(NorthwindContext ctx)
        {
            this.ctx = ctx;
        }

        [HttpGet("{top}/{skip}")]
        public async Task<IActionResult> Get(int? top = 5, int? skip = 0)
        {
            ResponseObject response = new ResponseObject();
            response.TotalRecords = (await ctx.CustomersEmployeesShippers.ToListAsync()).Count;
            if (top == 0 && skip == 0)
            {
                response.CustomersEmployeesShipper = await ctx.CustomersEmployeesShippers.ToListAsync();
            }

            if (top > 0 && skip == 0)
            {
                response.CustomersEmployeesShipper = await ctx.CustomersEmployeesShippers
                                         .Take(Convert.ToInt32(top))
                           .ToListAsync<CustomersEmployeesShipper>();
            }
            if (top > 0 && skip > 0)
            {
                response.CustomersEmployeesShipper = await ctx.CustomersEmployeesShippers
                                         .Skip(Convert.ToInt32(skip))
                                         .Take(Convert.ToInt32(top))
                           .ToListAsync<CustomersEmployeesShipper>();
            }

            return Ok(response);
        }


        
    }
}

Listing 7: The Controller Code

Have a careful look at the Get() method, this method accepts the top and skip parameters with default values are 5 and 0 respectively. The method gets the total count of records and then based on the top and skip parameter values, the records will be read from the table. The method saves the total record count and received records from the table in the Response Object which will be sent back to the client app using the HTTP response from the method.     

Test the API using Swagger and make sure that by default top 5 records are returned along with the TotalRecodrs as shown in Figure 2



Figure 2: The API Response

Let's create a React.js Front-End Application. We will use the create-react-app, the React CLI to create a React project. 

Step 5: Install create-react-app in the global scope from the command prompt as shown in Listing 8

npm install --global create-react-app

Listing 8: Install React CLI

Create the React Application using the command shown in Listing 9

create-react-app my-react-app

Listing 9: Create React App

This will create react application. In this application, install Bootstrap and axios packages which we will be using for the CSS Styling and the HTTP calls from the React app respectively. The command is shown in Listing 10

npm install --save bootstrap axios 

Listing 10: Installing packages  

Step 6: Open the React project in Visual Studio code. In the src folder of the React application add a new folder and name it as services. In this folder add a new JavaScript file and name it httpservice.js.  In this file, we will add a code for creating HttpService class that will make the call to the REST API which we have created in earlier steps. This class uses the axios object to access REST API. The class code is shown in Listing 11


import axios from 'axios';

export default class HttpService {
    constructor(){
        this.url = 'https://localhost:7278/api/Search';
    }

    async getData(top,skip){
        var records = await axios.get(`${this.url}/${top}/${skip}`);
        return records;
    }
}

Listing 11: The HttpService class

Step 7: In the React app in the src folder, add a new folder and name it utilities. In this folder will add a JavaScript file named pagination.js. In this file, we will add the Pagination class which contains logic for generating page numbers. The code of the class is shown in Listing 12 


export default class Pagination {
    constructor(){
        this.CurrentPage = 1;
    }

    pageCount(recCount, recPerPage){
        return parseInt(recCount/recPerPage); 
    }
}

Listing 12: The Pagination class

Step 8: Let's add a new folder named models in the src folder of the React application. In this folder, we will add the CustomersEmployeesShippers.js file that contains CustomersEmployeesShippers class containing properties same as the CustomersEmployeesShippers entity class which we have generated using the Entity Framework Core and its database first approach. The code for CustomersEmployeesShippers is shown in Listing 13


export default class CustomersEmployeesShippers {
    constructor(){
        this.OrderId=0;
        this.CustomerName= "";
        this.EmployeeName= "";
        this.OrderDate="";
        this.RequiredDate= "";
        this.ShippedDate="";
        this.ShipperName= "";
        this.Freight=0;
        this.ShipName= "";
        this.ShipAddress= "";
        this.ShipCity="";
        this.ShipPostalCode="";
        this.ShipCountry="";
    }
}

Listing 13: The CustomersEmployeesShippers class

In the same folder, we will add a new JavaScript file named ResponseObject.js. In this file, we will add code for the ResponseObject class that contains properties for TotalRecords received from REST API and the records received after pagination. The code for the ResponseObject class is shown in Listing 14


export default class ResponseObject {
    constructor(){
        this.TotalRecords = 0;
        this.CustomersEmployeesShipper = [];
    }
}

Listing 14: The ResponseObject class

Step 9: In the React app add a new folder named components in the src folder. In this folder, add a new file named pagination.css. In this file, we will add CSS classes that we will be using for the PaginationComponent which we will be creating in the next steps. The code for CSS classes is shown in Listing 15


.headers {
    position:sticky;
    top:0;
}
.tableDiv {
    max-width: 2800px; 
    max-height: 650px;
}
.divPage{
    overflow-y: auto;
    overflow-x: auto;
    max-width:1800px; 
    max-height: 600px;
}

Listing 15: The CSS class

Step 10: In the components folder, add a new file named paginationcomponent.jsx. In this file, we will add code for creating UI for pagination and logic to implement the pagination. Listing 16 shows the state properties used for storing fetched records, page numbers, page size, and the total number of records fetched from the REST API.


// State property for storing received records from API
    let [fetchedRecords, setRecords] = useState([]);
    // array for Page Size
    const recordsSize = [1,5,10,15,20,25,30,35,40,45,50,100,150,200,250,300,350,400,450,500,550,600,650,700,750,800,850,900];
    // Selecting Page size the default value is 5 records
    const [selectedRecordSizePerPage,setRecSizePerPage] = useState(5);
    // State Property for Table Headers
    let [headers,setHeaders] = useState([]);
    // Instance of CustomersEmployeesShippers to read its properties
    const modelInstance=new CustomersEmployeesShippers()
    // Count of Records received from the Server
    let [totalRecordLengthFromServer, setRecordLength] = useState(0);
    // the paging state property that will be used to generate page numbers
    let [paging,setPaging] = useState([]);
    // The service class instance
    const serv = new HttpService();
    // the Response object
    let responseObject = new ResponseObject();

Listing 16: State Properties

Listing 17 shows all state properties used by the PaginationComponent. These properties will be used to store records fetched from the API, the total number of records received from the server, pagination, etc. 

We will also add a function to generate page numbers that will be used to render pagination links so that end users can use them to load new pages with data. Listing 18 shows the code for generating page numbers


 // Method to generate page numbers. We will call this 
    // when the component is loaded and also the change event for the
    // select element to select No. of Records Per Page  and also the click event
    // on the page numbers
    const generatePageNumbers=(records,selectedpgSize=5)=>{
       
        let pagination = new Pagination();
        // create page numbers so that we can show them at the bottom of the table
        let pages = pagination.pageCount(records,selectedpgSize);
            let pageNumbers = [];
            // the pagination generation
            for (let index = 0; index <= pages; index++) {
                pageNumbers.push({
                    pageId:index 
                    });
            }    
            
            return pageNumbers;
        }

Listing 17: The generatePageNumbers function

The generatePageNumbers() method accepts records to show on the page and the page size. This function calls the pageCount() function of the pagination class. The pageCount() method is actually used to generate the pagination numbers. The generatePageNumbers() function defines an array named pageNumbers, this array is an array of objects which has the property as pageId. This property represents the page number that can be selected to show records on UI.

The component uses the useEffect() hook to call the created() function which calls the getData() function from the HttpService call which further calls the API and receives records from the API. The getData() method returns ReponseObject which contains the total count of records available on the server and the first 5 records. Values from the response object will be passed to setRecords(), setPaging(), and setRecordLength() state methods so that fetchRecords, paging, and totalRecordLengthFromServer state properties. The code in Listing 18 shows the code used in the useEffct() hook.

 // Get the Total Number of Records present on the server
    // get the 5 records
    useEffect(()=>{
         async function created(){
            setHeaders(Object.keys(modelInstance));
            
            // make to the service and get All records by default
            responseObject  = await  serv.getData(5,0);
            // set values for state properties
             setRecords(responseObject.data.CustomersEmployeesShipper);
             setPaging(generatePageNumbers(responseObject.data.TotalRecords, 
             	selectedRecordSizePerPage));
             setRecordLength(responseObject.data.TotalRecords);
        } 
        created();
         
    },[]); 


Listing 18: The code for useEffect()

Finally, we need to add functions that will be executed when the page size to be shown on UI is changed as well as when the page number is selected to show the new page with records. The code in Listing 19 shows the selectPageNumber() and changePageSize() functions.


// Method that will be executed when the page number is clicked
    // this will make call to REST API and get data to show on the Page
    const selectPageNumber= async(pgNumber)=>{
         
        let pageSize = selectedRecordSizePerPage * parseInt(pgNumber);
responseObject = await serv.getData(selectedRecordSizePerPage,pageSize); setRecords(responseObject.data.CustomersEmployeesShipper); pageSize = 0; } // Method that will be called when the Record Per Page // Drop Down is clicked. This will show Records fetched from the API // by calling the API const changePageSize= async(pgSize)=>{ setPaging(generatePageNumbers(totalRecordLengthFromServer, pgSize)); responseObject = await serv.getData(pgSize,0); setRecords(responseObject.data.CustomersEmployeesShipper); }

Listing 19: The selectPageNumber() and changePageSize() functions                 

The changePageSize() function accepts the parameter that represents the number of records to be shown on the UI by calling the getData() function from the HttpService class and passes the number of records to be received from the API. The selectPageNumber() function accepts the page number as a parameter to show the records for the selected page.  

The complete code for the component and its HTML UI is shown in Listing 20


import React, { Component, useState, useEffect } from 'react';
import Pagination from '../utilities/pagination';
import './pagination.css';
 
import HttpService from '../services/httpservice';
import CustomersEmployeesShippers from '../models/CustomersEmployeesShippers';
import ResponseObject from '../models/ResponseObject';
const PaginationComponent=()=>{
    // State property for storing received records from API
    let [fetchedRecords, setRecords] = useState([]);
    // array for Page Size
    const recordsSize = [1,5,10,15,20,25,30,35,40,45,50,100,150,200,250,300,350,400,450,500,550,600,650,700,750,800,850,900];
    // Selecting Page size the default value is 5 records
    const [selectedRecordSizePerPage,setRecSizePerPage] = useState(5);
    // State Property for Table Headers
    let [headers,setHeaders] = useState([]);
    // Instance of CustomersEmployeesShippers to read its properties
    const modelInstance=new CustomersEmployeesShippers()
    // Count of Records received from the Server
    let [totalRecordLengthFromServer, setRecordLength] = useState(0);
    // the paging state property that will be used to generate page numbers
    let [paging,setPaging] = useState([]);
    // The service class instance
    const serv = new HttpService();
    // the Response object
    let responseObject = new ResponseObject();
    
    useEffect(()=>{
         async function created(){
            setHeaders(Object.keys(modelInstance));
            
            // make to the service and get All records by default
            responseObject  = await  serv.getData(5,0);
            // set values for state properties
             setRecords(responseObject.data.CustomersEmployeesShipper);
             setPaging(generatePageNumbers(responseObject.data.TotalRecords, selectedRecordSizePerPage));
             setRecordLength(responseObject.data.TotalRecords);
        } 
        created();
         
    },[]); 

     
    const selectPageNumber= async(pgNumber)=>{
         
        let pageSize = selectedRecordSizePerPage * parseInt(pgNumber);
        responseObject = await serv.getData(selectedRecordSizePerPage,pageSize); 
        setRecords(responseObject.data.CustomersEmployeesShipper);
        pageSize = 0;
    }
     
    const changePageSize= async(pgSize)=>{
        setPaging(generatePageNumbers(totalRecordLengthFromServer, pgSize));
        responseObject =  await serv.getData(pgSize,0);
        setRecords(responseObject.data.CustomersEmployeesShipper);
    }
    
    const generatePageNumbers=(records,selectedpgSize=5)=>{
       
        let pagination = new Pagination();
        
        let pages = pagination.pageCount(records,selectedpgSize);
            let pageNumbers = [];
            // the pagination generation
            for (let index = 0; index <= pages; index++) {
                pageNumbers.push({
                    pageId:index 
                    });
            }    
            
            return pageNumbers;
        }


    return (
        <div>
            <h1>Perform The Server-Side Pagination</h1>
            <div>
                <label>Select no. of Records Per Page</label>
                <select className="form-control" 
                   value={selectedRecordSizePerPage}
                   onChange={(evt)=>{
                    setRecSizePerPage(parseInt(evt.target.value)); 
                    changePageSize(evt.target.value);}}>
                    {
                        recordsSize.map((recSize,idx)=>(
                            <option key={idx}>{recSize}</option>
                        ))
                    } 
                </select>
            </div>
            <br/>
            <div className="divPage">
                <table className="table table-striped table-bordered table-hover">
                    <thead className='headers'>
                        <tr>
                            {
                               headers.map((col,idx)=>(
                                <th key={idx} className="thead-dark">{col}</th>
                               ))         
                               
                            }
                        </tr>
                    </thead>
                    <tbody>
                        {
                            fetchedRecords.map((record,idx)=>(
                                <tr className="table-info"
                                 key={idx}>
                                    {
                                        headers.map((col,idx)=>(
                                            <td key={idx} className="thead-dark">{record[col]}</td>
                                         ))         
                               
                                    }
                                </tr>
                            ))
                        }
                      
                    </tbody>
                </table>
            </div>
            <br/>
            <div className="divPage">
                <nav aria-label="navigation">
                    <ul className="pagination">
                        {
                            paging.map((pageNo,idx)=>(
                                <li className="page-item c@pgIndex"
                                   key={idx} onClick={()=>selectPageNumber(pageNo.pageId)}>   
                                     <a className="page-link">{pageNo.pageId}</a>
                                </li>
                              
                            )) 
                        } 
                    </ul>
                </nav>
             </div>
        </div>
    );
};

export default PaginationComponent;

Listing 20: The Component code

The UI contains an HTML Select element that allows the selection of Page Size to show the number of records in the HTML table. Below the HTML Table, the HTML li element will be generated based on the pagination numbers which can be selected by the end-user to select the page to show records on the page. 

Run the React JS application using the command as shown in Listing 21

npm run start

Listing 21: Command to run React app                     

The component will be loaded in the browser which will show the first 5 records in the table and since the total number of records is 830, the pagination numbers will also be generated as shown in Figure  3



Figure 3: The React Component

Change the Page size from 5 to 30,  the first 30 records will be shown in the table and 27-page links will be generated as shown in Figure 4



Figure 4: The 30 record  

Now, change the page number to 1, and records of the next page will be displayed as shown in Figure 5



Figure 5: The Page 1

See the OrderId as 10278 is shown on the 1st page whereas the 0th page was shown the first OrderId as 10248 which means that each page is showing 30 records as we have selected the page size as 30.  

The code for this article can be downloaded from this link.

Conclusion: To show a large amount of data on UI is always a challenge but using server-side pagination it can be implemented easily.

Popular posts from this blog

ASP.NET Core 6: Adding Custom Middleware and Logging the Error Message in Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Role Based Security for ASP.NET Core 6 WEB API