Skip to content

Pagination and Sorting

What happens when you return 100,000 products at once? The client runs out of memory, the network becomes a bottleneck, and users have to scroll endlessly. Data must be sliced into appropriate sizes for delivery. This chapter covers the differences between Offset-based and Cursor (Keyset)-based pagination, and multi-field sorting using SortExpression.


After completing this chapter, you will be able to:

  1. Compose Offset-based pagination with PageRequest and PagedResult
  2. Compose Keyset-based pagination with CursorPageRequest and CursorPagedResult
  3. Express multi-field sorting with SortExpression’s fluent API
  4. Evaluate the trade-offs of Offset and Cursor pagination to choose the appropriate approach

”Why Is This Needed?” — Returning All Data at Once

Section titled “”Why Is This Needed?” — Returning All Data at Once”

Without pagination, returning all data is fine when there are 1,000 records but response time grows to tens of seconds at 100,000 records. There are two approaches: Offset navigates by page number, and Cursor navigates by “starting from after this item.” Let’s examine the pros and cons of each.

The Offset approach requests data as “from position N, get M items.”

PageRequest(page: 2, pageSize: 10) -> OFFSET 10 LIMIT 10
TypeProperties
PageRequestPage, PageSize, Skip
PagedResult<T>Items, TotalCount, TotalPages, HasPreviousPage, HasNextPage
  • Advantage: Can jump to a specific page directly, page numbers can be displayed in UI
  • Disadvantage: Performance degrades on deep pages (slower as OFFSET grows)

The Cursor approach requests data as “from after this cursor, get M items.”

CursorPageRequest(after: "cursor-value", pageSize: 10) -> WHERE id > 'cursor-value' LIMIT 10
TypeProperties
CursorPageRequestAfter, Before, PageSize
CursorPagedResult<T>Items, NextCursor, PrevCursor, HasMore
  • Advantage: O(1) performance even on deep pages, suitable for real-time data
  • Disadvantage: Cannot jump to a specific page directly, only “next/previous” navigation

Sorting must be controlled alongside pagination. SortExpression expresses multi-field sorting with a fluent API.

// Single field sorting
SortExpression.By("Name")
// Multi-field sorting (fluent API)
SortExpression.By("Category").ThenBy("Price", SortDirection.Descending)
// Empty sort (uses default sorting)
SortExpression.Empty

Which approach to choose depends on data characteristics and UI requirements.

CriterionOffsetCursor
Deep Page PerformanceO(N)O(1)
Jump to Specific PagePossibleNot possible
Real-time DataMay have duplicates/gapsStable
SQLLIMIT/OFFSETWHERE + LIMIT
UIPage numbers”Load more” button

Provides helper methods for creating PagedResult and CursorPagedResult, and sorting methods applying SortExpression. Simplifies the behavior of InMemoryQueryBase for demonstration.


ItemDescription
PageRequestOffset-based pagination request (Page, PageSize)
PagedResultOffset-based result (TotalCount, TotalPages, HasNext/Prev)
CursorPageRequestKeyset-based pagination request (After, Before, PageSize)
CursorPagedResultKeyset-based result (NextCursor, PrevCursor, HasMore)
SortExpressionMulti-field sort expression (By/ThenBy fluent API)

A: For most admin pages (boards, product lists), Offset is suitable. For infinite scroll, real-time feeds, and large datasets, Cursor is suitable. Functorium’s IQueryPort supports both.

Q2: Is there a maximum value for PageSize in PageRequest?

Section titled “Q2: Is there a maximum value for PageSize in PageRequest?”

A: It’s limited to MaxPageSize (10,000). Requesting a larger value is automatically adjusted to MaxPageSize.

A: When the client doesn’t specify sorting, passing Empty applies the Query Adapter’s DefaultSortField.


We’ve defined pagination and sorting. Now we need to actually implement these interfaces. Can we test before integrating with Dapper? In the next chapter, we’ll look at quickly validating with InMemory Query Adapter without a DB.

-> Chapter 4: InMemory Query Adapter