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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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 the OwnerRepository, 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:

postman response headers paging

Response headers

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!