hotchocolate-best-practices

Installation
SKILL.md

HotChocolate Best Practices

You are implementing or reviewing a HotChocolate GraphQL server in .NET. All guidance targets HotChocolate 16.6 or newer — do not apply it to v15-or-older codebases without checking the migration guides. This skill defines how ChilliCream expects that code to look. It covers implementation — resolvers, data access, DataLoaders, pagination. Schema design (SDL shape, naming, nullability) belongs to the sibling skill graphql-schema-design; use both when a task spans design and implementation.

Core principles

  1. Implementation-first. Write plain C# — static resolver classes with attributes like [QueryType], [MutationType], [ObjectType<T>], [DataLoader] — and let HotChocolate infer the schema. Do not use schema-first SDL binding, and reach for descriptor/Configure overrides only for what attributes cannot express.

  2. All data access flows through DataLoaders. A resolver for a relation (any non-root field that fetches data) never queries a DbContext or repository directly — it calls a DataLoader. Direct queries create N+1 loads and bypass request-scoped caching.

  3. Top-level byId fields use DataLoaders too. Root lookups are batchable: clients batch them with field aliases in a single operation and with request/variable batching across operations. A root resolver that hits the database directly forfeits that coalescing.

  4. DataLoaders are source-generated. Write a static method annotated with [DataLoader]; never hand-write a class deriving from BatchDataLoader/GroupedDataLoader. The generator emits the class, interface, and registration.

  5. Projections flow through QueryContext<T>. Every DataLoader accepts a QueryContext<TValue> parameter so consumers can pass selections down to the database, and always pins its key into the projection (query.Include(x => x.Id)) so the key survives narrowing. The flip side: every resolver that reads parent properties declares them with [Parent(requires: nameof(...))] so projections keep those fields available.

  6. Pagination is cursor-based and ordered. Paginated fields use PagingArguments + ToPageAsync/ToBatchPageAsync; every paginated query has a deterministic order ending in the key, and paging on non-root types goes through a batch-paging DataLoader. Never offset pagination — relative cursors cover jump-to-page needs.

Project setup

Installs
42
GitHub Stars
16
First Seen
Aug 18, 2026