Introducing Paginator.NET

There are many existing pagination solutions for UI frameworks, but few solutions for paginating data in memory within the .NET runtime environment. In many use cases data sets are filtered down to a small size before they are even fetched from the database. However, sometimes we might need APIs that could potentially return large sets of data. For example, a search tool could return hundreds or even thousands of matches, and in this scenario it would be useful to get the data from the database one page at a time.

.NET has built in types like IEnumerable, ICollection, IQueryable, IList, etc but nothing that handles pagination, so I came up with a new interface called IPagedCollection.

public interface IPagedCollection<T> : IEnumerable<T>
{
	int PageSize { get; }
	int PageCount { get; }
	int ItemCount { get; }

	IEnumerable<T> GetPage(int pageNumber);
	Dictionary<int, IEnumerable<T>> GetAllPages();
	IEnumerable<T> GetItems(int pageNumber);
	IEnumerable<T> GetAllItems();
	IEnumerable<T> this[int page] { get; }
}

Notice this interface inherits IEnumerable<T> which allows us to gradually introduce breaking changes if our API currently returns IEnumerable<T>.

IPagedCollection<T> gives us everything we need to get the data paginated, but there is a problem, when we get a page we have no way of knowing any context around that page, which page number it was, how many more pages there are, etc. When we pass the results back, the UI consuming the API will need to know this information so it can render page navigation components.

So I introduced a type called PagedResult that contains the pages data and the paging context around it.

public class PagedResult<T>
{
	public int CurrentPageNumber { get; }
	public int PageCount { get; }
	public int PageSize { get; }
	public int TotalItemCount { get; }
	public IEnumerable<T> Results { get; }
}

Now we can modify the IPagedCollection interface to return PagedResult<T> instead of IEnumerable<T>.

public interface IPagedCollection<T> : IEnumerable<T>
{
	int PageSize { get; }
	int PageCount { get; }
	int ItemCount { get; }

	PagedResult<T> GetPage(int pageNumber);
	IEnumerable<PagedResult<T>> GetAllPages();
	IEnumerable<T> GetItems(int pageNumber);
	IEnumerable<T> GetAllItems();
	PagedResult<T> this[int page] { get; }
}

But how does this solve the problem of only fetching the data we need (i.e. the specified page) from the database? Won’t all the data be enumerated before it is paginated? Well, it depends on what we pass into an instance of IPagedCollection. If we pass a List<T> for example then the contents of the list are already in memory, but if we pass in IEnumerable<T> we can use LINQ to convert it to IQueryable<T> using the AsQueryable() method. Then if the data passed into the paged collection implements IQueryable, say if we are using Entity Framework, we get the benefit of LINQ to SQL when we call the Skip() and Take() methods.

That’s great, but what if I want to get my data asynchronously? No problem, let’s introduce an async version of our interface, which can take an IAsyncEnumerable<T>.

public interface IAsyncPagedCollection<T>
{
	int PageSize { get; }
	
	Task<int> GetPageCountAsync(CancellationToken cancellationToken = default);

	Task<PagedResult<T>> GetPageAsync
    (
        int pageNumber,
        CancellationToken cancellationToken = default
    );

	Task<IEnumerable<PagedResult<T>>> GetAllPagesAsync
    (
        CancellationToken cancellationToken = default
    );

	Task<int> GetItemCountAsync(CancellationToken cancellationToken = default);

	Task<IEnumerable<T>> GetItemsAsync
    (
        int pageNumber,
        CancellationToken cancellationToken = default
    );

	Task<IEnumerable<T>> GetAllItemsAsync
    (
        CancellationToken cancellationToken = default
    );
}

I’ve made this solution into an open source project called Paginator.NET. If you want to view the full source code, I have committed it to a GitHub repository and if you want to use it in your projects I have published three NuGet packages: