Updated on
Pagination in an ASP.NET Core Web API means the client asks for a slice of a collection, ?pageNumber=2&pageSize=10, and the API answers with those ten records plus the numbers needed to ask for the next ten.
Two things have to be built. A parameters class binds the query string and clamps the page size so nobody can ask for everything; a repository method turns the page number into a Skip and a Take before the query reaches the database.
The metadata is the part that is easy to skip and hard to do without. A page of ten records tells the client nothing about whether there are eleven or eleven million, so the total count and the page count travel back in a response header alongside the records.
This article picks up where POST, PUT, and DELETE in ASP.NET Core Web API leaves off, and turns that API’s collection endpoint into a paged one.
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 With Onion Architecture.
What Is Paging in a REST API?
Paging returns one slice of a collection instead of all of it. The client asks for a page number and a page size, and the API answers with that many records plus enough information to ask for the next slice.
Two numbers do the work. pageNumber says which slice and pageSize says how big, and the API turns them into a Skip and a Take before the query ever reaches the database.
The API, not the client, sets the limits. A missing pageSize falls back to a default, and a pageSize of ten thousand is clamped to a maximum the server chooses. Without that clamp the parameter is just a slower way to ask for everything.
Paging is also where a response stops being only data. A page of records says nothing about how many records exist or whether another page follows, so that metadata has to travel back alongside them.
What Goes Wrong Without Paging?
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.GetOwners();
_logger.LogInfo("Returned all owners from database.");
return Ok(owners);
}
Which calls GetOwners() from OwnerRepository:
public IEnumerable<Owner> GetOwners() =>
FindAll()
.OrderBy(o => o.Name);
The FindAll() method is just a method from a base repository class that returns the whole set of owners.
public IQueryable<T> FindAll() =>
RepositoryContext.Set<T>()
.AsNoTracking();
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)? And 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.
How Do We Implement Paging in ASP.NET Core Web API?
Paging in ASP.NET Core Web API takes three pieces: a parameters class the query string binds to, a repository method that applies Skip and Take, and a response that carries the page metadata.
Declare a parameters class with PageNumber and PageSize, clamp PageSize to a maximum, and bind it on the action with [FromQuery]. One base class then serves every controller through inheritance.
In the repository, order the query first and then apply Skip((PageNumber - 1) * PageSize) and Take(PageSize). Ordering is not optional. Without an OrderBy the database is free to return matching rows in whatever order its plan produces, so the contents of page two are not guaranteed to be consistent with page one.
Serialize the page metadata into a response header and return the records as the body. The body stays a plain JSON array, which is what a client expects a collection endpoint to give it.
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 constrain 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 async Task<ActionResult<IEnumerable<Owner>>> GetOwners([FromQuery] OwnerParameters ownerParameters)
{
var owners = await _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
GetOwnersmethod 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 OwnerParametersclass 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;
private int _pageNumber = 1;
private int _pageSize = 10;
public int PageNumber
{
get => _pageNumber;
set => _pageNumber = Math.Max(value, 1);
}
public int PageSize
{
get => _pageSize;
set => _pageSize = Math.Clamp(value, 1, MaxPageSize);
}
}
The MaxPageSize constant restricts our API to a maximum of 50 owners per page. If the caller sends nothing, PageNumber is 1 and PageSize is 10.
The clamping matters more than it looks. Math.Clamp(value, 1, MaxPageSize) keeps ?pageSize=10000 from becoming a slower way of asking for everything, and it also rejects ?pageSize=0, which would otherwise return an empty page forever. Math.Max(value, 1) does the same job at the other end: without it, ?pageNumber=0 produces Skip(-10), and SQL Server refuses a negative OFFSET outright, so the request fails with a 500 rather than an empty result.
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>
{
Task<IEnumerable<Owner>> GetOwners(OwnerParameters ownerParameters);
Owner GetOwnerById(Guid ownerId);
void CreateOwner(Owner owner);
void UpdateOwner(Owner dbOwner, Owner owner);
void DeleteOwner(Owner owner);
}
And the logic:
public async Task<IEnumerable<Owner>> GetOwners(OwnerParameters ownerParameters) =>
await FindAll()
.OrderBy(o => o.Name)
.Skip((ownerParameters.PageNumber - 1) * ownerParameters.PageSize)
.Take(ownerParameters.PageSize)
.ToListAsync();
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 before applying the paging parameters to its result. FindAll() returns an IQueryable<Owner>, so nothing has been fetched from the database at that point and the OrderBy, Skip and Take all compose into the single statement the database eventually runs. We come back to that in the last section of this article, and the Ultimate ASP.NET Core Web API course works the same pattern through a larger API.
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?
How Do We Return Paging Metadata to the Client?
Paging metadata travels in the X-Pagination response header as a small JSON object. The body stays a plain array of records, so a client that ignores paging entirely still gets exactly what it asked for.
PagedList<T> is where the numbers come from. It inherits List<T>, adds CurrentPage, PageSize, TotalCount and TotalPages, and derives HasNext and HasPrevious from those.
Those extra properties never reach the body. A type that inherits List<T> serializes as a JSON array, and an array has nowhere to put the parent object’s properties, so the header is not decoration here, it is the only place the metadata survives.
One more call makes the header readable from a browser. X-Pagination is not a CORS-safelisted response header, so the CORS policy has to name it in WithExposedHeaders, or client-side JavaScript sees nothing at all.
Implementing the 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 async Task<PagedList<T>> ToPagedListAsync(IQueryable<T> source, int pageNumber, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
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.
ToPagedListAsync takes an IQueryable<T> and awaits CountAsync() and ToListAsync(), so neither round trip blocks a request thread. Both of those are EF Core extension methods on IQueryable<T>, which is exactly why the whole series keeps paging on the query rather than on a materialized list.
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 Task<PagedList<Owner>> GetOwners(OwnerParameters ownerParameters)
{
var owners = FindAll();
var sortedOwners = owners.OrderBy(o => o.Name);
return PagedList<Owner>.ToPagedListAsync(sortedOwners,
ownerParameters.PageNumber,
ownerParameters.PageSize);
}
And then the controller:
[HttpGet]
public async Task<ActionResult<IEnumerable<Owner>>> GetOwners([FromQuery] OwnerParameters ownerParameters)
{
var owners = await _repository.Owner.GetOwners(ownerParameters);
var metadata = new
{
owners.TotalCount,
owners.PageSize,
owners.CurrentPage,
owners.TotalPages,
owners.HasNext,
owners.HasPrevious
};
Response.Headers["X-Pagination"] = JsonSerializer.Serialize(metadata);
_logger.LogInfo($"Returned {owners.TotalCount} owners from database.");
return Ok(owners);
}
Two details in that one line are worth naming. We assign through the indexer rather than calling Response.Headers.Add(...), because Add throws an ArgumentException when the key is already present, and the ASP.NET Core analyzer flags it as ASP0019 at build time; the indexer replaces instead. And we serialize with System.Text.Json, the platform default, which writes the property names in PascalCase unless we change that globally. Our guide on configuring System.Text.Json globally covers the options that would alter it, and adding custom headers to an ASP.NET Core response covers the other ways a header reaches the client.
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.
| Field | Type | What it is | What a client does with it |
|---|---|---|---|
TotalCount | integer | Records matching the query, ignoring paging | Shows “1–10 of 4,317” |
PageSize | integer | Records actually returned on this page | Confirms the server honoured, or clamped, the request |
CurrentPage | integer | The 1-based page number returned | Highlights the active page in a pager |
TotalPages | integer | TotalCount divided by PageSize, rounded up | Sizes the pager and finds the last page |
HasNext | boolean | CurrentPage < TotalPages | Enables or disables the “next” control |
HasPrevious | boolean | CurrentPage > 1 | Enables or disables the “previous” control |
One thing has to be switched on before a browser can read any of that. X-Pagination is not one of the seven CORS-safelisted response headers, so cross-origin JavaScript cannot see it unless the server names it explicitly. AllowAnyHeader() does not cover this: it governs request headers. The response side needs its own call:
public static void ConfigureCors(this IServiceCollection services) =>
services.AddCors(options =>
options.AddPolicy("CorsPolicy",
builder => builder.WithOrigins("http://localhost:5000", "https://localhost:5001")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.WithExposedHeaders("X-Pagination")));
Without that last line the header still travels, and fetch() in the browser still cannot read it.
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;
private int _pageNumber = 1;
private int _pageSize = 10;
public int PageNumber
{
get => _pageNumber;
set => _pageNumber = Math.Max(value, 1);
}
public int PageSize
{
get => _pageSize;
set => _pageSize = Math.Clamp(value, 1, MaxPageSize);
}
}
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 async Task<ActionResult<IEnumerable<Account>>> GetAccountsForOwner(Guid ownerId, [FromQuery] AccountParameters parameters)
{
var accounts = await _repository.Account.GetAccountsByOwner(ownerId, parameters);
var metadata = new
{
accounts.TotalCount,
accounts.PageSize,
accounts.CurrentPage,
accounts.TotalPages,
accounts.HasNext,
accounts.HasPrevious
};
Response.Headers["X-Pagination"] = JsonSerializer.Serialize(metadata);
_logger.LogInfo($"Returned {accounts.TotalCount} accounts from database.");
return Ok(accounts);
}
Due to the inheritance of the paging parameters through the QueryStringParameters class, we get the same behavior.
Does Skip and Take Page in the Database or in Memory?
In the database. FindAll() returns an IQueryable<T>, so Skip and Take are added to an expression tree and translated into the SQL rather than applied to a list that has already been loaded.
A paged endpoint therefore runs two statements: a COUNT for the total, and a select with an offset and a limit for the page itself. Neither one reads the whole table.
The cost that does grow is the offset. A database asked to skip a million rows still walks past them before it returns anything, so page one is fast and page fifty thousand is not, however small the page size.
Keyset paging avoids that walk. Instead of an offset the client sends the last value it saw, and the query becomes a range the index can seek straight to. It cannot jump to an arbitrary page number, which is the trade.
The type is what decides this, which is why the difference between IEnumerable and IQueryable is worth knowing before writing a repository. Ask EF Core what it intends to run and it says so plainly:
DECLARE @p int = 10; SELECT [o].[Id], [o].[Age], [o].[City], [o].[Name] FROM [Owners] AS [o] ORDER BY [o].[Name] OFFSET @p ROWS FETCH NEXT @p ROWS ONLY
That is the whole page, expressed as one statement, with the ordering the repository supplied. Nothing is materialized in our process first.
The offset is the part that bites at depth. On a 200,000 row table with an index on the ordered column, returning ten rows from the front took 3 logical reads; returning ten rows after an offset of 190,000 took 828 logical reads for the same ten rows, because the plan scans the index forward and discards everything it passes. Page size never changes, and the work does.
Keyset paging is the escape hatch when that becomes real: the client sends back the last name or id it saw, the query filters on it, and the index seeks rather than scans. The price is that there is no “jump to page 4,000” any more, only “next” and “previous”, which is a fair trade for a feed and a bad one for a table with a pager.
The other honest answer is not to build it. Letting a library handle paging, sorting and filtering together is a reasonable choice when the endpoint is a plain list over a plain entity, and hand-rolling only pays once the rules stop being plain.
Both of these, the offset cost and the keyset alternative, are covered end to end in the Ultimate ASP.NET Core Web API course, which builds this API out past the point where paging stops being the interesting problem.
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 the
PagedListclass and separated our parameters for different controllers - Answered where
SkipandTakeactually run, and what the offset costs at depth
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, and after that sorting the same endpoint.
Tested with .NET 10.0.10 and EF Core 10.0.11.


