Learn how to efficiently implement sorted pagination in C# with a reusable SortedListPaginator class. Discover lazy initialization, encapsulated sorting, and flexible pagination design using interfaces like IPaginatedCollection<T> and IPage<T>. Perfect for handling large datasets with optimized performance!
Designing a system to handle paginated, sorted data efficiently requires balancing flexibility, performance, and usability. In this article, we will create a class named SortedListPaginator that implements the IPaginatedCollection<T> interface. The core idea is to encapsulate the logic of sorting and pagination, ensuring that consumers do not need to worry about these details.
Overview of the Design
- 
Input and Sorting: - The class takes an unordered sequence as input.
- It ensures the sequence is sorted based on a custom criterion provided by the caller.
 
- 
Lazy Initialization: - Sorting is deferred until explicitly required, leveraging the Lazy<T>class to optimize performance.
 
- Sorting is deferred until explicitly required, leveraging the 
- 
Read-Only Behavior: - Once sorted, the data becomes read-only, ensuring it can be safely shared across collaborating objects.
 
- 
Pagination: - The sorted data is divided into pages, each represented by a concrete implementation of the IPage<T>interface.
 
- The sorted data is divided into pages, each represented by a concrete implementation of the 
The Key Components
IPaginatedCollection Interface
Defines the contract for the collection, allowing access to sorted pages and their metadata.
public interface IPaginatedCollection<T> : IEnumerable<IPage<T>>
{
    int PageCount { get; }
    IPage<T> this[int index] { get; }
}
IPage Interface
Represents a single page, exposing its content and metadata.
public interface IPage<T> : IEnumerable<T>
{
    int Ordinal { get; }
    int Count { get; }
    int PageSize { get; }
}
SortedListPaginator Class
The SortedListPaginator class encapsulates the logic for sorting and pagination.
Implementing the SortedListPaginator
Class Definition
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
public class SortedListPaginator<T> : IPaginatedCollection<T>
{
    private readonly Lazy<List<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 for sorting
        _sortedData = new Lazy<List<T>>(() => source.OrderBy(sortKeySelector).ToList());
    }
    // Total number of pages
    public int PageCount => (int)Math.Ceiling((double)_sortedData.Value.Count / _pageSize);
    // Access a specific page
    public IPage<T> this[int index]
    {
        get
        {
            if (index < 0 || index >= PageCount)
                throw new ArgumentOutOfRangeException(nameof(index), "Page index is out of range.");
            var start = index * _pageSize;
            var pageData = _sortedData.Value.Skip(start).Take(_pageSize);
            return new Page<T>(pageData, index + 1, _pageSize);
        }
    }
    // Enumerator for pages
    public IEnumerator<IPage<T>> GetEnumerator()
    {
        for (int i = 0; i < PageCount; i++)
        {
            yield return this[i];
        }
    }
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
The Page Class
Each page is represented by a Page<T> object that implements the IPage<T> interface.
public class Page<T> : IPage<T>
{
    private readonly List<T> _content;
    public Page(IEnumerable<T> content, int ordinal, int pageSize)
    {
        _content = content.ToList();
        Ordinal = ordinal;
        PageSize = pageSize;
    }
    public int Ordinal { get; }
    public int Count => _content.Count;
    public int PageSize { get; }
    public IEnumerator<T> GetEnumerator() => _content.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
How It Works
- 
Lazy Initialization with Lazy<T>:- The Lazy<List<T>>ensures that sorting is deferred until the sorted data is accessed for the first time.
- This approach avoids unnecessary computation if no pages are requested.
 
- The 
- 
Sorting in the Constructor: - The sorting logic is encapsulated in a lambda function passed to the Lazy<T>instance.
- The lambda executes the sorting (OrderBy) only once, ensuring efficiency.
 
- The sorting logic is encapsulated in a lambda function passed to the 
- 
Pagination Logic: - The this[int index]property calculates the appropriate segment of the sorted data and returns it as aPage<T>object.
- The Page<T>object provides metadata and an enumerator for its content.
 
- The 
Usage Example
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
Advantages of the Design
- 
Efficiency: - Sorting occurs only once, and only when needed.
- Lazy initialization minimizes overhead.
 
- 
Encapsulation: - Sorting and pagination are fully encapsulated, freeing the consumer from dealing with these complexities.
 
- 
Read-Only Behavior: - The sorted list is immutable, ensuring safe usage across different parts of the application.
 
- 
Flexibility: - The design allows any sorting criterion through the Func<T, object>parameter.
 
- The design allows any sorting criterion through the 
Key Takeaways
- Lazy Initialization is a powerful pattern for deferring costly operations until needed.
- Separating the sorting logic from pagination improves maintainability and scalability.
- Designing with interfaces ensures flexibility and encourages reusable, testable code.
This approach provides an efficient and clean solution for handling sorted, paginated data in any application.
 

 
    
Top comments (0)