Mastering Filtering and Pagination in ASP.NET Core and React.js Part 1

Rashad Mammadov
9 min readApr 26, 2023

--

Hello everyone.

In my previous article, I mentioned dynamic filtering using Lamda Expression in C#. Now, in this article, we will talk about how to filter and paginate in Net Core Web API and React.js. This article will be in two parts. In the first part, we will create our backend (ASP.NET Core Web API) and make it suitable for filtering and pagination. In the second part, we will create our frontend application using React.js and we will use Material React Table as a table for filtering and pagination.

In this project we will use:

  • ASP.NET Core Web API (C#) (backend)
  • React.js (Typescript) (frontend)
  • Axios (for HTTP requests)
  • Material UI (visualization)
  • Material React Table (data table)

So let’s start.

First of all, I would like to give a brief overview of what is filtering and pagination.

Filtering and pagination are essential techniques for managing and displaying data in web applications, especially when dealing with large datasets. They offer the following key benefits:

  1. User experience: Filtering helps users find relevant information quickly by narrowing down datasets, while pagination simplifies navigation by dividing data into smaller chunks.
  2. Performance: Filtering and pagination reduce server and client-side load, resulting in faster response times and better application performance.
  3. Scalability: These techniques allow your application to maintain a consistent and efficient user experience even as the dataset grows.

Let’s start creating our project.

We are creating an ASP.NET Core Web API project. After creating our project, we come to the Controller folder and create a new UserController.

using Microsoft.AspNetCore.Mvc;

namespace FilteringandPagination.Controllers
{
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{

}
}

We will create a class and specify its name as Users.

 public class Users
{
public int user_id { get; set; }
public string user_name { get; set; }
public int user_age { get; set; }
}

Since we will not use any database in our project, we are creating dummy data that we can read data from.

private readonly List<Users> users = new List<Users>{
new Users { user_id=1,user_name="Simon",user_age=25 },
new Users { user_id=2,user_name="Jones",user_age=30 },
new Users { user_id=3,user_name="Adrian",user_age=21 },
new Users { user_id=4,user_name="Grace",user_age=32 },
new Users { user_id=5,user_name="Chloe",user_age=36 },
new Users { user_id=6,user_name="Lily",user_age=24 },
new Users { user_id=7,user_name="Joan",user_age=38 },
new Users { user_id=8,user_name="Jake",user_age=20 },
new Users { user_id=9,user_name="Boris",user_age=26 },
new Users { user_id=10,user_name="Caroline",user_age=24 },
new Users { user_id=11,user_name="Connor",user_age=27 },
new Users { user_id=12,user_name="Vanessa",user_age=25 },
new Users { user_id=13,user_name="Dorothy",user_age=35 },
new Users { user_id=14,user_name="Deirdre",user_age=36 },
new Users { user_id=15,user_name="Melanie",user_age=25 },

};

Now our Users class and dummy data are ready. Now we have to create two classes for filtering and pagination. For filtering, we will create SearchParams class and for pagination, create PaginationParams class. We will inherit SearchParams from PaginationParams, which means that it includes all the properties and methods defined in the PaginationParams class, in addition to its own properties.

public class SearchParams: PaginationParams
{
public string? OrderBy { get; set; }
public string? SearchTerm { get; set; }
public string? ColumnFilters { get; set; }
}
public class PaginationParams
{
private const int MaxPageSize = 100;
public int PageNumber { get; set; } = 1;
private int _pageSize = 6;
public int PageSize { get { return _pageSize; } set { _pageSize = value > MaxPageSize ? MaxPageSize : value; } }
}

Now let’s examine these two classes in detail.

1. SearchParams: This class is designed to store various parameters related to searching, filtering, and sorting data. It contains three properties:

· OrderBy: This property is a nullable string and is used to store the column name and sort order for sorting the data. For example, “user_name ASC” or “user_age DESC” could be values for this property, where “ASC” represents ascending order and “DESC” represents descending order.

· SearchTerm: This nullable string property is used to store a search term entered by the user. When this value is set, it can be used to filter the data based on the given search term, typically by matching the term with the content in one or more columns.

· ColumnFilters: This nullable string property is designed to store additional filtering criteria based on specific columns. The format for this property is not defined in the code snippet, but it could be, for example, a JSON string representing column names and their corresponding filter values.

2. PaginationParams: This class is designed to store pagination-related parameters, such as page number and page size. It contains three properties and one constant:

· MaxPageSize: A constant integer representing the maximum allowed page size. It is set to 100 in this example, which means that the largest possible page size is 100 items.

· PageNumber: An integer property representing the current page number. It is initialized to 1 by default.

· _pageSize: A private integer field is used to store the actual value of the page size. It is initialized to 6 by default.

· PageSize: A public integer property with a getter and setter. The getter returns the value of the _pageSize field, and the setter sets the _pageSize field’s value, ensuring that it doesn’t exceed the MaxPageSize constant.

I hope everything has been clear so far. Now let’s continue where we left off. We need to convert our Orderby and ColumnFilters fields that will come as JSON from the HTTP request. For this, we will create two classes: ColumnFilter class and ColumnSorting class.

public class ColumnFilter
{
public string id { get; set; }
public string value { get; set; }
}
public class ColumnSorting
{
public string? id { get; set; }
public bool desc { get; set; }
}

Let’s write convert methods.

using System.Text.Json;

List<ColumnFilter> columnFilters = new List<ColumnFilter>();
if (!String.IsNullOrEmpty(searchParam.ColumnFilters))
{
try
{
columnFilters.AddRange(JsonSerializer.Deserialize<List<ColumnFilter>>(searchParam.ColumnFilters));
}
catch (Exception)
{
columnFilters = new List<ColumnFilter>();
}
}
List<ColumnSorting> columnSorting = new List<ColumnSorting>();
if (!String.IsNullOrEmpty(searchParam.OrderBy))
{
try
{
columnSorting.AddRange(JsonSerializer.Deserialize<List<ColumnSorting>>(searchParam.OrderBy));
}
catch (Exception)
{
columnSorting = new List<ColumnSorting>();
}
}

Now we are ready to implement dynamic filtering. We’re going to make some changes to the dynamic filtering that I described in this article (Dynamically Building Lambda Expressions in C# | Medium), and also make it generic so that we can use it with other data models. For this, we will create CustomExpressionFilter class and implement our dynamic filtering.

public static class CustomExpressionFilter<T> where T : class
{
public class ExpressionFilter
{
public string ColumnName { get; set; }
public string Value { get; set; }
}
public static Expression<Func<T, bool>> CustomFilter(List<ColumnFilter> columnFilters, string className)
{
Expression<Func<T, bool>> filters = null;
try
{
var expressionFilters = new List<ExpressionFilter>();
foreach (var item in columnFilters)
{
expressionFilters.Add(new ExpressionFilter() { ColumnName = item.id, Value = item.value });
}
// Create the parameter expression for the input data
var parameter = Expression.Parameter(typeof(T), className);

// Build the filter expression dynamically
Expression filterExpression = null;
foreach (var filter in expressionFilters)
{
var property = Expression.Property(parameter, filter.ColumnName);

Expression comparison;

if (property.Type == typeof(string))
{
var constant = Expression.Constant(filter.Value);
comparison = Expression.Call(property, "Contains", Type.EmptyTypes, constant);
}
else if (property.Type == typeof(double))
{
var constant = Expression.Constant(Convert.ToDouble(filter.Value));
comparison = Expression.Equal(property, constant);
}
else if (property.Type == typeof(Guid))
{
var constant = Expression.Constant(Guid.Parse(filter.Value));
comparison = Expression.Equal(property, constant);
}
else
{
var constant = Expression.Constant(Convert.ToInt32(filter.Value));
comparison = Expression.Equal(property, constant);
}


filterExpression = filterExpression == null
? comparison
: Expression.And(filterExpression, comparison);
}

// Create the lambda expression with the parameter and the filter expression
filters = Expression.Lambda<Func<T, bool>>(filterExpression, parameter);
}
catch (Exception)
{
filters = null;
}
return filters;
}

}

Now, let’s use our CustomExpressionFilter class. First, we are checking our SearchTerm, If it contains information we are creating a filter. Then we are checking columnFilters, if columnFilters has data overwriting a filter.

Expression<Func<Users, bool>> filters = null;
//First, we are checking our SearchTerm. If it contains information we are creating a filter.
var searchTerm = "";
if (!string.IsNullOrEmpty(searchParam.SearchTerm))
{
searchTerm = searchParam.SearchTerm.Trim().ToLower();
filters = x => x.user_name.ToLower().Contains(searchTerm);
}
// Then we are overwriting a filter if columnFilters has data.
if (columnFilters.Count > 0)
{
filters = CustomExpressionFilter<Users>.CustomFilter(columnFilters, "users");
}

Now our custom filter is ready to use. Right now it won’t do anything on its own. Also, we need to add in pagination. For this, let’s create a generic structure that we can easily use in all data models. Create a new PaginationQuery class. Define class as static. We will add two generic IQueryable methods: CustomQuery<T> method and CustomPagination<T> method. These two methods are designed to add filtering and pagination functionality to LINQ queries, respectively.

public static class PaginationQuery
{
public static IQueryable<T> CustomQuery<T>(this IQueryable<T> query, Expression<Func<T, bool>> filter = null) where T : class
{
if (filter != null)
{
query = query.Where(filter);
}

return query;
}

public static IQueryable<T> CustomPagination<T>(this IQueryable<T> query, int? page = 0, int? pageSize = null)
{
if (page != null)
{
query = query.Skip(((int)page - 1) * (int)pageSize);
}

if (pageSize != null)
{
query = query.Take((int)pageSize);
}
return query;
}
}

Let’s examine the code together.

CustomQuery<T>: This generic method takes an IQueryable<T> object as input and an optional Expression<Func<T, bool>> delegate called a filter. The method checks if a filter is provided and, if so, applies the filter to the query using the Where method. The updated query is returned. If no filter is provided, the original query is returned unchanged. The T : class constraint ensures that this method can only be used with reference types.

CustomPagination<T>: This generic method takes an IQueryable<T> object as input and two optional nullable integer parameters, page and pageSize. The method applies pagination to the query using the Skip and Take methods.

· If a page value is provided (not null), the method calculates the number of items to skip using the formula ((int)page — 1) * (int)pageSize and skips them using the Skip method.

· If a pageSize value is provided (not null), the method limits the number of items in the result set using the Take method with the specified pageSize.

· Finally, the updated query is returned.

Now we are ready to complete the code. Let’s implement the PaginationQuery in our code.

var query = users.AsQueryable().CustomQuery(filters);
var count=query.Count();
var filteredData = query.CustomPagination(searchParam.PageNumber,searchParam.PageSize).ToList();
return filteredData;

While users are GenericList we have to convert to IQueryable in order to use CustomQuery. Then we are getting the record count with Count() method (we will use it to show in the data table). Finally, we apply the generic pagination with CustomPagination() method.

And here we are finally ready to test our project.

Default giving 6 records
Filtering with SearchTerm
Filtering with custom JSON

Note: We will test ColumnFilters with custom json for now. ( [{“id”:”user_name”,”value”:”im”}] )

And voila, filtering and pagination work perfectly.

In conclusion, we have successfully implemented filtering and pagination in an ASP.NET Core Web API project using Lambda expressions, and custom extension methods. By creating reusable, generic classes and methods, we have made it possible to apply the same filtering and pagination functionality to different data models with minimal effort.

This approach provides a scalable solution to handling large datasets and improves both user experience and application performance. By utilizing these techniques, you can easily build web applications that offer fast and efficient data management for a wide range of use cases.

In the next part of this article series, we will focus on creating the front end using React.js and TypeScript, as well as integrating the Material UI library and Material React Table for visualization and data table handling. Stay tuned to learn how to build a complete, end-to-end solution for filtering and pagination in web applications using modern technologies and best practices.

Thank you for reading my article…..

Access to the GitHub repo : Link

--

--

Rashad Mammadov

Passionate full-stack developer crafting efficient ERP solutions.