skip to content

In Hot Chocolate 16, how do you declare a books query and a Book.author resolver, implementation-first versus code-first?

level: juniorimportance: must knowfreq 50%

answer

  1. attributes versus descriptors
  2. partial classes, build-time generator
  3. [QueryType] on a static partial class
  4. [ObjectType<Book>] with a [Parent] parameter
  5. AddGraphQL, AddTypes, MapGraphQL

basics

~20 s

Implementation-first marks a static partial class with [QueryType] so its public methods become Query fields, and adds Book fields through an [ObjectType<Book>] class taking a [Parent] Book; code-first subclasses ObjectType<T> and declares fields in Configure.

solid answer

~40 s

Hot Chocolate 16 documents two C# styles. **Implementation-first**: I write `[QueryType] public static partial class BookQueries` with a method such as `GetBooks(BookstoreContext db)`. The `Get` prefix and `Async` suffix are stripped, so the field is `books`, and a parameter whose type is registered in DI is injected rather than exposed as an argument. To add `author` to `Book`, I write `[ObjectType<Book>] public static partial class BookNode` with `GetAuthorAsync([Parent] Book book, ...)`. The source generator emits the registration, so `Program.cs` is `builder.AddGraphQL().AddTypes()` plus `app.MapGraphQL()`, which serves `/graphql`. **Code-first** subclasses `ObjectType<Book>` and uses `descriptor.Field(...).Resolve(...)`, registered with `AddType<BookType>()` or `AddQueryType<T>()`. I default to implementation-first and reach for descriptors when the schema must differ from the C# model.

code

csharp · 11 lines
csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<BookstoreContext>();

builder
    .AddGraphQL()
    .AddTypes();

var app = builder.Build();
app.MapGraphQL();
app.Run();

go deeper

for a junior

Recall the two styles by name, the [QueryType] static partial class, the Get/Async naming rule, and the three Program.cs calls: AddGraphQL, AddTypes, MapGraphQL.

for a middle

Explain how the source generator turns attributes into registration code, why [Parent] and injected services are not arguments, and how code-first descriptors express the same field.

for a senior

Show judgment on schema shape: keep API resolvers off EF entities with [ObjectType<T>] classes, know the generated module name, and spot a resolver that silently introduces N+1.

for a principal

Weigh attribute-driven schemas against descriptor classes for a large codebase: ownership of schema files, how far the contract may drift from the model, and review cost.

