skip to content

Why must Hot Chocolate's [UsePaging], [UseProjection], [UseFiltering] and [UseSorting] appear in that order on an IQueryable<Book> resolver?

level: middleimportance: should knowfreq 40%

answer

  1. field middleware is a pipeline
  2. declared order in, reverse order out
  3. compose the IQueryable, then execute
  4. paging outermost slices last
  5. analyzer HC0100 checks the order

basics

~20 s

The attributes become field middleware that runs in declared order while the resolver's IQueryable travels back in reverse: sorting, filtering and projection compose onto the query, and paging, outermost, slices and executes it as one SQL statement.

solid answer

~40 s

Each attribute adds **field middleware**, and middleware runs in the order declared, with the resolver last. The resolver returns an `IQueryable<Book>`, and that result travels back through the chain in reverse: `[UseSorting]` adds `OrderBy`, `[UseFiltering]` adds the `Where` from the `where` argument, `[UseProjection]` adds a `Select` of only the selected fields, and `[UsePaging]`, outermost, applies the page window and executes. EF Core then sends one statement. In a different order, paging could cut the result before filtering or projection is applied. On a resolver method, Hot Chocolate's analyzer HC0100 reports a wrong order as a build error. Defaults to remember: 10 items when `first` is omitted, at most 50, no `totalCount` unless enabled, and at most 64 filter operations per `where` argument.

code

graphql · 10 lines
graphql
query {
  books(
    first: 10
    where: { title: { contains: "GraphQL" } }
    order: { title: ASC }
  ) {
    nodes { title author { name } }
    pageInfo { hasNextPage endCursor }
  }
}

go deeper

for a junior

Recall the four attributes, the order UsePaging, UseProjection, UseFiltering, UseSorting, and the where and order arguments they add.

for a middle

Explain field middleware: declared order in, results back in reverse, and why returning an unexecuted IQueryable lets EF Core emit one statement.

for a senior

Read the SQL a field produces, know the paging and filter-operation defaults, and spot offset scans, unindexed sorts and in-memory filtering.

for a principal

Decide how much query power to expose: which fields are filterable and sortable, page caps, and when the attribute pipeline gives way to explicit QueryContext code.

