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.
- 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.
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!