Designing an Abstract Collection Interface for Pagination in C#

moh_moh701

mohamed Tayel

Posted on November 24, 2024

Designing an Abstract Collection Interface for Pagination in C#

In this article, we’ll explore how to design a Paginated Collection interface in C# to manage sorted data and divide it into pages. This design ensures flexibility and efficiency while maintaining clean separation between interfaces and implementations. We’ll walk through the process step by step, providing clear explanations and examples.


Problem Overview

When working with large datasets, we often need to:

  1. Sort the data by specific criteria (e.g., alphabetically, by date, or ID).
  2. Paginate the data, dividing it into manageable chunks (pages).
  3. Ensure the data remains sorted and easily accessible page-by-page.

This is common in scenarios like:

  • Displaying search results.
  • Building paginated reports.
  • Loading data incrementally in web applications.

The challenge is to design a reusable abstraction that hides implementation details but provides powerful functionality for consumers.


Solution: Abstract Interfaces

To create a robust solution, we define two interfaces:

  1. IPaginatedCollection: Represents the collection of pages and provides functionality to iterate through them or access specific pages by index.

  2. IPage: Represents a single page of data, including its content and metadata, such as its position in the collection.


Step 1: Designing the Interfaces

IPaginatedCollection

This interface defines the structure of the entire paginated collection.

/// <summary>
/// Represents a generic paginated collection of data.
/// </summary>
public interface IPaginatedCollection<T> : IEnumerable<IPage<T>>
{
    /// <summary>
    /// Gets the total number of pages in the collection.
    /// </summary>
    int PageCount { get; }

    /// <summary>
    /// Gets the page at the specified (zero-based) index.
    /// </summary>
    IPage<T> this[int index] { get; }
}
Enter fullscreen mode Exit fullscreen mode

IPage

This interface defines the structure of a single page within the collection.

/// <summary>
/// Represents a single page in a paginated collection.
/// </summary>
public interface IPage<T> : IEnumerable<T>
{
    /// <summary>
    /// Gets the 1-based ordinal number of this page.
    /// </summary>
    int Ordinal { get; }

    /// <summary>
    /// Gets the number of elements in this page.
/// </summary>
    int Count { get; }

    /// <summary>
    /// Gets the declared size of the page.
    /// </summary>
    int PageSize { get; }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Implementing the Interfaces

PaginatedCollection

The PaginatedCollection<T> class handles:

  1. Sorting the input data.
  2. Splitting it into pages.
public class PaginatedCollection<T> : IPaginatedCollection<T>
{
    private readonly List<IPage<T>> _pages;

    public PaginatedCollection(IEnumerable<T> source, int pageSize, Func<T, object> sortKeySelector)
    {
        if (pageSize <= 0)
            throw new ArgumentOutOfRangeException(nameof(pageSize), "Page size must be greater than zero.");

        var sortedData = source.OrderBy(sortKeySelector).ToList();
        _pages = new List<IPage<T>>();

        for (int i = 0; i < sortedData.Count; i += pageSize)
        {
            var pageData = sortedData.Skip(i).Take(pageSize).ToList();
            _pages.Add(new Page<T>(pageData, i / pageSize + 1, pageSize));
        }
    }

    public int PageCount => _pages.Count;

    public IPage<T> this[int index] => _pages[index];

    public IEnumerator<IPage<T>> GetEnumerator() => _pages.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Enter fullscreen mode Exit fullscreen mode

Page

The Page<T> class encapsulates:

  1. The items in the page.
  2. Metadata like ordinal position and page size.
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();
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Using the Paginated Collection

Here’s how to use the PaginatedCollection<T> class:

using Pagination;

class Program
{
    static void Main()
    {
        // Sample data: unsorted integers
        var data = new List<int> { 5, 3, 1, 4, 2, 10, 9, 8, 7, 6 };

        // Create a paginated collection
        var paginatedCollection = new PaginatedCollection<int>(
            data,
            pageSize: 3,
            sortKeySelector: x => x // Sort by value
        );

        // Display total pages
        Console.WriteLine($"Total Pages: {paginatedCollection.PageCount}");

        // Iterate through pages
        foreach (var page in paginatedCollection)
        {
            Console.WriteLine($"Page {page.Ordinal}:");
            foreach (var item in page)
            {
                Console.WriteLine(item);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Output

Total Pages: 4
Page 1:
1
2
3
Page 2:
4
5
6
Page 3:
7
8
9
Page 4:
10
Enter fullscreen mode Exit fullscreen mode

Benefits of the Design

  1. Flexibility:

    • Works with any data type (T) and sorting criteria.
    • Lazy execution ensures efficiency for large datasets.
  2. Reusability:

    • Abstractions (IPaginatedCollection<T> and IPage<T>) decouple the interface from implementation.
  3. Scalability:

    • Easily extendable to handle additional requirements (e.g., filtering, caching).
  4. Simplicity:

    • Clear separation of concerns between the PaginatedCollection<T> and Page<T> classes.

Key Takeaways

  1. Design from the outside in: Focus on consumer needs before implementation details.
  2. Use abstractions (interfaces) to define clear contracts for functionality.
  3. Implement features like deferred execution (IEnumerable<T>) for efficient memory and performance management.

With this design, you can easily handle paginated and sorted data in any application, making your solution scalable and maintainable.

💖 💪 🙅 🚩
moh_moh701
mohamed Tayel

Posted on November 24, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related