Easy Pagination for Entity Framework in 3 steps

1/11/2023
5 minute read

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 and PageNumber. 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 to items 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:

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.

Resources

  • Source code to this blog post: here
  • All my sample code is hosted in this repository: here

Entity Framework and ordered indexes

In Entity Framework 7, the team has added support for ordered indexes to the fluent API. In this blog post we will look at how to use this feature and what it means for your database.

Entity Framework - Storing complex objects as JSON

From time to time, it is nice to store complex objects or lists as JSON in the database. With Entity Framework 8, this is now easily possible. But this was possible all along with Entity Framework 7.

Entity Framework 8: Raw SQL queries on unmapped types

The next iteration of Entity Framework, namely Entity Framework 8, will have a new and exciting feature:

Support raw SQL queries without defining an entity type for the result

That means less boilerplate code!

An error has occurred. This application may no longer respond until reloaded. Reload x