## What declaring a type means in Hot Chocolate A GraphQL server needs two things for every field: a place in the **schema** (the type, its name, its arguments) and a **resolver** (the code that produces the value). Hot Chocolate, the GraphQL server library for ASP.NET Core, builds both from C#. In the version 16 documentation there are **two C# authoring styles**: - **Implementation-first** — ordinary C# classes plus attributes; a Roslyn **source generator** reads them at build time and writes the registration code. - **Code-first** — classes that inherit `ObjectType<T>` and describe each field through a **descriptor** API. Both produce the same GraphQL schema, and a real project often uses both. ## Implementation-first: attributes and the source generator For the bookstore, the root query lives on a class marked `[QueryType]`: ```csharp [QueryType] public static partial class BookQueries { public static IQueryable<Book> GetBooks(BookstoreContext db) => db.Books.OrderBy(b => b.Title); public static Task<Book?> GetBookByIdAsync(int id, BookstoreContext db, CancellationToken ct) => db.Books.FirstOrDefaultAsync(b => b.Id == id, ct); } ``` The rules that matter: - The class is **`partial`** so the generator can add code to it; **static** is the documented default (a non-static `[QueryType]` class is registered as a singleton). - **Naming**: `GetBooks` becomes `books`, `GetBookByIdAsync` becomes `bookById` — the `Get` prefix and `Async` suffix are stripped and the rest is camelCased. `[GraphQLName]` overrides it. - **Arguments vs services**: `id` becomes a GraphQL argument; `BookstoreContext` and `CancellationToken` do not, because a parameter whose type is registered in dependency injection is injected without any `[Service]` attribute. - Several classes can carry `[QueryType]`; the generator merges them into the one `Query` type. Fields on a non-root type are added with `[ObjectType<Book>]` on a **`static partial`** class. A parameter marked `[Parent]` receives the already-resolved `Book`: ```csharp [ObjectType<Book>] public static partial class BookNode { public static Task<Author?> GetAuthorAsync( [Parent] Book book, BookstoreContext db, CancellationToken ct) => db.Authors.FirstOrDefaultAsync(a => a.Id == book.AuthorId, ct); } ``` This keeps API concerns out of the `Book` entity. (As written, this resolver runs one query per book — the N+1 case that a DataLoader fixes.) ## Code-first: ObjectType<T> descriptors The same `author` field in code-first style: ```csharp public sealed class BookType : ObjectType<Book> { protected override void Configure(IObjectTypeDescriptor<Book> descriptor) { descriptor.Ignore(b => b.AuthorId); descriptor.Field("author").Resolve(async ctx => { var book = ctx.Parent<Book>(); var db = ctx.Service<BookstoreContext>(); return await db.Authors.FirstOrDefaultAsync( a => a.Id == book.AuthorId, ctx.RequestAborted); }); } } ``` The resolver receives an `IResolverContext` and pulls the parent, services and cancellation token from it. Code-first types are registered with `AddType<BookType>()`, root types with `AddQueryType<T>()`. ## Wiring it into ASP.NET Core 1. `builder.AddGraphQL()` on the web application builder returns an `IRequestExecutorBuilder` (it forwards to `services.AddGraphQLServer()`, which still exists). 2. `.AddTypes()` calls the **generated** registration method. The project template names it through `[assembly: Module("Types")]`; without that attribute the generator names the method after the assembly, for example `AddBookstoreTypes()` for an assembly called `Bookstore`. 3. `app.MapGraphQL()` maps the endpoint at `/graphql` by default. ## How resolvers receive services Resolver inputs come from three places, and only one of them is visible to clients: - **Arguments** — plain parameters such as `int id`, which appear in the schema. - **Parent** — the `[Parent]` object in a type's resolver class (or `ctx.Parent<T>()` in code-first). - **Services** — any parameter whose type is registered in dependency injection, plus `CancellationToken`. For query fields Hot Chocolate runs resolvers in parallel, so by default each resolver gets services from its **own scope** (`DependencyInjectionScope.Resolver`); two resolvers never share one scoped `DbContext`. Mutation fields run one after another and share the **request scope**. That default is why injecting `BookstoreContext` straight into a query resolver is safe, while keeping one context in a singleton class is not. ## Choosing between the two | Concern | Implementation-first | Code-first | |---|---|---| | Where the schema lives | Next to the C# model, in attributes | In a separate `ObjectType<T>` class | | Registration | Generated at build time | `AddType<T>()` / `AddQueryType<T>()` | | Schema differs a lot from the model | Possible, but attribute-heavy | Natural: rename, ignore, retype in one place | | Resolver inputs | Typed parameters (`[Parent]`, services) | `IResolverContext` lookups | A useful default: implementation-first for application schemas, code-first where the GraphQL contract must diverge from the C# classes or a component is reused. ## Common mistakes - Forgetting `partial` (the generator cannot extend the class) or `static` on an `[ObjectType<T>]` class — analyzers HC0080 and HC0081 report both as errors. - Expecting a service parameter to show up as a GraphQL argument. - Putting a Book field on a `[QueryType]` class and wondering why it appears on `Query`. - Assuming the name `AddTypes()` is fixed: it comes from the template's `[assembly: Module("Types")]`. In a project without that attribute the generated method is `Add<AssemblyName>Types()`, and the framework's own `AddTypes(...)` overloads only add the types you pass them.

  • When would you choose code-first descriptors over implementation-first attributes in Hot Chocolate?
    When the GraphQL shape must differ from the C# model or a schema component is reused: renaming, ignoring or retyping fields in one place, or resolvers written against `IResolverContext`. Implementation-first keeps the contract next to the domain code and generates registration at build time, so the v16 docs treat it as the streamlined path for application schemas. Both styles can contribute to one schema.
  • How do services reach a Hot Chocolate 16 resolver method, and why does that matter for EF Core?
    Any parameter whose type is registered in DI is injected without a `[Service]` attribute and never becomes a GraphQL argument. For queries, Hot Chocolate 16 resolves services from a separate scope per resolver by default (`DependencyInjectionScope.Resolver`), so parallel resolvers never share one scoped `DbContext`; mutations use the request scope because top-level mutation fields run serially.
  • What happens if a [QueryType] class is not static in Hot Chocolate?
    It still works: the source generator registers a non-static `[QueryType]` class as a singleton, which allows constructor-injected dependencies. The docs still recommend static classes because they hold no state. A scoped dependency such as a `DbContext` belongs in the resolver method's parameters, not the singleton's constructor.

The attributes are labels on filing folders: the source generator walks the cabinet at build time and writes the index for you. Code-first is writing the index card for each folder by hand.

saying these in an interview costs you the question

  • Every public method on any class automatically becomes a Query field.
  • Service parameters such as a DbContext show up as GraphQL arguments.
  • The Hot Chocolate source generator discovers types by reflection when the app starts.
  • An [ObjectType<Book>] class adds its methods to the Query type.
  • A resolver on a separate class cannot reach the parent Book object.