Implementing Dynamic Pagination with Filters in ASP.NET Core

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.

Dynamic Pagination with Filters in ASP.NET Core Web API

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 like PropertyName, Value, and Comparison.
  • 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 and pageSize) 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, and Price.
  • 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.

In conclusion, implementing dynamic pagination with filters in an ASP.NET Core application enhances the user experience by enabling efficient data retrieval and browsing capabilities. By following the steps outlined in this guide, developers can create web applications that provide seamless navigation through large datasets, improving usability and performance. Stay tuned for more in-depth tutorials and best practices in ASP.NET Core development!

Leave a Reply