In this article, we’re going to learn how to implement paging in ASP.NET Core Web API. Paging (pagination) is one of the most important concepts in building RESTful APIs.
We don’t want to return a collection of all resources when querying our API. That can cause performance issues and it’s in no way optimized for public or private APIs. It can cause massive slowdowns and even application crashes in severe cases.
The source code for this article can be found on the GitHub repo. If you want to follow along with the article, you can use the start branch and if you want to get the final solution or if you get stuck, switch to the end branch.
NOTE: Some degree of previous knowledge is needed to follow this article. It relies heavily on the ASP.NET Core Web API series on Code Maze, so if you are unsure how to set up the database or how the underlying architecture works, we strongly suggest you go through the series.
VIDEO: Paging in ASP.NET Core Web API Video.
We’ll discuss what paging is, the easiest way to implement it, and then improve on that solution to create a more readable and flexible codebase.
Let’s start.
What is Paging?
Paging refers to getting partial results from an API. Imagine having millions of results in the database and having your application try to return all of them at once.
Not only that would be an extremely ineffective way of returning the results, but it could also have devastating effects on the application itself or the hardware it runs on. Moreover, every client has limited memory resources and it needs to restrict the number of shown results.
Thus, we need a way to return a set number of results to the client to avoid these consequences.
Let’s see how we can do that.
Initial Implementation
Before we make any changes to the source code, let’s inspect how it looks right now, and how you would probably begin with any project.
In our case, we have the OwnerController
which does all the necessary actions on the Owner
entity.
One particular action that stands out, and that we need to change is the GetOwners()
action:
[HttpGet] public IActionResult GetOwners() { var owners = _repository.Owner.GetAllOwners(); _logger.LogInfo($"Returned all owners from database."); return Ok(owners); }
Which calls GetOwners()
from OwnerRepository
:
public IEnumerable<Owner> GetOwners() { return FindAll() .OrderBy(ow => ow.Name); }
FindAll() method is just a method from a Base Repository class that returns the whole set of owners.
public IQueryable<T> FindAll() { return this.RepositoryContext.Set<T>(); }
As you can see it’s a straightforward action, meant to return all the owners from the database ordered by name.
And it does just that.
But, in our case, that’s just a few account owners (five). What if there were thousands or even millions of people in the database (you wish, but still, imagine another kind of entity)? End then add to that, a few thousand API consumers.
We would end up with a very long query that returns A LOT of data.
The best-case scenario would be that you started with a small number of owners that increased slowly over time so you can notice the slow decline in performance. Other scenarios are far less benign for your application and machines (imagine hosting it in the cloud and not having proper caching in place).
So, having that in mind, let’s modify this method to support paging.
Paging Implementation
Mind you, we don’t want to change the base repository logic or implement any business logic in the controller.
What we want to achieve is something like this: https://localhost:5001/api/owners?pageNumber=2&pageSize=2
. This should return the second set of two owners from our database.
We also want to constraint our API not to return all the owners even if someone calls https://localhost:5001/api/owners
.
Let’s start by changing the controller:
[HttpGet] public IActionResult GetOwners([FromQuery] OwnerParameters ownerParameters) { var owners = _repository.Owner.GetOwners(ownerParameters); _logger.LogInfo($"Returned {owners.Count()} owners from database."); return Ok(owners); }
A few things to take note:
- We’re calling the
GetOwners
method from theOwnerRepository
, which doesn’t exist yet, but we’ll implement it soon - We’re using
[FromQuery]
to point out that we’ll be using query parameters to define which page and how many owners we are requesting OwnerParameters
class is the container for the actual parameters
We also need to create the OwnerParameters
class since we are passing it as an argument to our controller. Let’s create it in the Models
folder of the Entities
project:
public class OwnerParameters { const int maxPageSize = 50; public int PageNumber { get; set; } = 1; private int _pageSize = 10; public int PageSize { get { return _pageSize; } set { _pageSize = (value > maxPageSize) ? maxPageSize : value; } } }
We are using constant maxPageSize
to restrict our API to a maximum of 50 owners. We have two public properties – PageNumber
and PageSize
. If not set by the caller, PageNumber
will be set to 1, and PageSize to 10.
Now, let’s implement the most important part, the repository logic.
We need to extend the GetOwners()
method in the IOwnerRepository
interface and the OwnerRepository
class:
public interface IOwnerRepository : IRepositoryBase<Owner> { IEnumerable<Owner> GetOwners(OwnerParameters ownerParameters); Owner GetOwnerById(Guid ownerId); OwnerExtended GetOwnerWithDetails(Guid ownerId); void CreateOwner(Owner owner); void UpdateOwner(Owner dbOwner, Owner owner); void DeleteOwner(Owner owner); }
And the logic:
public IEnumerable<Owner> GetOwners(OwnerParameters ownerParameters) { return FindAll() .OrderBy(on => on.Name) .Skip((ownerParameters.PageNumber - 1) * ownerParameters.PageSize) .Take(ownerParameters.PageSize) .ToList(); }
Ok, the easiest way to explain this is by example.
Say we need to get the results for the third page of our website, counting 20 as the number of results we want. That would mean we want to skip the first ((3 – 1) * 20) = 40 results, and then take the next 20 and return them to the caller.
One more thing. You could ask why we call the FindAll()
method to return all the data from the database and then apply parameters to that result. We’ve explained that in our Ultimate ASP.NET Core Web API book, and have shown another example by sending parameters to the database directly. The bottom line is that both examples are correct depending on the total amount of data in the database. You have to test it and choose which one is faster for your database.
Does that make sense?
Testing the Solution
Now, in our database we only have a few owners, so let’s try something like this:
https://localhost:5001/api/owners?pageNumber=2&pageSize=2
This should return the next subset of owners:
[ { "id": "66774006-2371-4d5b-8518-2177bcf3f73e", "name": "Nick Somion", "dateOfBirth": "1998-12-15T00:00:00", "address": "North sunny address 102" }, { "id": "a3c1880c-674c-4d18-8f91-5d3608a2c937", "name": "Sam Query", "dateOfBirth": "1990-04-22T00:00:00", "address": "91 Western Roads" } ]
If that’s what you got, you’re on the right track.
Now, what can we do to improve this solution?
Improving the Solution
Since we’re returning just a subset of results to the caller, we might as well have a PagedList
instead of List
.
PagedList
will inherit from the List
class and will add some more to it. We can also, move the skip/take logic to the PagedList
since it makes more sense.
Let’s implement it.
Implementing PagedList Class
We don’t want our skip/take logic implemented inside our repository:
public class PagedList<T> : List<T> { public int CurrentPage { get; private set; } public int TotalPages { get; private set; } public int PageSize { get; private set; } public int TotalCount { get; private set; } public bool HasPrevious => CurrentPage > 1; public bool HasNext => CurrentPage < TotalPages; public PagedList(List<T> items, int count, int pageNumber, int pageSize) { TotalCount = count; PageSize = pageSize; CurrentPage = pageNumber; TotalPages = (int)Math.Ceiling(count / (double)pageSize); AddRange(items); } public static PagedList<T> ToPagedList(IQueryable<T> source, int pageNumber, int pageSize) { var count = source.Count(); var items = source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToList(); return new PagedList<T>(items, count, pageNumber, pageSize); } }
As you can see, we’ve transferred the skip/take logic to the static method inside the PagedList
class. We’ve added a few more properties, that will come in handy as metadata for our response.
HasPrevious
is true if CurrentPage
is larger than 1, and HasNext
is calculated if CurrentPage
is smaller than the number of total pages. TotalPages
is calculated by dividing the number of items by the page size and then rounding it to the larger number since a page needs to exist even if there is one item on it.
Now that we’ve cleared that out, let’s change our OwnerRepository
and OwnerController
accordingly.
First, we need to change our repo (don’t forget to change the interface too):
public PagedList<Owner> GetOwners(OwnerParameters ownerParameters) { return PagedList<Owner>.ToPagedList(FindAll().OrderBy(on => on.Name), ownerParameters.PageNumber, ownerParameters.PageSize); }
And then the controller:
[HttpGet] public IActionResult GetOwners([FromQuery] OwnerParameters ownerParameters) { var owners = _repository.Owner.GetOwners(ownerParameters); var metadata = new { owners.TotalCount, owners.PageSize, owners.CurrentPage, owners.TotalPages, owners.HasNext, owners.HasPrevious }; Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(metadata)); _logger.LogInfo($"Returned {owners.TotalCount} owners from database."); return Ok(owners); }
Now, if we send the same request as we did earlier https://localhost:5001/api/owners?pageNumber=2&pageSize=2
, we get the same result:
[ { "id": "f98e4d74-0f68-4aac-89fd-047f1aaca6b6", "name": "Martin Miller", "dateOfBirth": "1983-05-21T00:00:00", "address": "3 Edgar Buildings" }, { "id": "66774006-2371-4d5b-8518-2177bcf3f73e", "name": "Nick Somion", "dateOfBirth": "1998-12-15T00:00:00", "address": "North sunny address 102" } ]
But now we have some additional useful information in X-Pagination
response header:
As you can see, all of our metadata is here. We can use this information when building any kind of frontend pagination functionality. You can play around with different requests to see how it works in other scenarios.
There is one more thing we can do to make our solution even more generic. We have the OwnerParameters
class, but what if we want to use it in our AccountController
? Parameters that we send to the Account controller might be different. Maybe not for paging, but we’ll send a bunch of different parameters later on and we need to separate the parameter classes.
Let’s see how to improve it.
Creating a Parent Parameters Class
First, let’s create an abstract class QueryStringParameters
. We’ll use this class to implement mutually used functionalities for every parameter class we will implement. And since we have OwnerController
and AccountController
, which means we need to create OwnerParameters
and AccountParameters
classes.
Let’s start by defining QueryStringParameters
class inside the Models folder of the Entities project:
public abstract class QueryStringParameters { const int maxPageSize = 50; public int PageNumber { get; set; } = 1; private int _pageSize = 10; public int PageSize { get { return _pageSize; } set { _pageSize = (value > maxPageSize) ? maxPageSize : value; } } }
We’ve also moved our paging logic inside the class since it will be valid for any entity we might want to return through the repository.
Now, we need to create AccountParameters
class, and then inherit the QueryStringParameters
class in both the OwnerParameters and the AccountParameters classes.
Remove the logic from OwnerParameters
and inherit QueryStringParameters
:
public class OwnerParameters : QueryStringParameters { }
And create AccountParameters
class inside the Models folder too:
public class AccountParameters : QueryStringParameters { }
Now, these classes look a bit empty, but soon we’ll be populating them with other useful parameters and we’ll see what the real benefit is. For now, it’s important that we have a way to send a different set of parameters for AccountController
and OwnerController
.
Now we can do something like this too, inside our AccountController
:
[HttpGet] public IActionResult GetAccountsForOwner(Guid ownerId, [FromQuery] AccountParameters parameters) { var accounts = _repository.Account.GetAccountsByOwner(ownerId, parameters); var metadata = new { accounts.TotalCount, accounts.PageSize, accounts.CurrentPage, accounts.TotalPages, accounts.HasNext, accounts.HasPrevious }; Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(metadata)); _logger.LogInfo($"Returned {accounts.TotalCount} owners from database."); return Ok(accounts); }
Due to the inheritance of the paging parameters through the QueryStringParameters
class, we get the same behavior.
Conclusion
Paging is a useful and important concept in building any API out there. Without it, our application would slow down considerably or just drop dead.
The solution we’ve implemented is not perfect, far from it, but you got the point. We’ve isolated different parts of the paging mechanism and we can go even further and make it more generic. But you can do it as an exercise and implement it in your project. You can also find one front-end application of paging in our Angular Material paging article.
In this article we’ve covered:
- The easiest way to implement pagination in ASP.NET Core Web API
- Tested the solution in a real-world scenario
- Improved that solution by introducing
PagedList
entity and separated our parameters for different controllers
Hope you liked this article and you’ve learned something new or useful from it. In the next article, we’re going to cover filtering.