Wow, just wow… Thanks for this
It didn’t say where to create the PagedList class. This caused a lot of problems.
Hi Sam. I am not sure why this caused a lot of problems because you have a source code linked at the beginning of the article. All our articles have the same. So you can inspect it and pretty easily find the location of the class: https://github.com/CodeMazeBlog/advanced-rest-concepts-aspnetcore/tree/paging-end/Entities/Helpers
How to convert this code into Async?
You can read more about this here: https://code-maze.com/asynchronous-programming-with-async-and-await-in-asp-net-core/ Also, in our book, we have covered it as well converting the entire project from sync to async.
Hi there,
I cloned the final repository (end) and ran the solution in VS 2022 and it did build and run without any compile errors (with output notifying that server listening on port 5000 as is expected as per the AccountOwnerServer launchsettings.json. On browsing to that URL it couldn’t find the page.
Anyways, I realised that I need to create the database needs to be created and in this solution it’s not abvious as there are no projects with a database context and we are using mysql. So how to create the migration and update database.
Let me know how to create the database needed.as I am facing this kind of a solution for the first time without a clue as to how to run it successfully.
Your blog doesn’t document the process fully as I experineced.
Regards
Kaushik
Hi there,
Your project end repository link is :
https://github.com/CodeMazeBlog/advanced-rest-concepts-aspnetcore/tree/paging-end
However on cloning the project in my local folder through git bash of the copied URL from the above link which is:
https://github.com/CodeMazeBlog/advanced-rest-concepts-aspnetcore.git
I could only get the start branch cloned (without code). Is there something wrong or am I missing something ?
Cheers,
Kaushik
Hello Kaushik Roy. That’s because you get the main branch, which is empty. But the repo you cloned contains both start and end branches for each article in the series, so you have to switch branch locally and yiu will see the source code for the specific article.
when i implement the accounts controller i get an error after sending request from postman
System.InvalidOperationException: ‘No service for type ‘Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory’ has been registered.’
and if i use
builder.Services.AddMvc() in the program.cs class i get
The View “GetAccountsForOwner was not foun”
I am not sure what is going on there. Please download our source code and compare it with your solution.
Hi Marinko,
In the book (p.190) you give additional advice on adding a CountAsync() method to improve the pagination when dealing with large databases.
Shouldn’t that line of code include the Skip and Take extension methods as in the call to get the paginated employees.
The reason I ask, is it alters the returned TotalCount in the MetaData info in the Header.
In the client app as in the BlazorWasm course this plays havoc with the links component.
Hello David. Well, if you see that entire example, you will see that Skip and Take are already implemented on the FindByCondition method. Then you just call the CountAsync to get all the items. Finally, you return a new PagedList with all the required data.
Marinko,
Ignore above comment, my issue was with filtering between dates.
Thanks for you precious website.A great source of inspiration!
I followed this tutorial about pagination.
My Web Api is ok but I can’t achive to recover the “X-Pagination” data with Angular (an http get method).
I can see the “X-Pagination” data in the web browser response panel but that’s all.
Perhaps you can help me?
Here is my Angular code :
public getAll(pageNumber: number, pageSize: number){ const api: string = this.apiAddress + '/' +truckParameters?PageNumber=${pageNumber}&PageSize=${pageSize}; const httpOptions = { headers: new HttpHeaders({'Content-Type': 'application/json'}), observe: 'response' as 'response'}; return this.httpClient.get(api, httpOptions).subscribe( (response) => { console.info('Header :' + response.headers.keys); } ); }Thank you
I finaly found the response on this brilliant website : https://code-maze.com/aspnetcore-add-custom-headers/ 🙂
I had just to expose the custom header in the Startup file.
Now I try in Angular to accede to X-Pagination each element.
Hi,
The question is, how to change the Delete action of the OwnerController
if (_repository.Account.AccountsByOwner(id).Any())
{
_logger.LogError($”Cannot delete owner with id: {id}. It has related accounts. Delete those accounts first”);
return BadRequest(“Cannot delete owner. It has related accounts. Delete those accounts first”);
}
what is the second parameter of the AccountsByOwner?
Hello, great tutorial.
I’m having trouble getting this to work with the CQRS & Mediator pattern.
Do you have any example on how to use this with that architecture?
This will be extremely helpful if so!
Hi William. I’m sorry but we don’t have such an article. But now when you ask bout it, it seems like a good topic to cover.
Hi again, I got ot the front end now. I have implemented all paging, filtering and sorting from this series. Do you know of an example in React which exposes this functionality? Thank you!
Hey Konstantina, great, hope it’s working as expected.
We don’t have anything like that in React, our React series is more basic than that, and I’d say even outdated. You can check it if you want: https://code-maze.com/react-series/
My suggestion, in this case, would be to search for a React specialized website or book/course.
Thank you very much, Vladimir!
Thanks for the wonderful job, but I stumbled on this tutorial. Why do you use FindAll() and then apply the pagination parameters on the full records set? Isn’t this would be more efficient to ask DBMS and fetching the exact number of required records?
The answer on your second question is yes and no. You have to return a count of all the rows to the frontend and for that you can call count and then ask db to return exact numbers of entities. But for the small amount of data these two queries would take more time than a single find all and then applying request parameters. But if you have a lot of data in db, than you shouldn’t be calling findall. We’ve explained this and tested it with a large amount of data in our Ultimate ASP.NET Core Web API book. You can find more info about it by clicking the book menu item in the nav bar
Marinko, thanks a lot for your prompt and comprehensive answer. In the real world scenarios there no such thing as small amount of data in the db (otherwise, what is the reason of using DBMS?).
That depends on what is your perspective on “small amount of data”. I am not talking about 3 or 4 rows in the table, but a lot more. This is why testing is crucial for this functionality. But again, this is the core explained in the article, everyone can improve it and adjust it to thier needs.
Hi! great job! you are really help my on my proyect.
I have a doubt. I have a complete Async CRUD following this post, however, when I added the pagination feature, I cant keep the async code on GetOwners method due to the PagedList class. Any idea about how to fix it?
Thank you very much!
Hello Jose. I’m glad our articles help you. Regarding your question, you can extractk the skip, take and count logic inside the GetOwners method from a Paged list class and just pass all the parameters to it. That way you can write something like this: var owners = await FindAll().Skip(x).Take(y).ToListAsync(); Also you can execute countasync as a separate query. Even though there are two requests, this is faster on a large amount of data – we have tested it for our Ultimate ASP.NET Core Web API book. Then you can just pass all the parameters to the sync PagedList class.
Ok, I think I got it, Thanks!
This is needed as I have a requirement for all instances to be displayed on one page if the user wishes.
Hi,
Thank you for the easy-to-understand tutorial. I need MaxPageSize to be dynamic – the count of the entities in the respective db set in the db context. Any ideas how I could do that?
Well you have to add some custom logic where you will fetch the required number from the database and then assign it to the MaxPageSize property. You can try modifying the Parameters class as well.
Thank you!
hi, have you met with an opinion that pagination metadata should also return link to the next and the previos page ?
Hi. Well from our expirience, these information are quite enough. You can see here how the frontend can utilize it: https://code-maze.com/blazor-webassembly-pagination/
well, that’s true about the information is enough, however it is the matter of server to create link to the resource, you just send link to UI
I am looking at that this way. If you have pagination span of 10 pages. So, you have previous, next and 1 to 10 buttons. And you are currently on a fifth page. With a links to previous and next pages, you have covered prev, next and 4th and 6th buttons, but what about 7th, 8th? You don’t have links for those, it is a front-end’s task to create it. Also, every time you change something on the page, like Sort option or Search, or number of items per page a new request is sent with different query parameters created by front end. That’s the main reason why I let FE deals with it. API resources could always be exposed through HATEOAS.
of cource, next, prev – it was shortcut for pagination as you mention, all page numbers need to be returned with links ….and for sorting, filtering, searching you have got the new pagination structure generated by server
Can you tell bit more on the above statement? 😉 the solution looks perfect so I wonder what you can see can be improved.
( except going async ).
thank you
Hi Lukasz. Well, we updated this article couple of times, so, we should probably remove that sentence 🙂 And yes, going async would be even better, but at this point I am pretty sure that converting this soution to the async one should be a peace of cake.
Hi Marinko,
Thanks for your prompt reply. When working on this found a tiny spot to improve 😉
There is an error when pageSize or pageNumber is < 0. ( at least when using postgres db saying that offset / limit can not be negative. )
Hi, this is great. Could you please explain asynchronous implementation for paging. Thank you very much !
Hi. Well it is almost the same, just you need propertly to apply async await keywords. Please read this article: https://code-maze.com/async-generic-repository-pattern/ Everything is explained here and after it, you will have no problem in implementing async paging functionality for sure.
Nice informative article!
Paging is not a “nice to have”. It is mandatory.
Not implementing server side paging correctly is nothing less than a security breach (Unbound query).
You should always implement it from the beginning, unless the data by definition has a maximum low row count (e.g. days of a month, etc..)
We completly agree with you on that. Thank you for reading and commenting as well.
So I’m not new to programming, but I am new to C# and ASP.NET and I’ve got to say: THANK YOU! These articles on the various aspects of building/designing/implementing a .NET CORE Web API are just awesome. Thank you so much!
Thank you so much fro the kind words Guy!
Hi, for me it’s not clear why you set the pagination properties in the Header why you use this approach instead of just return in Json as data? I is better use this approach? thanks for your help
Hi Joao,
This approach is preferred because each endpoint should return only the relevant data for the resource that’s requested. By putting the metadata inside the value of the response we would break the self-descriptiveness of the API (check the HATEOAS part to learn more). Metadata is not actually part of either Owner nor Account resources, so returning it as a result would be wrong. Having said all this, if your solution can benefit from having metadata in the response, you should implement it that way. Many big APIs do it like that. But you should now that it’s not RESTful, and you should be aware of the consequences if doing that.
Hope this helps.
Hi Mr. Vladimir, I’ve got confuse about combine AutoMapper and Paging, in previous net-core-web-development-part5 GetOwners() method mapping from Owner->OwnerDto. Should I using paging for listOwner or listOwnerDto? Thanks.
You use paging functionality for the List but when you create such a list, you can map it to the List because you should always return a DTO object to the client. The model classes are here just for the database purpose.
https://media3.giphy.com/media/GCvktC0KFy9l6/giphy.gif
Thanks Marinko, I have a question:
public IActionResult GetAllServers([FromQuery] ServerParameters serverParams)
{
try
{
var servers = _repository.Server.GetAllServer(serverParams);
var metadata = new
{
servers.TotalRecords,
servers.PageSize,
servers.CurrentPage,
servers.TotalPages,
servers.HasNext,
servers.HasPrevious
};
Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(metadata));
_logger.LogInfo($"return {servers.PageSize} server from database.");
var lstServerResult = _mapper.Map<IEnumerable>(servers);
foreach (var serverDTO in lstServerResult)
{
var lastestComment = _repository.Comment.GetLastestCommentByServerID(serverDTO.ServerID);
if (lastestComment != null)
{
serverDTO.LastComment = lastestComment.Description;
}
}
return Ok(lstServerResult);
}
catch (Exception e)
{
_logger.LogError($"Error when get all servers: {e.Message}");
return StatusCode(500, "Internal server error");
}
}
—————
as excerpt from my code. Was I right when mapping Server with ServerDTO using this line:
var lstServerResult = _mapper.Map<IEnumerable>(servers);
It seems right. You have prepared everything, added the pagination header, and then mapped the result.
yep! thanks sir :D.
at first, i wonder about List doesn’t have some metadata about(next page, previous page,…) in header.
but I realize those metadata can be share between obj and objDTO.
I like the service paging concept. Good way to prevent those folks that don’t do proper filtering on the front end web sites from returning the universe.
What matters in the end is application security and only application security. This means that “Proper” filtering on the front end is MEANINGLESS. We must always implement server side paging (and of course validate any inputs only the server side, only there it matters)
Shouldn’t be the first parameter in ToPagedList method IQueryable instead of IEnumerable ? I checked it in my postgres database with pg_stat_statements and when it was IEnumerable I’ve got all of the records and only then it was sliced into smaller parts to retrieve it in the view. When I changed to IQueryable then it was fine, the query returned only the elements I requested and an additional COUNT(*) request was send. 🙂
Yes, you are exactly right. The query is executed in the ToPagedList method and it should have been IQueriable at that point of execution. Good catch, thanks! We’ll update the article and the source code ASAP.
Article “Searching in ASP.NET Core Web API” also needs to be updated, when list is passed as a parameter to ToPagedList method inside repository. Nice articles!
Hi Code Maze,
I am beginner, can you please explain about RepositoryBase.cs, where this can be write if following repository pattern with modelview, and how can use it if I have other lists like List of Student, List of Owner, List of Teacher.
Hey Vipin Jha,
As we mentioned at the beginning of the article, some of the concepts are explained in our basic ASP.NET Core Web API series:
http://34.65.74.140/net-core-series/
Make sure to go through that series first to be able to follow this one, since you’ve just begun your journey.
For the concrete answer to your question on what RepositoryBase is, I would go directly to:
http://34.65.74.140/net-core-web-development-part4/
And if you manage to get it down and understand the concept, you can improve it even more by making it asynchronous:
http://34.65.74.140/async-generic-repository-pattern/
Hope this helps and if you have any more questions, feel free to leave us a comment and we’ll try to help you as soon as we can.
Thanks Vladimir Pecanac for help..
In your blog the scenario defined as Generic repository, wrapper classes. Where as in my case it is not generic classes for repository. Here is example given below:
public interface ISchoolsRepository{
Task<ICollection> GetAllSchoolAsync();
}
and Repository Class:
public class SchoolsRepository : ISchoolsRepository{
public readonly learning_gpsContext _GpsContext;
public SchoolsRepository(learning_gpsContext GpsContext)
{
_GpsContext = GpsContext;
}
public async Task<ICollection> GetAllSchoolAsync()
{
List results = null;
var result = PagdeList.Create(_GpsContext.School, 1, 10);
results = await _GpsContext.School.AsNoTracking().ToListAsync();
return results;
}
}
and my controller is look like this:
[HttpGet]public async Task GetSchoolAll()
{
var schools = await _schoolsRepository.GetAllSchoolAsync();
List schoolsVms = new List();
foreach (Schools school in schools)
{
schoolsVms.Add(new SchoolsVm
{
Id = school.ID.ToString(),
Name = school.Name,
creatDate = school.CreatedAt.ToString()
});
}
return Ok(schoolsVms);
}
Any suggestion to do pagination in the given situation. Although your tutorial is very clear and well articulated but if I will follow your blog, I will have to do lots of changes in my projects.
hi, all your calls are synchronous. Would it be better to implement asynchronous in repository or just simply do
var owner = await Task.Run(() => { return _repository.Owner.GetOwners(ownerParameters);});. Thank you!Hey vidriduch, yes they are. We intentionally siplified the series, but if you can find our take on asynchronous implementation here: http://34.65.74.140/async-generic-repository-pattern/
We also have a few different “improvement” articles related to Web API, so you can try the search function to find out if we’ve already covered it.
Thanks for the feedback!