## The four data attributes Hot Chocolate's data package turns a resolver that returns `IQueryable<T>` into a paged, filtered, sorted and projected GraphQL field. For the bookstore: ```csharp [QueryType] public static partial class BookQueries { [UsePaging] [UseProjection] [UseFiltering] [UseSorting] public static IQueryable<Book> GetBooks(BookstoreContext db) => db.Books; } ``` with `.AddProjections().AddFiltering().AddSorting()` on the GraphQL builder. Each attribute adds schema and behaviour: - **`[UsePaging]`** turns `[Book]` into a cursor **connection** with `first`/`after`/`last`/`before`, `edges`, `nodes` and `pageInfo`. - **`[UseProjection]`** turns the client's **selection set** into a LINQ `Select`, so only requested columns are read, and nested navigation selections become joins. - **`[UseFiltering]`** adds a `where` input argument and translates it to a `Where` predicate. - **`[UseSorting]`** adds an `order` argument and translates it to `OrderBy`/`ThenBy`. ## Why the order matters: field middleware Each attribute registers **field middleware** — a function wrapped around the resolver. Middleware runs **in the order declared**, the resolver comes last, and the **result travels back through the chain in reverse**. So for the declared order above: 1. `UsePaging` calls the next middleware and waits. 2. `UseProjection`, then `UseFiltering`, then `UseSorting` do the same. 3. The resolver returns `db.Books` — an unexecuted `IQueryable<Book>`. 4. On the way back, `UseSorting` adds `OrderBy`, `UseFiltering` adds `Where`, and `UseProjection` adds `Select`. 5. `UsePaging`, the outermost, applies the page window (`Skip`/`Take`) and **executes** the query. Because nothing materialises until the last step, EF Core translates the whole composition into **one SQL statement** — roughly `SELECT b.Title, a.Name FROM Books b JOIN Authors a ON b.AuthorId = a.Id WHERE b.Title LIKE '%GraphQL%' ORDER BY b.Title`, followed by a limit and offset computed from `first` and `after`. Declared in another order, an outer data middleware would be handed the output of paging — a page that has already been cut — instead of the composable query. That is why the docs say the order is `UsePaging` > `UseProjection` > `UseFiltering` > `UseSorting`, and why the analyzer **HC0100** ("Data attributes must be ordered correctly") reports a wrong order on a resolver method as a build error. C# does not guarantee attribute order by itself; Hot Chocolate's middleware attributes capture their **line number** at compile time to fix it, which is another reason to keep one attribute per line. ## Controlling what clients may filter and sort By default `[UseFiltering]` exposes a filter for every field of `Book` that Hot Chocolate can translate, with operations chosen by type — `eq`, `contains`, `startsWith` and friends for strings, `gt`, `lte`, `in` and friends for numbers. That is rarely what a production API wants. A custom `FilterInputType<Book>` that calls `BindFieldsExplicitly()` and lists only `title` and `publishedOn` narrows the `where` input, and `[UseFiltering<BookFilterType>]` (or `[UseFiltering(typeof(BookFilterType))]`) attaches it to the field. Sorting has the same shape with a sort input type. Narrowing is the real protection: it keeps clients on columns you have indexed, and it makes the schema say what the API supports instead of mirroring the table. ## Defaults you will be asked about | Setting | Default | Where to change it | |---|---|---| | `DefaultPageSize` (no `first`/`last` given) | 10 | `[UsePaging(DefaultPageSize = ...)]` or `ModifyPagingOptions` | | `MaxPageSize` | 50 | same | | `IncludeTotalCount` | `false` | same; adds `totalCount` | | `RequirePagingBoundaries` | `false` | same; forces the client to send `first` or `last` | | Filter operations per `where` argument | 64 (since 16) | the filter convention's `MaxAllowedFilterOperations` | The 64-operation cap is new in version 16: a `where` with more operations is rejected with an error instead of producing an enormous SQL predicate. ## What the cost looks like - **Paging over `IQueryable`** with `[UsePaging]` uses index-based cursors translated to `Skip`/`Take`, so deep pages cost an OFFSET scan. Keyset paging comes from the `PagingArguments` + `ToPageAsync` route instead. - **Filtering** runs in the database only if the `where` translates; unindexed `contains` on a large table is still a full scan. - **Sorting** on arbitrary columns needs indexes you have chosen deliberately, or a sort type that exposes only indexed fields. - **Projection** saves columns but not rows; a big nested selection can become a wide join. ## Common mistakes - Returning `List<Book>` or awaiting `ToListAsync()` inside the resolver: the data middleware then work in memory on everything loaded. - Adding `[UseFiltering]` without `.AddFiltering()` on the builder (and likewise for sorting and projections). - Treating `MaxPageSize` as a security control on its own — it caps one list, not a deeply nested document. - Mixing `[UseProjection]` with a `QueryContext<T>` parameter, which Hot Chocolate 16 rejects.

  • What changed in Hot Chocolate 16 about filter size, and how do you raise the limit?
    Version 16 caps a single `where` argument at 64 filter operations by default and returns an error above that, protecting the database from huge predicates. The limit belongs to the filter convention: `AddFiltering(x => x.AddDefaults().MaxAllowedFilterOperations(256))` raises it and `null` removes it. Prefer a narrower filter input type over simply raising the cap.
  • How do you page a Hot Chocolate books field with keyset cursors instead of offsets?
    Take `PagingArguments` as a resolver parameter and call `ToPageAsync(pagingArgs, ct)` on an ordered `IQueryable`; it builds cursors from the sort keys and throws if no `OrderBy` key exists. Return the page as a connection, for example `new PageConnection<Book>(page)` with `[UseConnection]`. Add a unique tiebreaker such as `Id` to the order.

saying these in an interview costs you the question

  • Attribute order is cosmetic because Hot Chocolate sorts the data attributes itself.
  • Filtering should sit above paging so it applies to the final page.
  • [UsePaging] over an IQueryable gives keyset pagination automatically.
  • Returning ToListAsync() results still lets filtering run in SQL.
  • The default page size in Hot Chocolate is unlimited when first is omitted.