Relay
The Relay GraphQL Server Specification defines patterns for globally unique identifiers, object refetching, and cursor-based pagination. While these patterns originated in Facebook's Relay client, they improve schema design for any GraphQL client.
The patterns on this page benefit all GraphQL clients, not only Relay. We recommend them for every Hot Chocolate project.
Global Identifiers#
GraphQL clients often use the id field to build a client-side cache. If two different types both have a row with id: 1, the cache encounters collisions. Global identifiers solve this by encoding the type name and the underlying ID into an opaque, Base64-encoded string that is unique across the entire schema.
Hot Chocolate handles this through a middleware. The [ID] attribute opts a field into global identifier behavior. At runtime, Hot Chocolate combines the type name with the raw ID to produce a globally unique value. Your business code continues to work with the original ID.
Output Fields#
Input Arguments#
When a field returns a serialized global ID, any argument that accepts that ID must also be marked with [ID] to deserialize it back to the raw value.
Input Object Fields#
Mark input object properties with [ID] to deserialize global IDs in input types.
public class UpdateProductInput
{
[ID]
public int ProductId { get; set; }
public string Name { get; set; }
}ID Serializer#
You can access the IIdSerializer service directly to serialize or deserialize global IDs in custom code.
[QueryType]
public static partial class ProductQueries
{
public static string GetGlobalId(int productId, IIdSerializer serializer)
{
return serializer.Serialize(null, "Product", productId);
}
}The Serialize method takes the schema name (or null for the default schema), the type name, and the raw ID.
Global Object Identification#
Global object identification extends global identifiers by enabling clients to refetch any object by its ID through a standardized node query field. This requires three things:
- The type implements the
Nodeinterface. - The type has an
id: ID!field. - A node resolver method can fetch the object by its ID.
Enabling Global Object Identification#
builder
.AddGraphQL()
.AddGlobalObjectIdentification();This adds the Node interface and the node / nodes query fields:
interface Node {
id: ID!
}
type Query {
node(id: ID!): Node
nodes(ids: [ID!]!): [Node]!
}You can configure options when enabling global object identification:
builder
.AddGraphQL()
.AddGlobalObjectIdentification(opts =>
{
opts.MaxAllowedNodeBatchSize = 50;
});At least one type in the schema must implement Node, or the schema fails to build.
Implementing Node#
Node resolvers are ideal places to use DataLoaders for efficient batched fetching.
Node with Type Extensions#
When adding Node support through a type extension, place the [Node] attribute on the extension class:
[Node]
[ExtendObjectType<Product>]
public static partial class ProductExtensions
{
public static async Task<Product?> GetAsync(
int id, CatalogContext db, CancellationToken ct)
=> await db.Products.FindAsync([id], ct);
}Complex IDs#
Some data models use composite keys (multiple fields forming a unique identifier). Hot Chocolate supports complex IDs through custom ID types and type converters.
public readonly record struct ProductId(string Sku, int BatchNumber)
{
public override string ToString() => $"{Sku}:{BatchNumber}";
public static ProductId Parse(string value)
{
var parts = value.Split(':');
return new ProductId(parts[0], int.Parse(parts[1]));
}
}
public class Product
{
[ID]
public ProductId Id { get; set; }
}Register type converters so Hot Chocolate can serialize and deserialize the complex ID:
builder
.AddGraphQL()
.AddTypeConverter<string, ProductId>(ProductId.Parse)
.AddTypeConverter<ProductId, string>(x => x.ToString())
.AddGlobalObjectIdentification();The source generator can produce a NodeIdValueSerializer for your custom ID type, reducing the need for manual converter registration.
Query Field in Mutation Payloads#
Mutation payloads can include a query field that gives clients access to the full Query type. This lets a client fetch everything it needs to update its state in a single round trip.
builder
.AddGraphQL()
.AddQueryFieldToMutationPayloads();By default, a query: Query field is added to every mutation payload type whose name ends in Payload. You can customize this:
builder
.AddGraphQL()
.AddQueryFieldToMutationPayloads(options =>
{
options.QueryFieldName = "rootQuery";
options.MutationPayloadPredicate =
(type) => type.Name.Value.EndsWith("Result");
});Next Steps#
- Need to fetch data efficiently? See DataLoader.
- Need pagination? See Pagination.
- Need to understand ID types? See Scalars.
- Need to extend types? See Extending Types.