Pagination is the process of dividing a set into discrete pages. In the context of Entity Framework, that means we are only getting a certain amount of entries from the database.
And we will implement a very easy solution to make that happen in 3 steps.
Not only that, pagination should also allow us to get the possible amount of entries in the database so that we can indicate to the user: "Hey, there is a next page!".
1. Create the object
First we need some kind of object that a) holds all of our objects and b) holds the meta information:
public class PagedList<T> : IReadOnlyList<T>
{
private readonly IList<T> subset;
public PagedList(IEnumerable<T> items, int count, int pageNumber, int pageSize)
{
PageNumber = pageNumber;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
subset = items as IList<T> ?? new List<T>(items);
}
public int PageNumber { get; }
public int TotalPages { get; }
public bool IsFirstPage => PageNumber == 1;
public bool IsLastPage => PageNumber == TotalPages;
public int Count => subset.Count;
public T this[int index] => subset[index];
public IEnumerator<T> GetEnumerator() => subset.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => subset.GetEnumerator();
}
A few notes here:
- We implement
IReadOnlyList
so the consumer has no way of tempering with the data itself - It also gives us the possibility to expose an indexer
myList[2]
- The more information you need, the more you can expose here. I just started with some properties like
IsFirstPage
andPageNumber
. You could also expose the total number of pages if you wish - The underlying data structure is an internal list, that holds all of our information
- A small remark to the constructor:
items as IList<T>
is quite unsafe if used in the wrong context. If someone from the outside would add objects toitems
our internal list would see those changes as well. So make sure that this constructor is only used by the upcoming method I'll show you. An easy way of ensuring that is to make the constructor internal and place it next to the extension method (I'll show in a few seconds) in a separate assembly.
2. Extension method
To make the integration as easy as possible we will use an extension method:
public static class PagedListQueryableExtensions
{
public static async Task<PagedList<T>> ToPagedListAsync<T>(
this IQueryable<T> source,
int page,
int pageSize,
CancellationToken token = default)
{
var count = await source.CountAsync(token);
if (count > 0)
{
var items = await source
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(token);
return new PagedList<T>(items, count, page, pageSize);
}
return new(Enumerable.Empty<T>(), 0, 0, 0);
}
}
Remarks here:
- Super easy we sit on top of
IQueryable
so the usage will be extremely simple - As said earlier, we retrieve the count from the database to determine if we have other pages
- You might want to change
ToListAsync
toToList
if you have aNVARCHAR(MAX)
or binary column in your table. There is a open bug about that: https://stackoverflow.com/questions/28543293/entity-framework-async-operation-takes-ten-times-as-long-to-complete/28619983
3. Usage
Now the easy part - how to use that. As we used an extension method, we can just use ToPagedListAsync
as last call instead of ToListAsync
in our db context.
var pagedList = await db.BlogPosts.ToPagedListAsync(1, 15);
You can for sure also use it in combination with all the other LINQ methods:
await db.BlogPosts
.Where(bp => bp.IsPublished)
.Where(bp => bp.Title.Contains(searchTerm))
.OrderBy(bp => bp.PublishDate)
.ToPagedListAsync(1, 15);
Of course, I only loaded here the first page. If you have a website and the user wants to have the next page, you can load this via ToPagedListAsync(2, 15);
and so on. You can also disable or enable buttons based on whether or not there is a previous or next page.
Extra: IEnumerable
The shown code works with Entity Framework but one could make it easily possible to work with types like IEnumerable<T>
. You only have to adopt the extension method, or add a new one:
public static PagedList<T> ToPagedList<T>(
this IEnumerable<T> source,
int page,
int pageSize)
{
var count = await source.Count(token);
if (count > 0)
{
var items = source
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
return new PagedList<T>(items, count, page, pageSize);
}
return new(Enumerable.Empty<T>(), 0, 0, 0);
}
This implementation uses your goto stuff from the System.Linq
namespace.
Extra: RavenDB
You can do the same with RavenDB as well. Here instead of IQueryable<T>
we are using IRavenQueryable<T>
:
public static async Task<IPagedList<T>> ToPagedListAsync<T>(
this IRavenQueryable<T> source,
int pageIndex, int pageSize,
CancellationToken token = default)
{
var count = await source.CountAsync(token);
if (count > 0)
{
var items = await source
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.ToListAsync(token);
return new PagedList<T>(items, count, pageIndex, pageSize);
}
return PagedList<T>.Empty;
}
Keep in mind that you have to have an asynchronous document session: using var session = documentStore.OpenAsyncSession();
.
Conclusion
Pagination doesn't have to be difficult. Here I showed an easy way of implementing the backend logic for pagination. This blog post basically uses a similar implementation to what I showed you. As I have multiple storage providers available like (RavenDb
, SqlServer
), there are also multiple extensions for the respective implementation.