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:
- Sort the data by specific criteria (e.g., alphabetically, by date, or ID).
- Paginate the data, dividing it into manageable chunks (pages).
- 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:
IPaginatedCollection: Represents the collection of pages and provides functionality to iterate through them or access specific pages by index.
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; }
}
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; }
}
Step 2: Implementing the Interfaces
PaginatedCollection
The PaginatedCollection<T>
class handles:
- Sorting the input data.
- 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();
}
Page
The Page<T>
class encapsulates:
- The items in the page.
- 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();
}
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);
}
}
}
}
Output
Total Pages: 4
Page 1:
1
2
3
Page 2:
4
5
6
Page 3:
7
8
9
Page 4:
10
Benefits of the Design
-
Flexibility:
- Works with any data type (
T
) and sorting criteria. - Lazy execution ensures efficiency for large datasets.
- Works with any data type (
-
Reusability:
- Abstractions (
IPaginatedCollection<T>
andIPage<T>
) decouple the interface from implementation.
- Abstractions (
-
Scalability:
- Easily extendable to handle additional requirements (e.g., filtering, caching).
-
Simplicity:
- Clear separation of concerns between the
PaginatedCollection<T>
andPage<T>
classes.
- Clear separation of concerns between the
Key Takeaways
- Design from the outside in: Focus on consumer needs before implementation details.
- Use abstractions (
interfaces
) to define clear contracts for functionality. - 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.
Top comments (0)