LSCore SortAndPage
The SortAndPage module provides a standardized approach to sorting and paginating IQueryable<T> data sources. It is split into two NuGet packages: a lightweight Contracts package for shared models and a Domain package that contains the query-execution logic.
NuGet Packages
| Package | Description |
|---|---|
LSCore.SortAndPage.Contracts | Request/response models, sort rules, pagination data, and the IQueryable extension methods for building queries. No external dependencies. |
LSCore.SortAndPage.Domain | The ToSortedAndPagedResponse extension methods that execute queries and return fully populated responses. Depends on the Contracts package. |
Both packages target .NET 9.0.
Core Concepts
Sort Column Enum
Every sortable request is generic over a TSortColumn type parameter constrained to struct. In practice this is an enum you define in your project that lists the columns a client is allowed to sort by.
public enum UserSortColumn
{
Name,
Email,
CreatedAt
}
Sort Rules Dictionary
A Dictionary<TSortColumn, LSCoreSortRule<T>> maps each enum value to the LINQ expression that should be used when that column is requested. This dictionary is the single source of truth for how sort columns translate into database expressions.
var sortRules = new Dictionary<UserSortColumn, LSCoreSortRule<User>>
{
{ UserSortColumn.Name, new LSCoreSortRule<User>(u => u.Name) },
{ UserSortColumn.Email, new LSCoreSortRule<User>(u => u.Email) },
{ UserSortColumn.CreatedAt, new LSCoreSortRule<User>(u => u.CreatedAt) }
};
Request Models
LSCoreSortableRequest<TSortColumn>
The base request for sorting without pagination.
public class LSCoreSortableRequest<TSortColumn>
where TSortColumn : struct
{
public TSortColumn? SortColumn { get; set; }
public ListSortDirection SortDirection { get; set; } = ListSortDirection.Ascending;
}
| Property | Type | Default | Description |
|---|---|---|---|
SortColumn | TSortColumn? | null | The column to sort by. When null, no sorting is applied. |
SortDirection | ListSortDirection | Ascending | Ascending or Descending. Uses System.ComponentModel.ListSortDirection. |
LSCoreSortableAndPageableRequest<TSortColumn>
Extends LSCoreSortableRequest<TSortColumn> with pagination fields.
public class LSCoreSortableAndPageableRequest<TSortColumn> : LSCoreSortableRequest<TSortColumn>
where TSortColumn : struct
{
public int PageSize { get; set; } = 10;
public int CurrentPage { get; set; } = 1;
}
| Property | Type | Default | Description |
|---|---|---|---|
PageSize | int | 10 | Number of items per page. |
CurrentPage | int | 1 | The 1-based page number to retrieve. |
All properties from LSCoreSortableRequest (SortColumn, SortDirection) are inherited.
Response Model
LSCoreSortedAndPagedResponse<TPayload>
The standard response wrapper returned after sorting and paging a query.
public class LSCoreSortedAndPagedResponse<TPayload>
{
public List<TPayload>? Payload { get; set; }
public LSCorePaginationData? Pagination { get; set; }
}
| Property | Type | Description |
|---|---|---|
Payload | List<TPayload>? | The items for the current page. |
Pagination | LSCorePaginationData? | Metadata about the current page, page size, total count, and total pages. |
Pagination Data
LSCorePaginationData
A record that carries pagination metadata.
public record LSCorePaginationData(int Page, int PageSize, int TotalCount)
{
public int TotalPages => TotalCount / PageSize + (TotalCount % PageSize == 0 ? 0 : 1);
}
| Property | Type | Description |
|---|---|---|
Page | int | The current page number (1-based). |
PageSize | int | The number of items per page. |
TotalCount | int | The total number of items across all pages. |
TotalPages | int | Computed. The total number of pages, accounting for a partial final page. |
Sort Rules
LSCoreSortRule<T>
Wraps a LINQ expression that tells the sorting engine which property (or computed value) to sort by.
public class LSCoreSortRule<T>(Expression<Func<T, object>> sortExpression)
where T : class
{
public Expression<Func<T, object>> SortExpression { get; } = sortExpression;
}
The expression is typed as Expression<Func<T, object>> so it can be passed directly to OrderBy / OrderByDescending on an IQueryable<T>.
QueryableExtensions (Contracts)
These extension methods live in LSCore.SortAndPage.Contracts and operate on IQueryable<T> without executing the query. They are useful when you need to compose additional query operations after sorting/paging.
SortQuery
Applies sorting to a query based on the request and sort rules. Returns the query unchanged if SortColumn is null.
public static IQueryable<T> SortQuery<T, TSortColumn>(
this IQueryable<T> source,
LSCoreSortableRequest<TSortColumn> request,
Dictionary<TSortColumn, LSCoreSortRule<T>> sortRules
)
SortAndPageQuery
Applies sorting (via SortQuery) and then pagination (Skip / Take) to a query.
public static IQueryable<T> SortAndPageQuery<T, TSortColumn>(
this IQueryable<T> source,
LSCoreSortableAndPageableRequest<TSortColumn> request,
Dictionary<TSortColumn, LSCoreSortRule<T>> sortRules
)
Domain Extensions
These extension methods live in LSCore.SortAndPage.Domain and execute the query, returning a fully populated LSCoreSortedAndPagedResponse.
ToSortedAndPagedResponse (same type)
Counts the total items, applies sort and page, and materializes the results.
public static LSCoreSortedAndPagedResponse<T> ToSortedAndPagedResponse<T, TSortColumn>(
this IQueryable<T> source,
LSCoreSortableAndPageableRequest<TSortColumn> request,
Dictionary<TSortColumn, LSCoreSortRule<T>> sortRules
)
ToSortedAndPagedResponse (with mapping)
Same as above, but accepts a Func<T, TPayload> mapping function to project each entity into a different type (e.g., a DTO or view model).
public static LSCoreSortedAndPagedResponse<TPayload> ToSortedAndPagedResponse<T, TSortColumn, TPayload>(
this IQueryable<T> source,
LSCoreSortableAndPageableRequest<TSortColumn> request,
Dictionary<TSortColumn, LSCoreSortRule<T>> sortRules,
Func<T, TPayload> mapFunc
)
Usage Examples
1. Define a sort column enum and sort rules
public enum ProductSortColumn
{
Name,
Price,
CreatedAt
}
public static class ProductSortRules
{
public static readonly Dictionary<ProductSortColumn, LSCoreSortRule<Product>> Rules = new()
{
{ ProductSortColumn.Name, new LSCoreSortRule<Product>(p => p.Name) },
{ ProductSortColumn.Price, new LSCoreSortRule<Product>(p => p.Price) },
{ ProductSortColumn.CreatedAt, new LSCoreSortRule<Product>(p => p.CreatedAt) }
};
}
2. Accept the request in a controller
[HttpGet("products")]
public IActionResult GetProducts([FromQuery] LSCoreSortableAndPageableRequest<ProductSortColumn> request)
{
var response = _productRepository.GetProducts(request);
return Ok(response);
}
The client sends query parameters like:
GET /products?SortColumn=Price&SortDirection=Descending&PageSize=20&CurrentPage=2
3. Execute in a repository – returning entities directly
public LSCoreSortedAndPagedResponse<Product> GetProducts(
LSCoreSortableAndPageableRequest<ProductSortColumn> request)
{
return _context.Products
.AsQueryable()
.ToSortedAndPagedResponse(request, ProductSortRules.Rules);
}
4. Execute in a repository – mapping to a DTO
public LSCoreSortedAndPagedResponse<ProductDto> GetProducts(
LSCoreSortableAndPageableRequest<ProductSortColumn> request)
{
return _context.Products
.AsQueryable()
.ToSortedAndPagedResponse(
request,
ProductSortRules.Rules,
product => new ProductDto
{
Id = product.Id,
Name = product.Name,
Price = product.Price
}
);
}
5. Sort-only (no pagination)
If you only need sorting without pagination, use SortQuery from the Contracts package directly:
public List<Product> GetSortedProducts(LSCoreSortableRequest<ProductSortColumn> request)
{
return _context.Products
.AsQueryable()
.SortQuery(request, ProductSortRules.Rules)
.ToList();
}
6. Composing additional query logic
Because SortQuery and SortAndPageQuery return IQueryable<T>, you can chain additional LINQ operations:
var query = _context.Products
.Where(p => p.IsActive)
.SortAndPageQuery(request, ProductSortRules.Rules);
// Add projection or further filtering
var results = query
.Select(p => new { p.Name, p.Price })
.ToList();
Example JSON Response
A typical API response using LSCoreSortedAndPagedResponse looks like:
{
"payload": [
{ "id": 1, "name": "Widget A", "price": 9.99 },
{ "id": 2, "name": "Widget B", "price": 14.99 }
],
"pagination": {
"page": 2,
"pageSize": 10,
"totalCount": 55,
"totalPages": 6
}
}