In this guide, we’ll explore how to implement dynamic pagination with filters in an ASP.NET Core application step by step.
In modern web applications, efficient data retrieval is crucial for providing a seamless user experience. Dynamic pagination with filters allows users to search and browse through large datasets effectively. In this guide, we’ll explore how to implement dynamic pagination with filters in an ASP.NET Core application step by step.
In this guide, we’ll explore how to implement dynamic pagination with filters in an ASP.NET Core application step by step, enhancing usability and performance.
If you find anything inappropriate please report it here.
- Define Filter Model and Comparison Enum: Introduce the ExpressionFilter class and Comparison enum, which represent filter criteria and comparison operations, respectively. These components form the foundation for defining and applying filters in the ASP.NET Core application.
- Implement Expression Builder: Explore the ExpressionBuilder class, which provides methods for dynamically constructing LINQ expressions based on provided filters. The ConstructAndExpressionTree method generates an expression tree based on a list of filters, while the GetExpression method constructs a LINQ expression for a single filter criterion.
- Base Repository Interface and Implementation: Define the repository interface and implementation responsible for querying the database and applying filters for pagination. Discuss how filters are dynamically applied to the LINQ query, enabling efficient data retrieval based on user-defined criteria.
- Base Service Interface and Implementation: Explain the service interface and implementation for retrieving paginated data with filters. Highlight how the service interacts with the repository to fetch data and map it to view models, facilitating the creation of paginated data view models for presentation.
- Controller Setup: Detail the setup of the controller method to handle HTTP GET requests for retrieving paginated data with filters. Discuss how the controller accepts parameters for pagination, search criteria, and applies default values if not provided. Explore how filters are constructed based on the search criteria and applied to the ProductService to retrieve paginated data.
1. Define Filter Model and Comparison Enum
public class ExpressionFilter { public string? PropertyName { get; set; } public object? Value { get; set; } public Comparison Comparison { get; set; } } public enum Comparison { [Display(Name = "==")] Equal, [Display(Name = "<")] LessThan, [Display(Name = "<=")] LessThanOrEqual, [Display(Name = ">")] GreaterThan, [Display(Name = ">=")] GreaterThanOrEqual, [Display(Name = "!=")] NotEqual, [Display(Name = "Contains")] Contains, //for strings [Display(Name = "StartsWith")] StartsWith, //for strings [Display(Name = "EndsWith")] EndsWith, //for strings }
- The
ExpressionFilter
class represents a filter criterion with properties likePropertyName
,Value
, andComparison
. - The
Comparison
enum enumerates various comparison operations like equal, less than, greater than, etc.
2. Implement Expression Builder
public static class ExpressionBuilder { public static Expression<Func<T, bool>> ConstructAndExpressionTree<T>(List<ExpressionFilter> filters) { if (filters.Count == 0) return null; ParameterExpression param = Expression.Parameter(typeof(T), "t"); Expression exp = null; if (filters.Count == 1) { exp = GetExpression<T>(param, filters[0]); } else { exp = GetExpression<T>(param, filters[0]); for (int i = 1; i < filters.Count; i++) { exp = Expression.Or(exp, GetExpression<T>(param, filters[i])); } } return Expression.Lambda<Func<T, bool>>(exp, param); } public static Expression GetExpression<T>(ParameterExpression param, ExpressionFilter filter) { MethodInfo containsMethod = typeof(string).GetMethod("Contains", new Type[] { typeof(string) }); MethodInfo startsWithMethod = typeof(string).GetMethod("StartsWith", new Type[] { typeof(string) }); MethodInfo endsWithMethod = typeof(string).GetMethod("EndsWith", new Type[] { typeof(string) }); MemberExpression member = Expression.Property(param, filter.PropertyName); ConstantExpression constant = Expression.Constant(filter.Value); switch (filter.Comparison) { case Comparison.Equal: return Expression.Equal(member, constant); case Comparison.GreaterThan: return Expression.GreaterThan(member, constant); case Comparison.GreaterThanOrEqual: return Expression.GreaterThanOrEqual(member, constant); case Comparison.LessThan: return Expression.LessThan(member, constant); case Comparison.LessThanOrEqual: return Expression.LessThanOrEqual(member, constant); case Comparison.NotEqual: return Expression.NotEqual(member, constant); case Comparison.Contains: return Expression.Call(member, containsMethod, constant); case Comparison.StartsWith: return Expression.Call(member, startsWithMethod, constant); case Comparison.EndsWith: return Expression.Call(member, endsWithMethod, constant); default: return null; } } }
- The
ExpressionBuilder
class provides methods for dynamically constructing LINQ expressions based on provided filters. - The
ConstructAndExpressionTree
method constructs an expression tree based on a list of filters. - The
GetExpression
method constructs a LINQ expression for a single filter criterion based on its comparison type.
3. Base Repository Interface and Implementation
// Interface Task<PaginatedDataViewModel<T>> GetPaginatedDataWithFilter(int pageNumber, int pageSize, List<ExpressionFilter> filters, CancellationToken cancellationToken); // Implementation public async Task<PaginatedDataViewModel<T>> GetPaginatedDataWithFilter(int pageNumber, int pageSize, List<ExpressionFilter> filters, CancellationToken cancellationToken = default) { var query = _dbContext.Set<T>().AsNoTracking(); // Apply search criteria if provided if (filters != null && filters.Any()) { // Construct expression tree based on filters var expressionTree = ExpressionBuilder.ConstructAndExpressionTree<T>(filters); query = query.Where(expressionTree); } // Pagination var data = await query .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(cancellationToken); // Total count of data var totalCount = await query.CountAsync(cancellationToken); // Create and return paginated data view model return new PaginatedDataViewModel<T>(data, totalCount); }
- The repository interface defines a method
GetPaginatedDataWithFilter
to retrieve paginated data with filters. - The repository implementation constructs a LINQ query dynamically based on the provided filters.
- Filters are applied to the query using the ExpressionBuilder class.
- Pagination is applied to the query to retrieve a specific subset of data.
- The total count of data is calculated.
- A paginated data view model containing the queried data and total count is created and returned.
4. Base Service Interface and Implementation
// Interface Task<PaginatedDataViewModel<TViewModel>> GetPaginatedDataWithFilter(int pageNumber, int pageSize, List<ExpressionFilter> filters, CancellationToken cancellationToken); // Implementation public virtual async Task<PaginatedDataViewModel<TViewModel>> GetPaginatedDataWithFilter(int pageNumber, int pageSize, List<ExpressionFilter> filters, CancellationToken cancellationToken) { // Retrieve paginated data with filters from repository var paginatedData = await _repository.GetPaginatedDataWithFilter(pageNumber, pageSize, filters, cancellationToken); // Map data to view models var mappedData = _viewModelMapper.MapList(paginatedData.Data); // Create paginated data view model var paginatedDataViewModel = new PaginatedDataViewModel<TViewModel>(mappedData.ToList(), paginatedData.TotalCount); // Return paginated data view model return paginatedDataViewModel; }
- The service interface defines a method
GetPaginatedDataWithFilter
to retrieve paginated data with filters. - The service implementation retrieves paginated data with filters from the repository.
- Retrieved data is mapped to view models using a view model mapper.
- A paginated data view model is created and returned.
5. Controller Setup
[HttpGet("paginated-data-with-filter")] public async Task<IActionResult> Get(int? pageNumber, int? pageSize, string? search, CancellationToken cancellationToken) { try { // Setting default values for pagination int pageSizeValue = pageSize ?? 10; int pageNumberValue = pageNumber ?? 1; // List to hold filters var filters = new List<ExpressionFilter>(); // Check if search criteria is provided if (!string.IsNullOrWhiteSpace(search) && search != null) { // Add filters for relevant properties based on the search string filters.AddRange(new[] { new ExpressionFilter { PropertyName = "Code", Value = search, Comparison = Comparison.Contains }, new ExpressionFilter { PropertyName = "Name", Value = search, Comparison = Comparison.Contains }, new ExpressionFilter { PropertyName = "Description", Value = search, Comparison = Comparison.Contains } }); // Check if the search string represents a valid numeric value for the "Price" property if (double.TryParse(search, out double price)) { filters.Add(new ExpressionFilter { PropertyName = "Price", Value = price, Comparison = Comparison.Equal }); } } // Retrieve paginated data with filters from ProductService var products = await _productService.GetPaginatedDataWithFilter(pageNumberValue, pageSizeValue, filters, cancellationToken); // Create response containing paginated data var response = new ResponseViewModel<PaginatedDataViewModel<ProductViewModel>> { Success = true, Message = "Products retrieved successfully", Data = products }; // Return response return Ok(response); } catch (Exception ex) { // Log error _logger.LogError(ex, "An error occurred while retrieving products"); // Create error response var errorResponse = new ResponseViewModel<IEnumerable<ProductViewModel>> { Success = false, Message = "Error retrieving products", Error = new ErrorViewModel { Code = "ERROR_CODE", Message = ex.Message } }; // Return error response return StatusCode(StatusCodes.Status500InternalServerError, errorResponse); } }
- This controller method handles HTTP GET requests to retrieve paginated data with filters.
- It accepts parameters for pagination (
pageNumber
andpageSize
) and a search string (search
). - Default values for pagination are set if not provided.
- Filters are constructed based on the search criteria, including properties like
Code
,Name
,Description
, andPrice
. - The search string is checked to determine if it represents a valid numeric value for the
Price
property. - Paginated data with filters is retrieved from the ProductService.
- A response containing paginated data is created and returned if successful.
- If an error occurs, it is logged, and an error response is returned.
If you find anything inappropriate please report it here.