EfCoreKit provides two pagination strategies: offset-based (traditional page numbers) and keyset/cursor-based (for high-volume data and infinite scroll).
Best for: admin dashboards, search results with page numbers, moderate data volumes.
var page = await context.Orders
.Where(o => o.Status == OrderStatus.Active)
.OrderBy(o => o.CreatedAt)
.ToPagedAsync(page: 2, pageSize: 25);
page.Items // Items on this page
page.TotalCount // Total matching rows across all pages
page.TotalPages // Calculated total page count
page.Page // Current page number
page.PageSize // Items per page
page.HasNextPage // Whether there's a next page
page.HasPreviousPage // Whether there's a previous page
page.From // 1-based index of the first item on this page
page.To // 1-based index of the last item on this page
page.IsEmpty // True when TotalCount == 0public sealed class PagedResult<T>
{
public IReadOnlyList<T> Items { get; }
public int TotalCount { get; }
public int Page { get; }
public int PageSize { get; }
public int TotalPages { get; }
public bool HasPreviousPage { get; }
public bool HasNextPage { get; }
public bool IsEmpty { get; }
public int From { get; } // 1-based first item index
public int To { get; } // 1-based last item index
public PagedResult<TDestination> Map<TDestination>(Func<T, TDestination> mapper);
}var page = await context.Customers
.Where(c => c.IsActive)
.ToPagedAsync(page: 1, pageSize: 20);
var dtoPage = page.Map(c => new CustomerDto
{
Name = c.Name,
Email = c.Email
});Project before the query executes — only the columns you need are fetched:
var page = await context.Customers
.Where(c => c.IsActive)
.SelectToPagedAsync(
c => new CustomerDto { Name = c.Name, Email = c.Email },
page: 1,
pageSize: 20);var spec = new ActiveOrdersSpec(customerId); // ApplyPaging already set on the spec
var page = await context.Orders.ToPagedFromSpecAsync(spec, page: 1, pageSize: 20);pagemust be ≥ 1pageSizemust be between 1 and 1000- Throws
ArgumentOutOfRangeExceptionfor invalid values
Best for: infinite scroll, real-time feeds, large datasets. More efficient than offset pagination because it does not scan skipped rows.
// First page — no cursor
var first = await context.Orders
.OrderBy(o => o.Id)
.ToKeysetPagedAsync(o => o.Id, cursor: null, pageSize: 50);
// Next page — pass the cursor from the previous result
var next = await context.Orders
.OrderBy(o => o.Id)
.ToKeysetPagedAsync(o => o.Id, cursor: int.Parse(first.NextCursor!), pageSize: 50);public sealed class KeysetPagedResult<T>
{
public IReadOnlyList<T> Items { get; }
public string? NextCursor { get; } // Pass this to get the next page
public string? PreviousCursor { get; } // The cursor used for this page
public bool HasMore { get; } // Whether more pages exist
}- If a cursor is provided, adds
WHERE key > cursorto the query - Fetches
pageSize + 1rows to determine if there are more pages - Returns the key of the last item as the
NextCursor
| Offset | Keyset | |
|---|---|---|
| Jump to page N | Yes | No |
| Performance on large tables | Degrades with depth | Consistent |
| Infinite scroll | Possible | Ideal |
| Stable with concurrent inserts | Can skip / duplicate rows | Always consistent |
| Requires ordered query | Recommended | Required |