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.