Table of contents
In our post GraphQL in .NET with Hot Chocolate, we gave an introduction about how to use the library with a simple example. Today we want to extend that exercise with a set of common features such as pagination, filtering, and sorting, showing how easy it is to implement them with Hot Chocolate.
Prerequisites
We will use as a starting code the solution located here, clone or download the code. Let's start by adding a new NuGet package to the api
project:
dotnet add api package HotChocolate.Data --version 12.14.0
All HotChocolate.* packages need to have the same version.
Open the Program.cs
file and replace the content with:
using api;
var builder = WebApplication.CreateBuilder(args);
var posts = Enumerable.Range(0, 20).Select(index => new Post() { Body = $"Body {index}", Id = Guid.NewGuid(), Title = $"Title {index}" });
builder.Services.AddSingleton(new Storage() { Posts = new List<Post>(posts) });
builder.Services
.AddGraphQLServer()
.AddQueryType<PostQueriesType>()
.AddMutationType<PostMutationsType>() ;
var app = builder.Build();
app.MapGraphQL();
app.Run();
Pagination
Offset-based
Offset-based pagination is a technique wherein the client requests two parameters to paginate through items in a collection:
limit
: the number of records.offset
: number of records that need to be skipped.
Go to the PostQueriesType.cs
file to add UseOffsetPaging()
to the descriptor as follow:
public class PostQueriesType : ObjectType<PostQueries>
{
protected override void Configure(IObjectTypeDescriptor<PostQueries> descriptor)
{
descriptor
.Field(f => f.GetPost(default!, default!))
.Type<PostType>();
descriptor
.Field(f => f.GetPosts(default!))
.Type<ListType<PostType>>()
.UseOffsetPaging();
}
}
And that's it. Hot Chocolate will add a middleware in charge of applying the pagination arguments to what we have returned. Run the application with the following command:
dotnet run --project api\api.csproj --urls="http://localhost:7121"
Open the URL http://localhost:7121/graphql/
in your browser and run the following query in Banana Cake Pops:
query {
posts(skip: 2, take: 5) {
items {
title
body
id
}
pageInfo {
hasNextPage
hasPreviousPage
}
}
}
PagingOptions
is available as an argument of the UseOffsetPaging
method:
descriptor
.Field(f => f.GetPosts(default!))
.Type<ListType<PostType>>()
.UseOffsetPaging(options: new PagingOptions()
{
MaxPageSize = 10,
DefaultPageSize = 5,
IncludeTotalCount =true,
});
Cursor-based
A cursor is a unique identifier for a specific record, which acts as a pointer to the next record we want to start querying from to get the next page of results. We will update the PostQueriesType.cs
file to use UsePaging()
as follow:
public class PostQueriesType : ObjectType<PostQueries>
{
protected override void Configure(IObjectTypeDescriptor<PostQueries> descriptor)
{
descriptor
.Field(f => f.GetPost(default!, default!))
.Type<PostType>();
descriptor
.Field(f => f.GetPosts(default!))
.Type<ListType<PostType>>()
.UsePaging();
}
}
Run the following query:
query {
posts(first: 2) {
edges {
cursor
node {
title
body
id
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
To get something like:
{
"data": {
"posts": {
"edges": [
{
"cursor": "MA==",
"node": {
"title": "Title 0",
"body": "Body 0",
"id": "f8641118a70c455c954f067a58b9c02e"
}
},
{
"cursor": "MQ==",
"node": {
"title": "Title 1",
"body": "Body 1",
"id": "1c631d9cb12b4d68960ff075ae3ab095"
}
}
],
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "MA==",
"endCursor": "MQ=="
}
}
}
}
Let's see the structure of the result:
Connections
: Instead of returning a list of entries, we return a Connection.Edges
: We return an array of Edges in a Connection. An Edge has two properties, acursor
(a unique identifier for the entry) and anode
(the entry itself).PageInfo
: Information to aid in pagination.
The list of arguments that we can use are:
first
: Returns the first N elements from the list.after
: Returns the elements in the list that come after the specified cursor.last
: Returns the last N elements from the list.before
: Returns the elements in the list that come before the specified cursor.
PagingOptions
is available as an argument of the UsePaging
method:
descriptor
.Field(f => f.GetPosts(default!))
.Type<ListType<PostType>>()
.UsePaging(options: new PagingOptions()
{
MaxPageSize = 10,
DefaultPageSize = 5,
IncludeTotalCount =true,
});
Filtering
To use the Hot Chocolate default filtering implementation, we need to modify the Program.cs
file with:
builder.Services
.AddGraphQLServer()
.AddQueryType<PostQueriesType>()
.AddMutationType<PostMutationsType>()
.AddFiltering();
Then go to the PostQueriesType.cs
file and update as follow:
public class PostQueriesType : ObjectType<PostQueries>
{
protected override void Configure(IObjectTypeDescriptor<PostQueries> descriptor)
{
descriptor
.Field(f => f.GetPost(default!, default!))
.Type<PostType>();
descriptor
.Field(f => f.GetPosts(default!))
.Type<ListType<PostType>>()
.UsePaging(options: new PagingOptions()
{
MaxPageSize = 10,
DefaultPageSize = 5,
IncludeTotalCount = true,
})
.UseFiltering();
}
}
Run the application and go to Schema Definition
to see the PostFilterInput
type:
input PostFilterInput {
and: [PostFilterInput!]
or: [PostFilterInput!]
id: ComparableGuidOperationFilterInput
title: StringOperationFilterInput
body: StringOperationFilterInput
}
input StringOperationFilterInput {
and: [StringOperationFilterInput!]
or: [StringOperationFilterInput!]
eq: String
neq: String
contains: String
ncontains: String
in: [String]
nin: [String]
startsWith: String
nstartsWith: String
endsWith: String
nendsWith: String
}
input ComparableGuidOperationFilterInput {
eq: UUID
neq: UUID
in: [UUID!]
nin: [UUID!]
gt: UUID
ngt: UUID
gte: UUID
ngte: UUID
lt: UUID
nlt: UUID
lte: UUID
nlte: UUID
}
Let's run a query based on that definition:
query {
posts(where: { title:{ contains: "5" } }) {
edges {
cursor
node {
title
body
id
}
}
}
}
If we don't want to expose all this filtering surface to our clients, we can customize what we offer and how. Let's rename the title
argument to topic
limit the search to only use contains
, and disallow the use of the and
operator. Create a PostFilterType.cs
file with the following content:
public class PostFilterType : FilterInputType<Post>
{
protected override void Configure(
IFilterInputTypeDescriptor<Post> descriptor)
{
descriptor.BindFieldsExplicitly();
descriptor.Field(f => f.Title).Name("topic").Type<TitleOperationFilterInput>();
}
}
public class TitleOperationFilterInput : StringOperationFilterInputType
{
protected override void Configure(IFilterInputTypeDescriptor descriptor)
{
descriptor.Operation(DefaultFilterOperations.Contains).Type<StringType>();
descriptor.AllowOr(false);
}
}
Update the PostQueriesType.cs
file with:
public class PostQueriesType : ObjectType<PostQueries>
{
protected override void Configure(IObjectTypeDescriptor<PostQueries> descriptor)
{
descriptor
.Field(f => f.GetPost(default!, default!))
.Type<PostType>();
descriptor
.Field(f => f.GetPosts(default!))
.Type<ListType<PostType>>()
.UsePaging(options: new PagingOptions()
{
MaxPageSize = 10,
DefaultPageSize = 5,
IncludeTotalCount = true,
})
.UseFiltering<PostFilterType>();
}
}
Run the application, and let's see, again, the Schema Definition
:
input PostFilterInput {
and: [PostFilterInput!]
or: [PostFilterInput!]
topic: TitleOperationFilterInput
}
input TitleOperationFilterInput {
and: [TitleOperationFilterInput!]
contains: String
}
Sorting
To use the Hot Chocolate default sorting implementation, we need to modify the Program.cs
file with:
builder.Services
.AddGraphQLServer()
.AddQueryType<PostQueriesType>()
.AddMutationType<PostMutationsType>()
.AddFiltering()
.AddSorting();
Then go to the PostQueriesType.cs
file and update as follow:
public class PostQueriesType : ObjectType<PostQueries>
{
protected override void Configure(IObjectTypeDescriptor<PostQueries> descriptor)
{
descriptor
.Field(f => f.GetPost(default!, default!))
.Type<PostType>();
descriptor
.Field(f => f.GetPosts(default!))
.Type<ListType<PostType>>()
.UsePaging(options: new PagingOptions()
{
MaxPageSize = 10,
DefaultPageSize = 5,
IncludeTotalCount = true,
})
.UseFiltering<PostFilterType>()
.UseSorting();
}
}
Note: If you use more than one middleware, keep in mind that ORDER MATTERS. The correct order is UsePaging > UseProjections > UseFiltering > UseSorting
Run the application and go to Schema Definition
to see the PostSortInput
type:
input PostSortInput {
id: SortEnumType
title: SortEnumType
body: SortEnumType
}
enum SortEnumType {
ASC
DESC
}
As we did with the filtering, we can customize the sorting. Let's limit the sorting to the title
property. Create a PostSortType.cs
file with the following content:
public class PostSortType : SortInputType<Post>
{
protected override void Configure(ISortInputTypeDescriptor<Post> descriptor)
{
descriptor.BindFieldsExplicitly();
descriptor.Field(f => f.Title).Name("topic");
}
}
Modify the PostQueriesType.cs
file with:
public class PostQueriesType : ObjectType<PostQueries>
{
protected override void Configure(IObjectTypeDescriptor<PostQueries> descriptor)
{
descriptor
.Field(f => f.GetPost(default!, default!))
.Type<PostType>();
descriptor
.Field(f => f.GetPosts(default!))
.Type<ListType<PostType>>()
.UsePaging(options: new PagingOptions()
{
MaxPageSize = 10,
DefaultPageSize = 5,
IncludeTotalCount = true,
})
.UseFiltering<PostFilterType>()
.UseSorting<PostSortType>();
}
}
Start up the application and run the query:
posts( order: { topic: DESC } ) {
edges {
cursor
node {
title
body
id
}
}
}
}
All these topics, and more, can be found in the official documentation here. The code of this post is available here. Thanks, and happy coding.