Quick start: ASP.Net Core 3.1, Entity Framework Core, CQRS, React JS Series — Part 5: Pagination, Filtering, Searching and Sorting
In this content; Pagination, filtering, searching and sorting will be implemented through queries.
The parent content of this series: Quick start: ASP.Net Core 3.1, Entity Framework Core, CQRS, React JS Series
Outline
- Github feature branch
- Parameter Definitons (paging, filtering, search, sorting)
- What comes from Pagination?
- PagedList definition
- Sort Helper
- Using ISortHelper
- Searching on query and items
- Use it in Repository
- Map parameters DTO and send your object as query
- Response
Github feature branch
Parameter Definitons (paging, filtering, search, sorting)
For a paging and sorting, app needs values to being used as criteria.
public abstract class QueryStringParameters
{
const int maxPageSize = 50;
public int PageNumber { get; set; } = 1;
private int _pageSize = 10;
public int PageSize
{
get
{
return _pageSize;
}
set
{
_pageSize = (value > maxPageSize) ? maxPageSize : value;
}
}
public string OrderBy { get; set; }
}
For Product specific domain:
public class ProductParameters : QueryStringParameters
{
public ProductParameters()
{
OrderBy = "ProductRegisterDate desc";
}
public string Name { get; set; }
}
What comes from Pagination?
The answer brings paged data for sure. Besides, to be able to manage this paging mechanism, Size of page and total count are important, too.
public int CurrentPage { get; private set; }
public int TotalPages { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public bool HasPrevious => CurrentPage > 1;
public bool HasNext => CurrentPage < TotalPages;
To be able to use PagedList as follows:
return await PagedList<Product>.ToPagedList(sorderProducts, productParameters.PageNumber, productParameters.PageSize);
There is PagedList class and its extension method:
public class PagedList<T> : List<T>
{
public int CurrentPage { get; private set; }
public int TotalPages { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public bool HasPrevious => CurrentPage > 1;
public bool HasNext => CurrentPage < TotalPages;
public PagedList(List<T> items, int count, int pageNumber, int pageSize)
{
TotalCount = count;
PageSize = pageSize;
CurrentPage = pageNumber;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
AddRange(items);
}
public static async Task<PagedList<T>> ToPagedList(IQueryable<T> source, int pageNumber, int pageSize)
{
var count = source.Count();
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedList<T>(items, count, pageNumber, pageSize);
}
}
Sort Helper
As deferred execution, Sorthelper will apply the sort action on IQueryable collection:
public interface ISortHelper<T>
{
IQueryable<T> ApplySort(IQueryable<T> entities, string orderByQueryString);
}
Implementation:
namespace Domain.Infrastructure.EF.Helpers
{
public class SortHelper<T> : ISortHelper<T>
{
public IQueryable<T> ApplySort(IQueryable<T> entities, string orderByQueryString)
{
if (!entities.Any())
return entities;
if (string.IsNullOrWhiteSpace(orderByQueryString))
{
return entities;
}
var orderParams = orderByQueryString.Trim().Split(',');
var propertyInfos = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
var orderQueryBuilder = new StringBuilder();
foreach (var param in orderParams)
{
if (string.IsNullOrWhiteSpace(param))
continue;
var propertyFromQueryName = param.Split(" ")[0];
var objectProperty = propertyInfos.FirstOrDefault(pi => pi.Name.Equals(propertyFromQueryName, StringComparison.InvariantCultureIgnoreCase));
if (objectProperty == null)
continue;
var sortingOrder = param.EndsWith(" desc") ? "descending" : "ascending";
orderQueryBuilder.Append($"{objectProperty.Name} {sortingOrder}, ");
}
var orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' ');
return entities.OrderBy(orderQuery);
}
}
}
As a generic function, basically it determines column name and sort option and build a orderquery.
Using ISortHelper
Inject:
private ISortHelper<Product> _productSortHelper;
public async Task<PagedList<Product>> GetProductsAsync(ProductParameters productParameters)
{
var products = FindAll();
SearchByName(ref products, productParameters.Name);
var sorderProducts = _productSortHelper.ApplySort(products, productParameters.OrderBy);
return await PagedList<Product>.ToPagedList(sorderProducts, productParameters.PageNumber, productParameters.PageSize);
}
Searching
ProductParameters already includes Name field for search purposes.
private void SearchByName(ref IQueryable<Product> products, string productName)
{
if (!products.Any() || string.IsNullOrWhiteSpace(productName))
return;
products = products.Where(o => o.Name.ToLower().Contains(productName.Trim().ToLower()));
}
Map parameters DTO and send your object as query
Created DTO object below:
public class ProductParametersInfo : IValidatableDto
{
public ProductParametersInfo()
{
OrderBy = "ProductRegisterDate";
PageNumber = 1;
PageSize = 10;
}
public int PageNumber { get; set; }
public int PageSize { get; set; }
public string Name { get; set; }
public string OrderBy { get; set; }
}
And mapping with ProductParameters:
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<ProductParameters, ProductParametersInfo>().ReverseMap();
}
}
In Specific API, pass your object as follows:
[HttpGet(Name = "get-all-products")]
public async Task<IActionResult> GetAllProducts([FromQuery] ProductParametersInfo productParameters)
{
var productParametersEntity = _mapper.Map<ProductParameters>(productParameters);
var products = await _mediator.Send(new GetAllProductsQuery() { Parameters = productParametersEntity });
Response.Headers.Add("X-Pagination", products.GetMetadata());
var productsResult = _mapper.Map<IEnumerable<ProductDto>>(products);
return Ok(productsResult);
}
Response
Conclusion
Pagination, searching and sorting implementations are demonstrated within implementation level.