Learn how to implement an efficient and safe pagination system in C# using a projection-based design. Discover how to create immutable, read-only pages with dynamic offsets, leveraging IReadOnlyList<T> for consistent and scalable data handling. Perfect for optimizing large datasets!
In this article, we’ll implement a list projection design for pagination, where each page acts as a "window" into a shared, immutable list. This approach ensures efficiency by avoiding unnecessary copying of list segments and provides safety by leveraging read-only collections.
Key Concepts
- 
Projection Design: - Each page is a view of a portion of a shared list, defined by its starting (lower offset) and ending (upper offset) positions.
- Pages share the same underlying list but expose different sections of it based on their ordinal position.
 
- 
Read-Only List: - To prevent accidental mutation of the shared list, we use a read-only wrapper.
- This ensures consistency across all pages and avoids potential bugs caused by list modifications.
 
- 
Efficient Calculations: - Page metadata (e.g., Count, offsets) is calculated dynamically, ensuring that page objects are lightweight.
 
- Page metadata (e.g., 
- 
Lazy Enumeration: - Items in a page are enumerated lazily using yield return, providing efficient access to the underlying list.
 
- Items in a page are enumerated lazily using 
Implementation Plan
We will:
- Create a class Page<T>implementing theIPage<T>interface.
- Use a read-only wrapper for the shared list to ensure immutability.
- Calculate offsets dynamically based on the page’s ordinal position and page size.
- Implement lazy enumeration for accessing items within the page.
Code Implementation
The Page Class
using System;
using System.Collections;
using System.Collections.Generic;
public class Page<T> : IPage<T>
{
    private readonly IReadOnlyList<T> _source;
    private readonly int _offset;
    private readonly int _upperOffset;
    public Page(IReadOnlyList<T> source, int ordinal, int pageSize)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (ordinal < 1) throw new ArgumentOutOfRangeException(nameof(ordinal), "Ordinal must be 1 or greater.");
        if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize), "Page size must be greater than zero.");
        _source = source;
        // Calculate lower and upper offsets
        _offset = (ordinal - 1) * pageSize;
        _upperOffset = Math.Min(_offset + pageSize, _source.Count);
        // Ensure offset is valid
        if (_offset >= _source.Count)
        {
            _offset = _source.Count;
            _upperOffset = _source.Count;
        }
        Ordinal = ordinal;
        PageSize = pageSize;
    }
    public int Ordinal { get; }
    public int Count => _upperOffset - _offset;
    public int PageSize { get; }
    // Lazy enumeration of items
    public IEnumerator<T> GetEnumerator()
    {
        for (int i = _offset; i < _upperOffset; i++)
        {
            yield return _source[i];
        }
    }
    // Non-generic enumerator
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Explanation of the Code
- 
Read-Only List: - The _sourceis anIReadOnlyList<T>, ensuring that the underlying list cannot be modified.
- This guarantees that all page objects reflect the same, immutable dataset.
 
- The 
- 
Offset Calculations: - 
_offset: The starting index for the current page, calculated as(Ordinal - 1) * PageSize.
- 
_upperOffset: The exclusive upper index, capped at the size of the list to handle cases where the page exceeds the list's bounds.
 
- 
- 
Count Calculation: - The Countproperty dynamically calculates the number of items in the page as_upperOffset - _offset.
 
- The 
- 
Lazy Enumeration: - The GetEnumeratormethod usesyield returnto lazily return items within the calculated offsets.
- This approach avoids creating unnecessary collections and ensures efficient memory usage.
 
- The 
Integrating with the Paginated Collection
The SortedListPaginator class will create instances of Page<T> as projections onto the shared, sorted list.
Updated SortedListPaginator Class
public class SortedListPaginator<T> : IPaginatedCollection<T>
{
    private readonly Lazy<IReadOnlyList<T>> _sortedData;
    private readonly int _pageSize;
    public SortedListPaginator(IEnumerable<T> source, int pageSize, Func<T, object> sortKeySelector)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize), "Page size must be greater than zero.");
        if (sortKeySelector == null) throw new ArgumentNullException(nameof(sortKeySelector));
        _pageSize = pageSize;
        // Lazy initialization of sorted read-only list
        _sortedData = new Lazy<IReadOnlyList<T>>(() => source.OrderBy(sortKeySelector).ToList().AsReadOnly());
    }
    public int PageCount => (int)Math.Ceiling((double)_sortedData.Value.Count / _pageSize);
    public IPage<T> this[int index]
    {
        get
        {
            if (index < 0 || index >= PageCount)
                throw new ArgumentOutOfRangeException(nameof(index), "Page index is out of range.");
            return new Page<T>(_sortedData.Value, index + 1, _pageSize);
        }
    }
    public IEnumerator<IPage<T>> GetEnumerator()
    {
        for (int i = 0; i < PageCount; i++)
        {
            yield return this[i];
        }
    }
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Example Usage
class Program
{
    static void Main()
    {
        // Sample data: unsorted numbers
        var data = new List<int> { 5, 3, 1, 4, 2, 10, 9, 8, 7, 6 };
        // Create a SortedListPaginator
        var paginator = new SortedListPaginator<int>(
            data,
            pageSize: 3,
            sortKeySelector: x => x // Sort by value
        );
        Console.WriteLine($"Total Pages: {paginator.PageCount}");
        foreach (var page in paginator)
        {
            Console.WriteLine($"Page {page.Ordinal}:");
            foreach (var item in page)
            {
                Console.WriteLine(item);
            }
        }
    }
}
Output
Total Pages: 4
Page 1:
1
2
3
Page 2:
4
5
6
Page 3:
7
8
9
Page 4:
10
Key Benefits of the Projection Design
- 
Efficiency: - No copying of data into new collections.
- Pages directly reference segments of the shared list.
 
- 
Safety: - The use of IReadOnlyList<T>ensures immutability, preventing accidental modifications.
 
- The use of 
- 
Simplicity: - Offsets and counts are dynamically calculated, reducing complexity and avoiding indexing errors.
 
- 
Scalability: - This approach works seamlessly with large datasets and integrates well with other components.
 
Conclusion
The projection-based pagination design provides a powerful, efficient, and safe way to handle paginated data. By sharing a read-only list among page objects, we minimize memory usage and maintain consistency. This approach ensures that sorting and pagination remain encapsulated, offering a clean, reusable solution for developers.
 

 
    
Top comments (0)