Pagination for ASP.NET Core.
Supports both offset and keyset pagination for Entity Framework Core.
This package provides an easy to use service that auto reads pagination related params from the query string of the request to paginate EF Core data.
Keyset pagination support uses MR.EntityFrameworkCore.KeysetPagination. Make sure to read its README for an overview.
Keyset pagination (also known as cursor/seek pagination) is much more efficient and has stable performance over large amounts of data, but it's harder to work with than offset pagination.
Offset | Keyset | |
---|---|---|
Performance | worse over large data* | stable over large data* |
Duplicate/Skipped data | always possible if data gets updated between page navigations | no duplication/skipping** |
Pages | can access random pages | can only go to first/previous/next/last |
Check here for benchmarks between the two methods.
We recommend keyset pagination over offset, unless you have a requirement for wanting to randomly access pages.
Add pagination services:
services.AddPagination();
You can configure some default options here:
services.AddPagination(options =>
{
options.PageQueryParameterName = "p";
// ...
});
Check the PaginationOptions
class to see what you can configure.
And then just inject IPaginationService
in your controller/page and use it. The returned result is either a KeysetPaginationResult<>
or an OffsetPaginationResult<>
, each containing all the info you need for this pagination result.
Do a keyset pagination:
var usersPaginationResult = await _paginationService.KeysetPaginateAsync(
_dbContext.Users,
b => b.Descending(x => x.Created).Descending(x => x.Id),
async id => await _dbContext.Users.FindAsync(int.Parse(id)));
id
above will always be a string, so make sure to parse it to your entity's id type.
Important The keyset should be deterministic.
Check the readme for MR.EntityFrameworkCore.KeysetPagination for more important info about keyset pagination in general.
Prebuilt keyset query definitions from MR.EntityFrameworkCore.KeysetPagination are also supported:
// In the ctor or someplace similar, set this to a static field for example.
_usersKeysetQuery = KeysetQuery.Build<User>(b => b.Descending(x => x.Created).Descending(x => x.Id));
// Then when calling KeysetPaginateAsync, we use the prebuilt definition.
var usersPaginationResult = await _paginationService.KeysetPaginateAsync(
_dbContext.Users,
_usersKeysetQuery,
async id => await _dbContext.Users.FindAsync(int.Parse(id)));
Prebuilt keyset query definitions are the recommended way, but the examples here just build the keyset in KeysetPaginateAsync
for brevity.
Do a keyset pagination and map to dto:
// Using AutoMapper for example:
var usersPaginationResult = await _paginationService.KeysetPaginateAsync(
_dbContext.Users,
b => b.Descending(x => x.Created),
async id => await _dbContext.Users.FindAsync(int.Parse(id)),
query => query.ProjectTo<UserDto>(_mapper.ConfigurationProvider));
// Using manual select:
var usersPaginationResult = await _paginationService.KeysetPaginateAsync(
_dbContext.Users,
b => b.Descending(x => x.Created),
async id => await _dbContext.Users.FindAsync(int.Parse(id)),
query => query.Select(user => new UserDto(...)));
Do an offset pagination:
var usersPaginationResult = await _paginationService.OffsetPaginateAsync(
_dbContext.Users.OrderByDescending(x => x.Created));
Do an offset pagination and map to dto:
// Using AutoMapper for example:
var usersPaginationResult = await _paginationService.OffsetPaginateAsync(
_dbContext.Users.OrderByDescending(x => x.Created),
query => query.ProjectTo<UserDto>(_mapper.ConfigurationProvider));
// Using manual select:
var usersPaginationResult = await _paginationService.OffsetPaginateAsync(
_dbContext.Users.OrderByDescending(x => x.Created),
query => query.Select(x => new UserDto(...)));
There's additional support for doing an offset pagination over in memory list of data:
// Assume we have an in memory list of orders.
var orders = new List<Order>();
// This does efficient offset pagination over the orders list.
var result = _paginationService.OffsetPaginate(orders);
There's a helper PaginationActionDetector
class that can be used with reflection, for example in ASP.NET Core conventions, which can tell you whether the action method returns a pagination result or not. This is what the MR.AspNetCore.Pagination.Swashbuckle package uses to configure swagger for those apis.
The getReferenceAsync
delegate parameter on KeysetPaginateAsync
that you'll provide allows returning nulls. The behavior when that happens is to always return the first page (enforcing Forward direction).
If you want to have a special way to handle this, you'll simply have to do that logic in the getReferenceAsync
delegate you'll provide. Here's an example:
var result = await _paginationService.KeysetPaginateAsync(
query,
b => b.Descending(x => x.Created),
async id =
{
var reference = await _dbContext.Users.FindAsync(int.Parse(id));
if (reference == null)
{
// Throw a custom exception which will be captured by some middleware to process.
throw new MyKeysetPaginationReferenceIsNullException();
}
return reference;
};
All methods also have overloads that accept the pagination query model directly as an argument, as opposed to parsing it from the request query.
// Won't parse anything from the request's query.
var usersPaginationResult = await _paginationService.KeysetPaginateAsync(
_dbContext.Users,
b => b.Descending(x => x.Created),
async id => await _dbContext.Users.FindAsync(int.Parse(id)),
queryModel: new KeysetQueryModel
{
// Get the first page, with 10 items.
First = true,
Size = 10,
});
This is a support package for Swashbuckle.
Use it when you're using Swashbuckle (swagger). Actions that return keyset/offset pagination results will automatically have the query parameters (page, before, after, etc) show up in swagger ui.
When you're configuring swagger:
builder.Services.AddSwaggerGen(c =>
{
// ...
c.ConfigurePagination();
});
Check this blog post for a detailed look into benchmarks between offset and keyset pagination.
Check the samples folder for project samples.