Pagination
HotChocolate supports both offset and cursor based pagination. The cursor based pagination is based on the Relay spec.
Usage
To add pagination capabilities, we add the UsePaging
middleware:
public class Query
{
[UsePaging]
public IEnumerable<User> GetUsers([Service] IUserRespository repository)
=> repository.GetUsers();
}
For the UsePaging
middleware to work, our resolver needs to return an IEnumerable<T>
or an IQueryable<T>
. The middleware will then apply the pagination arguments to what we have returned. In the case of an IQueryable<T>
this means that the pagination operations can be directly translated to native database queries. HotChocolate also offer pagination integrations for some database technologies that do not use IQueryable
.
Naming
The name of the Connection and Edge type is automatically inferred from the field name. If our field is called users
, a UsersConnection
and UsersEdge
type is automatically generated. We can also specify a custom name for our Connection like the following:
public class Query
{
[UsePaging(ConnectionName = "CustomUsers")]
public IEnumerable<User> GetUsers([Service] IUserRespository repository)
{
// Omitted code for brevity
}
}
Options
We can define a number of options on a per-field basis trough properties on the attribute, e.g.:
[UsePaging(MaxPageSize = 100)]
public IEnumerable<User> GetUsers([Service] IUserRespository repository)
{
// Omitted code for brevity
}
Total count
Sometimes we might want to return the total number of pageable entries. For this to work we need to enable the IncludeTotalCount
flag on the UsePaging
middleware:
[UsePaging(IncludeTotalCount = true)]
public IEnumerable<User> GetUsers([Service] IUserRespository repository)
{
// Omitted code for brevity
}
This will add a new field called totalCount
to our Connection. If our resolver returns an IEnumerable<T>
or an IQueryable<T>
the totalCount
will be automatically computed, if it has been specified as a subfield in the query.
If we have customized our pagination and our resolver now returns a Connection<T>
, we have to explicitly declare how the totalCount
value is computed:
var connection = new Connection<User>(
edges,
pageInfo,
getTotalCount: cancellationToken => ValueTask.FromResult(0)
);
Custom pagination logic
If we need more control over the pagination process we can do so, by returning a Connection<T>
:
{
[UsePaging]
public Connection<User> GetUsers(string? after, int? first, string sortBy)
{
// Get users using the above arguments
IEnumerable<User> users = null;
var edges = users.Select(user => new Edge<User>(user, user.Id))
.ToList();
var pageInfo = new ConnectionPageInfo(false, false, null, null);
var connection = new Connection<User>(edges, pageInfo,
ct => ValueTask.FromResult(0));
return connection;
}
}
Changing the node type
Lets say we are returning a collection of string
from our pagination resolver, but we want these string
to be represented in the schema using the ID
scalar. We can change the node type like the following:
public class Query
{
[UsePaging(typeof(IdType))]
public IEnumerable<string> GetIds()
{
// Omitted code for brevity
}
}
The same applies of course, if we are returning a collection of User
from our pagination resolver, but we want to use the UserType
for representation in the schema.
Adding fields to an Edge
We can add new fields to an Edge type, by creating a type extension that targets the Edge type by its name. If our Edge is named UsersEdge
, we can add a new field to it like the following:
[ExtendObjectType("UsersEdge")]
public class UsersEdge
{
public string NewField([Parent] Edge<User> edge)
{
var cursor = edge.Cursor;
var user = edge.Node;
// Omitted code for brevity
}
}
Adding fields to a Connection
We can add new fields to a Connection type, by creating a type extension that targets the Connection type by its name. If our Connection is named UsersConnection
, we can add a new field to it like the following:
[ExtendObjectType("UsersConnection")]
public class UsersConnectionExtension
{
public string NewField()
{
// Omitted code for brevity
}
}
These additional fields are great to perform aggregations either on the entire dataset, by for example issuing a second database call, or on top of the paginated result. We can access the pagination result like the following:
[ExtendObjectType("UsersConnection")]
public class UsersConnectionExtension
{
public string NewField([Parent] Connection<User> connection)
{
var result = connection.Edges.Sum(e => e.Node.SomeField);
// Omitted code for brevity
}
}