Version: v1.0.0 | Changelog
Pagination
The Pagination module gives you a clean, reusable way to slice in-memory collections into pages without writing the maths yourself every time. You get back both the paginated data and all the metadata a frontend needs to render navigation controls.
How to Access
There are two ways to use it:
1. Via Dependency Injection (recommended)
IPaginationHelperFactory is registered as a singleton. Inject it into your service, then call Create() with your collection and page size. This keeps your code testable and decoupled.
public class UserService
{
private readonly IPaginationHelperFactory _pagination;
public UserService(IPaginationHelperFactory pagination)
{
_pagination = pagination;
}
public PaginatedResponse<User> GetUsers(List<User> allUsers, int page, int pageSize)
{
var helper = _pagination.Create(allUsers, pageSize);
return helper.GetPagedResponse(page, pageSize);
}
}
2. Direct instantiation
You can instantiate PaginationHelper<T> directly:
var helper = new PaginationHelper<User>(allUsers, itemsPerPage: 10);
var result = helper.GetPagedResponse(pageNumber: 1, pageSize: 10);
Performance Deep Dive:
IEnumerable<T>vs.IQueryable<T>The
PaginationHelper<T>in this package operates onIEnumerable<T>. This means it works with collections that are already loaded into your server's memory.How it works here (
IEnumerable<T>): If you have 10,000 users in your database and you pass them to this helper to get page 1 (10 items), your application first downloads all 10,000 users from the database into RAM, and then the helper slices out the 10 you asked for.
- Pros: Great for small datasets, cached lists, or data originating from a 3rd-party API where memory footprint is negligible.
- Cons: Terrible for large database tables. It wastes server memory, increases garbage collection pressure, and slows down database query times.
How to accurately manage large databases (
IQueryable<T>): When querying large SQL databases via Entity Framework (EF Core), you do not usePaginationHelper<T>to slice the data. Instead, slice the data natively on the SQL server using.Skip().Take(), and then manually construct aPaginatedResponse<T>to maintain a consistent API response format for your frontend:var totalItems = await dbContext.Users.CountAsync(); var pagedUsers = await dbContext.Users .OrderBy(u => u.Id) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); // You can then wrap the result in PaginatedResponse<T> // (HasNextPage, PreviousPage, etc. are calculated automatically) var response = new PaginatedResponse<User> { Data = pagedUsers, CurrentPage = pageNumber, PageSize = pageSize, TotalItems = totalItems, TotalPages = (int)Math.Ceiling((double)totalItems / pageSize) }; return Ok(response);The Takeaway: Use
PaginationHelper<T>when you already have the full list in memory (e.g., parsing a CSV file, grouping an existing dataset, or caching). Use native EF Core.Skip().Take()combined with a manualPaginatedResponse<T>when querying databases directlycor large datasets to ensure peak efficiency while preserving standard API responses.
Available Methods on PaginationHelper
GetPagedResponse (basic)
Returns a page of items from the collection with full metadata — total items, total pages, current page, and whether there are previous/next pages.
If an out-of-range page number is requested (e.g., page -1 or page 999 when only 5 pages exist), it's clamped automatically. You won't get an exception — just the nearest valid page.
var result = helper.GetPagedResponse(pageNumber: 2, pageSize: 10);
// result.Data — the items on this page
// result.TotalItems — full dataset count
// result.TotalPages — total number of pages
// result.HasNextPage / result.HasPreviousPage — for UI navigation
GetPagedResponse (with filter)
Filters the collection using a predicate before paginating. TotalItems in the response reflects the count after filtering, not the full original collection size.
var result = helper.GetPagedResponse(
predicate: u => u.IsActive,
pageNumber: 1,
pageSize: 10
);
GetPagedResponse (with sort)
Sorts the collection by a given key, then paginates. Pass descending: true to reverse the order (newest first, Z-A, etc.).
var result = helper.GetPagedResponse(
keySelector: u => u.CreatedAt,
pageNumber: 1,
pageSize: 10,
descending: true // newest users first
);
PaginationRequest
If you're accepting pagination parameters from an API request, bind them into a PaginationRequest and call Validate() before use. It enforces safe boundaries — any PageNumber below 1 snaps to 1, any PageSize above 100 is capped at 100.
public IActionResult GetUsers([FromQuery] PaginationRequest req)
{
req.Validate(); // enforces minimum/maximum bounds
var result = helper.GetPagedResponse(req.PageNumber, req.PageSize);
return Ok(result);
}
| Property | Default | Constraints |
|---|---|---|
PageNumber |
1 |
Minimum: 1 |
PageSize |
10 |
Minimum: 1, Maximum: 100 |
PaginatedResponse Properties
| Property | Type | Description |
|---|---|---|
Data |
List<T> |
Items on the current page |
CurrentPage |
int |
The page number returned |
PageSize |
int |
Items per page |
TotalItems |
int |
Total dataset size (post-filter if applicable) |
TotalPages |
int |
Total number of pages |
HasNextPage |
bool |
true if there's a page after this one |
HasPreviousPage |
bool |
true if there's a page before this one |
NextPage |
int? |
Next page number, or null if on the last page |
PreviousPage |
int? |
Previous page number, or null if on the first page |