Entity Pattern
The global object identification pattern describes what an entity is and how an entity can be fetched by its globally unique identifier. This pattern is a fundamental building block for any GraphQL server that wants to be compatible with the Relay specification. There are a couple of parts to this specification. The first thing is that any entity must implement the Node
interface. The Node
interface is a simple interface that has a single field called id
that returns a non-null ID
. This id
should be a globally unique identifier for the object, meaning it must have a unique identifier within your schema. Given this id
, the server should be able to refetch the object through the node
root field. The node
root field provides a generic way for clients to refetch parts of an entity by its globally unique identifier.
Open the lesson directory in your IDE.
code src/Chapter4/Lesson1/Begin
In Hot Chocolate we have made it very easy to implement the global object identification pattern.
builder.Services
.AddGraphQLServer()
...
.AddGlobalObjectIdentification()
...
By opting in Hot Chocolate will demand at least one implementation of the Node
interface and make sure that all entities have unique identifiers. To make sure that every ID
is unique within your schema, Hot Chocolate will encode the original ID with the type name.
In order to provide your own ID
encoding, you can implement the INodeIdSerializer
interface and register it with GraphQL configuration builder.
builder.Services
.AddGraphQLServer()
...
.AddGlobalObjectIdentification()
.AddNodeIdSerializer<MyNodeIdSerializer>()
...
There are two ways to specify an entity and this depends largely on your domain model. The first way is if you alread have a entityById
field on your query type. If you remeber, we already have for instance a field productById
to fetch the product by its primary key.
[QueryType]
public static class ProductQueries
{
public static async Task<Product?> GetProductByIdAsync(
int id,
ProductService productService,
CancellationToken cancellationToken)
=> await productService.GetProductByIdAsync(id, cancellationToken);
}
In this case we can simply annotate this field with the NodeResolver
attribute.
[QueryType]
public static class ProductQueries
{
[NodeResolver]
public static async Task<Product?> GetProductByIdAsync(
int id,
ProductService productService,
CancellationToken cancellationToken)
=> await productService.GetProductByIdAsync(id, cancellationToken);
}
Since if there is a node resolver for an object the returning object must be an entity. Hot Chocolate will implicitly implement the interface and ensure that the id
field is encoded.
The second way is to specify this on the entity itself with a type extension. This is useful if you have more complex identifiers like a composite key or if you do not want to have a public entityById
field on the query type for this entity but you still want it to be refetchable.
[ObjectType<BookChapter>]
public static partial class BookChapterNode
{
[NodeResolver]
public static BookChapter? GetChapterById(BookChapterId id, BookChapterRepository repository)
=> repository.GetBookChapterById(id.BookId, id.ChapterNumber);
}
Tasks
- Add the global object identification spec to the lesson project.
- Make
Book
aNode
by reusing the query field to fetch a book by its identifier. - Make
Author
aNode
by introducing a type extension. - Run the server and grab a book identifier and try to fetch the book title through the
node
field.