Isaac.

Pagination Strategies

Implement efficient pagination for large datasets.

By EMEPublished: February 20, 2025
paginationoffset limitcursorperformance

A Simple Analogy

Pagination is like dividing a book into chapters. Instead of loading the whole book, users get one chapter at a time.


Why Pagination?

  • Performance: Don't load all data
  • Bandwidth: Reduce transfer size
  • UX: Faster page loads
  • Database: Less query overhead
  • Scalability: Handle large datasets

Offset/Limit

public async Task<PagedResult<ProductDto>> GetProducts(int page, int pageSize = 10)
{
    var skip = (page - 1) * pageSize;
    
    var total = await _db.Products.CountAsync();
    var items = await _db.Products
        .OrderBy(p => p.Id)
        .Skip(skip)
        .Take(pageSize)
        .ToListAsync();
    
    return new PagedResult<ProductDto>
    {
        Items = items,
        Page = page,
        PageSize = pageSize,
        Total = total,
        TotalPages = (total + pageSize - 1) / pageSize
    };
}

Cursor-Based Pagination

public async Task<CursorPage<ProductDto>> GetProductsAsync(string cursor = null, int limit = 10)
{
    var query = _db.Products.AsQueryable();
    
    if (!string.IsNullOrEmpty(cursor))
    {
        var cursorId = long.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(cursor)));
        query = query.Where(p => p.Id > cursorId);
    }
    
    var items = await query
        .OrderBy(p => p.Id)
        .Take(limit + 1)
        .ToListAsync();
    
    var hasMore = items.Count > limit;
    items = items.Take(limit).ToList();
    
    var nextCursor = hasMore ? items.Last().Id : null;
    
    return new CursorPage<ProductDto>
    {
        Items = items,
        NextCursor = nextCursor != null 
            ? Convert.ToBase64String(Encoding.UTF8.GetBytes(nextCursor.ToString()))
            : null,
        HasMore = hasMore
    };
}

Request/Response

// Request
[HttpGet("products")]
public async Task<IActionResult> GetProducts(
    [FromQuery] int page = 1,
    [FromQuery] int pageSize = 10)
{
    return Ok(await _service.GetProducts(page, pageSize));
}

// Response
{
    "items": [ ... ],
    "page": 1,
    "pageSize": 10,
    "total": 500,
    "totalPages": 50
}

Best Practices

  1. Limit max size: Cap pageSize at 100
  2. Default reasonable: Start with 10-20 items
  3. Total count: Include for UI
  4. Sorting: Consistent ordering required
  5. Index: Add database indexes on sort columns

Related Concepts

  • Infinite scroll
  • Load more
  • Lazy loading
  • Virtual scrolling

Summary

Implement pagination with offset/limit for simple cases or cursor-based for high-performance APIs.