From 7e106067d9a65a176af80137b568ce4e31b9683a Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 12 May 2026 16:15:44 -0400 Subject: [PATCH 1/6] [HotChocolate] Rework fetching data docs chapter --- website/src/docs/docs.json | 98 ++--- .../v16/defining-a-schema/arguments.md | 2 +- .../v16/defining-a-schema/documentation.md | 62 ++- .../v16/defining-a-schema/object-types.md | 2 +- .../v16/defining-a-schema/relay.md | 4 +- .../v16/defining-a-schema/scalars.md | 54 +-- .../v16/defining-a-schema/subscriptions.md | 2 +- .../v16/execution-engine/index.md | 209 ---------- .../fetching-data/batching/batch-resolver.md | 117 ++++++ .../{ => batching}/dataloader.md | 146 +------ .../v16/fetching-data/batching/index.md | 86 ++++ .../v16/fetching-data/dependency-injection.md | 24 +- .../hotchocolate/v16/fetching-data/errors.md | 2 +- .../fetching-data/fetching-from-databases.md | 113 ------ .../v16/fetching-data/fetching-from-rest.md | 158 -------- .../field-middleware.md | 3 +- .../hotchocolate/v16/fetching-data/index.md | 2 +- .../v16/fetching-data/projections.md | 2 +- .../v16/fetching-data/resolvers.md | 372 +++++++++--------- .../hotchocolate/v16/fetching-data/sorting.md | 2 +- .../v16/fetching-data/spatial-data.md | 2 +- .../src/docs/hotchocolate/v16/fusion/index.md | 3 - .../hotchocolate/v16/guides/federation.md | 2 +- .../hotchocolate/v16/guides/performance.md | 4 +- .../hotchocolate/v16/guides/public-api.md | 2 +- .../entity-framework.md | 2 +- .../extending-filtering.md | 2 +- .../executable.md => integrations/index.md} | 2 +- .../{fetching-data => integrations}/marten.md | 2 +- .../mongodb.md | 2 +- .../hotchocolate/v16/server/command-line.md | 4 +- .../src/docs/hotchocolate/v16/server/files.md | 2 +- .../hotchocolate/v16/server/http-transport.md | 4 +- .../src/docs/hotchocolate/v16/server/index.md | 6 +- .../v16/server/instrumentation.md | 2 +- .../docs/hotchocolate/v16/server/options.md | 6 +- 36 files changed, 574 insertions(+), 933 deletions(-) delete mode 100644 website/src/docs/hotchocolate/v16/execution-engine/index.md create mode 100644 website/src/docs/hotchocolate/v16/fetching-data/batching/batch-resolver.md rename website/src/docs/hotchocolate/v16/fetching-data/{ => batching}/dataloader.md (63%) create mode 100644 website/src/docs/hotchocolate/v16/fetching-data/batching/index.md delete mode 100644 website/src/docs/hotchocolate/v16/fetching-data/fetching-from-databases.md delete mode 100644 website/src/docs/hotchocolate/v16/fetching-data/fetching-from-rest.md rename website/src/docs/hotchocolate/v16/{execution-engine => fetching-data}/field-middleware.md (98%) delete mode 100644 website/src/docs/hotchocolate/v16/fusion/index.md rename website/src/docs/hotchocolate/v16/{fetching-data => integrations}/entity-framework.md (99%) rename website/src/docs/hotchocolate/v16/{fetching-data => integrations}/extending-filtering.md (99%) rename website/src/docs/hotchocolate/v16/{fetching-data/executable.md => integrations/index.md} (98%) rename website/src/docs/hotchocolate/v16/{fetching-data => integrations}/marten.md (93%) rename website/src/docs/hotchocolate/v16/{fetching-data => integrations}/mongodb.md (97%) diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index 2372e7d5a63..dbe0cf60028 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -577,8 +577,8 @@ "title": "Dependency Injection" }, { - "path": "dataloader", - "title": "DataLoader" + "path": "field-middleware", + "title": "Field Middleware" }, { "path": "pagination", @@ -596,41 +596,53 @@ "path": "projections", "title": "Projections" }, - { - "path": "fetching-from-databases", - "title": "Fetching from Databases" - }, - { - "path": "fetching-from-rest", - "title": "Fetching from REST" - }, - { - "path": "entity-framework", - "title": "Entity Framework" - }, - { - "path": "mongodb", - "title": "MongoDB" - }, { "path": "spatial-data", "title": "Spatial Data" }, { - "path": "marten", - "title": "Marten" + "path": "batching", + "title": "Batching", + "items": [ + { + "path": "dataloader", + "title": "DataLoader" + }, + { + "path": "batch-resolver", + "title": "Batch Resolvers" + } + ] }, { "path": "errors", - "title": "Error Codes" - }, - { - "path": "extending-filtering", - "title": "Extending Filtering" + "title": "Errors" }, { - "path": "executable", - "title": "Executable" + "path": "integrations", + "title": "Integrations", + "items": [ + { + "path": "index", + "title": "Overview" + }, + { + "path": "entity-framework", + "title": "Entity Framework" + }, + { + "path": "mongodb", + "title": "MongoDB" + }, + { + "path": "marten", + "title": "Marten" + }, + { + "path": "extending-filtering", + "title": "Extending Filtering" + } + ] } ] }, @@ -680,20 +692,6 @@ } ] }, - { - "path": "execution-engine", - "title": "Execution Engine", - "items": [ - { - "path": "index", - "title": "Overview" - }, - { - "path": "field-middleware", - "title": "Field Middleware" - } - ] - }, { "path": "server", "title": "Server", @@ -796,24 +794,6 @@ } ] }, - { - "path": "api-reference", - "title": "API Reference", - "items": [ - { - "path": "custom-attributes", - "title": "Attributes" - }, - { - "path": "language", - "title": "Language" - }, - { - "path": "visitors", - "title": "Visitors" - } - ] - }, { "path": "migrating", "title": "Migrating", diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/arguments.md b/website/src/docs/hotchocolate/v16/defining-a-schema/arguments.md index 77c15f8bd6c..1cf88f11d49 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/arguments.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/arguments.md @@ -181,7 +181,7 @@ public static Product? GetProduct( => db.Products.Find(id); ``` -In v16, you can also use the generic form `[ID]` which infers the type name automatically. +You can also use the generic form `[ID]` which infers the type name automatically. # Complex Arguments diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/documentation.md b/website/src/docs/hotchocolate/v16/defining-a-schema/documentation.md index e6fe405a1d6..401032df7bc 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/documentation.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/documentation.md @@ -86,7 +86,7 @@ In code-first, the `Description()` method takes precedence over all other forms # Using XML Documentation Comments -Hot Chocolate can generate descriptions from standard C# XML documentation comments. This lets you maintain a single source of documentation for both your C# code and GraphQL schema. +Hot Chocolate generates descriptions from standard C# XML documentation comments. The source generator extracts them at build time, so your C# code is the single source of documentation for both your code and GraphQL schema. ```csharp /// @@ -128,9 +128,59 @@ public static partial class UserQueries } ``` -## Enabling XML Documentation +## Source Generator (Default) -To make XML docs available at runtime, enable `GenerateDocumentationFile` in your `.csproj`: +Hot Chocolate's source generator extracts XML documentation comments directly from the source code during compilation. This is the default behavior, no additional project configuration is required. + +The source generator reads ``, ``, ``, and `` tags and embeds the extracted text into the generated type configuration. Because this happens at build time, you do not need to ship XML documentation files with your application. + +### Supported Tags + +| Tag | Usage | +| ----------------------------------- | -------------------------------------------------------------------- | +| `` | Sets the description of the type, field, or enum value. | +| `` | Sets the description of a field argument. | +| `` | Appended to the field description under a **Returns** heading. | +| `` | Appended under an **Errors** heading. Requires the `code` attribute. | +| `` | Resolves documentation from a base class or interface. | +| `` | Resolves documentation from a specific member. | + +### Example with Returns and Errors + +```csharp +[QueryType] +public static partial class UserQueries +{ + /// + /// Finds a user by their unique username. + /// + /// The username to search for. + /// The matching user, or null if not found. + /// + /// The caller does not have permission to search users. + /// + public static User? GetUser(string username, UserService users) + => users.FindByName(username); +} +``` + +This produces a field description that includes the summary, a **Returns** section, and an **Errors** section. + +### Disabling Source Generator Documentation + +To prevent the source generator from extracting XML documentation, add the `Module` attribute with the `DisableXmlDocumentation` option: + +```csharp +using HotChocolate; + +[assembly: Module("MyModule", ModuleOptions.Default | ModuleOptions.DisableXmlDocumentation)] +``` + +When `DisableXmlDocumentation` is set, `[GraphQLDescription]` attributes continue to work. Only the automatic extraction of XML comments is suppressed. + +## Runtime XML Documentation + +If you are not using the source generator or need XML documentation from referenced assemblies outside the source generator's scope, Hot Chocolate can read XML documentation files at runtime. Enable `GenerateDocumentationFile` in your `.csproj`: ```xml @@ -141,9 +191,9 @@ To make XML docs available at runtime, enable `GenerateDocumentationFile` in you The `` element is optional. It suppresses compiler warnings for types without documentation comments. -## Disabling XML Documentation +### Disabling Runtime XML Documentation -If you do not want XML comments to appear in the schema: +If you do not want runtime XML comments to appear in the schema: ```csharp builder @@ -161,7 +211,7 @@ When both `[GraphQLDescription]` and XML documentation are present, they follow # Custom Naming Conventions -If you use a custom naming convention and XML documentation, pass an `XmlDocumentationProvider` to the convention so descriptions are preserved: +If you use a custom naming convention and runtime XML documentation, pass an `XmlDocumentationProvider` to the convention so descriptions are preserved. This does not apply when using the source generator, which handles documentation extraction at build time. ```csharp public class CustomNamingConventions : DefaultNamingConventions diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/object-types.md b/website/src/docs/hotchocolate/v16/defining-a-schema/object-types.md index d254384078f..741826766e8 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/object-types.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/object-types.md @@ -576,7 +576,7 @@ For full details on nullability, see [Non-Null](/docs/hotchocolate/v16/defining- # Dictionary Support -Hot Chocolate v16 automatically maps `Dictionary` properties to a list of key-value pair objects. This eliminates the need for custom resolvers when exposing dictionary data. +Hot Chocolate automatically maps `Dictionary` properties to a list of key-value pair objects. This eliminates the need for custom resolvers when exposing dictionary data. diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/relay.md b/website/src/docs/hotchocolate/v16/defining-a-schema/relay.md index cd7f66427eb..32a3cb8c9ef 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/relay.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/relay.md @@ -42,7 +42,7 @@ public class OrderItem } ``` -In v16, the generic `[ID]` form infers the GraphQL type name from the type argument. You can also use `[ID("Product")]` to specify it as a string. +The generic `[ID]` form infers the GraphQL type name from the type argument. You can also use `[ID("Product")]` to specify it as a string. @@ -338,7 +338,7 @@ builder .AddGlobalObjectIdentification(); ``` -In v16, the source generator can produce a `NodeIdValueSerializer` for your custom ID type, reducing the need for manual converter registration. +The source generator can produce a `NodeIdValueSerializer` for your custom ID type, reducing the need for manual converter registration. # Query Field in Mutation Payloads diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/scalars.md b/website/src/docs/hotchocolate/v16/defining-a-schema/scalars.md index ececb57db53..698d725965e 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/scalars.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/scalars.md @@ -8,33 +8,33 @@ Every scalar defines how values convert between the GraphQL wire format (JSON) a Hot Chocolate comes with many more scalars than the GraphQL core scalars, mapping to common .NET primitive types and structs. -| .NET Type | GraphQL Scalar | Binding | Notes | Spec | -| ----------------------- | --------------- | -------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------- | -| `string` | `String` | Implicit | UTF-8 character sequence | [Spec](https://spec.graphql.org/draft/#sec-String) | -| `bool` | `Boolean` | Implicit | `true` or `false` | [Spec](https://spec.graphql.org/draft/#sec-Boolean) | -| `int` | `Int` | Implicit | Signed 32-bit integer | [Spec](https://spec.graphql.org/draft/#sec-Int) | -| `float`, `double` | `Float` | Implicit | IEEE 754 double-precision | [Spec](https://spec.graphql.org/draft/#sec-Float) | -| `string`, `int`, `Guid` | `ID` | Explicit | Unique identifier, always serialized as `string` | [Spec](https://spec.graphql.org/draft/#sec-ID) | -| `decimal` | `Decimal` | Implicit | High-precision decimal (separate from `Float`) | [Spec](https://scalars.graphql.org/chillicream/decimal.html) | -| `long` | `Long` | Implicit | Signed 64-bit integer | [Spec](https://scalars.graphql.org/chillicream/long.html) | -| `short` | `Short` | Implicit | Signed 16-bit integer | [Spec](https://scalars.graphql.org/chillicream/short.html) | -| `DateTime` | `DateTime` | Implicit | Date and time with time zone offset | [Spec](https://scalars.graphql.org/chillicream/date-time.html) | -| `DateTimeOffset` | `DateTime` | Implicit | Date and time with time zone offset | [Spec](https://scalars.graphql.org/chillicream/date-time.html) | -| `DateOnly` | `LocalDate` | Implicit | Date without time or time zone | [Spec](https://scalars.graphql.org/chillicream/local-date.html) | -| `DateOnly` | `Date` | Explicit | Date in UTC | [Spec](https://scalars.graphql.org/chillicream/date.html) | -| `DateTime` | `LocalDateTime` | Explicit | Date and time without time zone | [Spec](https://scalars.graphql.org/chillicream/local-date-time.html) | -| `TimeOnly` | `LocalTime` | Implicit | Time of day without date or time zone | [Spec](https://scalars.graphql.org/chillicream/local-time.html) | -| `TimeSpan` | `Duration` | Implicit | Duration of time (renamed from `TimeSpan` in v16) | [Spec](https://scalars.graphql.org/chillicream/duration.html) | -| `Guid` | `UUID` | Implicit | Universally unique identifier (RFC 9562) | [Spec](https://scalars.graphql.org/chillicream/uuid.html) | -| `Uri` | `URI` | Implicit | Uniform resource identifier (new in v16, replaces `URL` for `System.Uri`) | [Spec](https://scalars.graphql.org/chillicream/uri.html) | -| `Uri` | `URL` | Explicit | Deprecated, use `URI` instead | [Spec](https://scalars.graphql.org/chillicream/url.html) | -| `byte[]` | `Base64String` | Implicit | Base64-encoded byte array (new in v16, replaces deprecated `ByteArray`) | [Spec](https://scalars.graphql.org/chillicream/base64-string.html) | -| `byte` | `UnsignedByte` | Implicit | Unsigned 8-bit integer (renamed in v16) | [Spec](https://scalars.graphql.org/chillicream/unsigned-byte.html) | -| `sbyte` | `Byte` | Implicit | Signed 8-bit integer (renamed in v16) | [Spec](https://scalars.graphql.org/chillicream/byte.html) | -| `ushort` | `UnsignedShort` | Implicit | Unsigned 16-bit integer (new in v16) | [Spec](https://scalars.graphql.org/chillicream/unsigned-short.html) | -| `uint` | `UnsignedInt` | Implicit | Unsigned 32-bit integer (new in v16) | [Spec](https://scalars.graphql.org/chillicream/unsigned-int.html) | -| `ulong` | `UnsignedLong` | Implicit | Unsigned 64-bit integer (new in v16) | [Spec](https://scalars.graphql.org/chillicream/unsigned-long.html) | -| `JsonElement` | `Any` | Implicit | Any valid GraphQL value (v16 merged `Json` into `Any`) | [Spec](https://scalars.graphql.org/chillicream/any.html) | +| .NET Type | GraphQL Scalar | Binding | Notes | Spec | +| ----------------------- | --------------- | -------- | ------------------------------------------------------------- | -------------------------------------------------------------------- | +| `string` | `String` | Implicit | UTF-8 character sequence | [Spec](https://spec.graphql.org/draft/#sec-String) | +| `bool` | `Boolean` | Implicit | `true` or `false` | [Spec](https://spec.graphql.org/draft/#sec-Boolean) | +| `int` | `Int` | Implicit | Signed 32-bit integer | [Spec](https://spec.graphql.org/draft/#sec-Int) | +| `float`, `double` | `Float` | Implicit | IEEE 754 double-precision | [Spec](https://spec.graphql.org/draft/#sec-Float) | +| `string`, `int`, `Guid` | `ID` | Explicit | Unique identifier, always serialized as `string` | [Spec](https://spec.graphql.org/draft/#sec-ID) | +| `decimal` | `Decimal` | Implicit | High-precision decimal (separate from `Float`) | [Spec](https://scalars.graphql.org/chillicream/decimal.html) | +| `long` | `Long` | Implicit | Signed 64-bit integer | [Spec](https://scalars.graphql.org/chillicream/long.html) | +| `short` | `Short` | Implicit | Signed 16-bit integer | [Spec](https://scalars.graphql.org/chillicream/short.html) | +| `DateTime` | `DateTime` | Implicit | Date and time with time zone offset | [Spec](https://scalars.graphql.org/chillicream/date-time.html) | +| `DateTimeOffset` | `DateTime` | Implicit | Date and time with time zone offset | [Spec](https://scalars.graphql.org/chillicream/date-time.html) | +| `DateOnly` | `LocalDate` | Implicit | Date without time or time zone | [Spec](https://scalars.graphql.org/chillicream/local-date.html) | +| `DateOnly` | `Date` | Explicit | Date in UTC | [Spec](https://scalars.graphql.org/chillicream/date.html) | +| `DateTime` | `LocalDateTime` | Explicit | Date and time without time zone | [Spec](https://scalars.graphql.org/chillicream/local-date-time.html) | +| `TimeOnly` | `LocalTime` | Implicit | Time of day without date or time zone | [Spec](https://scalars.graphql.org/chillicream/local-time.html) | +| `TimeSpan` | `Duration` | Implicit | Duration of time | [Spec](https://scalars.graphql.org/chillicream/duration.html) | +| `Guid` | `UUID` | Implicit | Universally unique identifier (RFC 9562) | [Spec](https://scalars.graphql.org/chillicream/uuid.html) | +| `Uri` | `URI` | Implicit | Uniform resource identifier (replaces `URL` for `System.Uri`) | [Spec](https://scalars.graphql.org/chillicream/uri.html) | +| `Uri` | `URL` | Explicit | Deprecated, use `URI` instead | [Spec](https://scalars.graphql.org/chillicream/url.html) | +| `byte[]` | `Base64String` | Implicit | Base64-encoded byte array (replaces deprecated `ByteArray`) | [Spec](https://scalars.graphql.org/chillicream/base64-string.html) | +| `byte` | `UnsignedByte` | Implicit | Unsigned 8-bit integer | [Spec](https://scalars.graphql.org/chillicream/unsigned-byte.html) | +| `sbyte` | `Byte` | Implicit | Signed 8-bit integer | [Spec](https://scalars.graphql.org/chillicream/byte.html) | +| `ushort` | `UnsignedShort` | Implicit | Unsigned 16-bit integer | [Spec](https://scalars.graphql.org/chillicream/unsigned-short.html) | +| `uint` | `UnsignedInt` | Implicit | Unsigned 32-bit integer | [Spec](https://scalars.graphql.org/chillicream/unsigned-int.html) | +| `ulong` | `UnsignedLong` | Implicit | Unsigned 64-bit integer | [Spec](https://scalars.graphql.org/chillicream/unsigned-long.html) | +| `JsonElement` | `Any` | Implicit | Any valid GraphQL value | [Spec](https://scalars.graphql.org/chillicream/any.html) | > **Note:** Hot Chocolate only exposes scalars that your schema uses. Unused scalars do not appear in the generated schema. diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/subscriptions.md b/website/src/docs/hotchocolate/v16/defining-a-schema/subscriptions.md index 8ab32646dbd..9cd6da5003e 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/subscriptions.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/subscriptions.md @@ -261,7 +261,7 @@ The Redis provider uses [StackExchange.Redis](https://github.com/StackExchange/S ## NATS -The NATS provider is new in Hot Chocolate v16. Like Redis, it supports multi-instance deployments. NATS uses core publish/subscribe. JetStream is not required. +The NATS provider supports multi-instance deployments, similar to Redis. NATS uses core publish/subscribe. JetStream is not required. Install the packages: diff --git a/website/src/docs/hotchocolate/v16/execution-engine/index.md b/website/src/docs/hotchocolate/v16/execution-engine/index.md deleted file mode 100644 index d937292f0eb..00000000000 --- a/website/src/docs/hotchocolate/v16/execution-engine/index.md +++ /dev/null @@ -1,209 +0,0 @@ ---- -title: Execution Engine -description: Learn about the Hot Chocolate v16 request execution pipeline, keyed middleware, and how to add custom middleware with precise ordering. ---- - -# Overview - -The Hot Chocolate execution engine processes GraphQL requests through a pipeline of request middleware. Each middleware handles one step of execution, such as parsing the document, validating semantics, or executing the operation. You can extend, replace, or reorder middleware in this pipeline. - -# Request Pipeline - -When a GraphQL request arrives, the execution engine passes a `RequestContext` through a chain of middleware. Each middleware performs its work and then calls the next middleware in the chain. The default pipeline includes the following middleware, in order: - -1. **InstrumentationMiddleware** -- Records diagnostic events and telemetry -2. **ExceptionMiddleware** -- Catches unhandled exceptions and converts them to GraphQL errors -3. **TimeoutMiddleware** -- Enforces execution timeout -4. **DocumentCacheMiddleware** -- Looks up previously parsed documents in the cache -5. **DocumentParserMiddleware** -- Parses the GraphQL document from source text -6. **DocumentValidationMiddleware** -- Validates the document against the schema -7. **OperationCacheMiddleware** -- Looks up previously compiled operations in the cache -8. **OperationResolverMiddleware** -- Resolves and compiles the operation from the document -9. **SkipWarmupExecutionMiddleware** -- Short-circuits execution for warmup requests -10. **OperationVariableCoercionMiddleware** -- Coerces variable values to their expected types -11. **OperationExecutionMiddleware** -- Executes the compiled operation and produces a result - -# RequestContext - -In v16, the `IRequestContext` interface has been replaced by the concrete `RequestContext` class. This class carries all request state through the pipeline. - -Key properties on `RequestContext`: - -| Property | Type | Description | -| ----------------------- | ------------------------------------------ | --------------------------------------- | -| `Schema` | `ISchemaDefinition` | The schema the request executes against | -| `Request` | `IOperationRequest` | The incoming GraphQL request | -| `RequestServices` | `IServiceProvider` | The request-scoped service provider | -| `OperationDocumentInfo` | `OperationDocumentInfo` | Parsed document metadata | -| `RequestAborted` | `CancellationToken` | Cancellation token for the request | -| `VariableValues` | `ImmutableArray` | Coerced variable value sets | -| `Result` | `IExecutionResult?` | The execution result | -| `ContextData` | `IDictionary` | Arbitrary request state | -| `Features` | `IFeatureCollection` | Feature collection for extensibility | - -# OperationDocumentInfo - -Document-related information that was previously scattered across `IRequestContext` properties is now consolidated into the `OperationDocumentInfo` class, accessible via `RequestContext.OperationDocumentInfo`. - -| Property | Type | Description | -| ---------------- | ----------------------- | ---------------------------------------------------------- | -| `Document` | `DocumentNode?` | The parsed GraphQL document | -| `Id` | `OperationDocumentId` | Unique document identifier | -| `Hash` | `OperationDocumentHash` | Hash of the document | -| `OperationCount` | `int` | Number of operation definitions in the document | -| `IsCached` | `bool` | Whether the document was retrieved from the cache | -| `IsPersisted` | `bool` | Whether the document came from a persisted operation store | -| `IsValidated` | `bool` | Whether the document has been validated | - -# Keyed Middleware Pipeline - -In v16, the request pipeline uses a keyed middleware system. Every built-in middleware has a unique key defined in `WellKnownRequestMiddleware`. This allows you to insert custom middleware at a precise position relative to any built-in middleware. - -## Adding Custom Middleware - -Use the `UseRequest()` method on `IRequestExecutorBuilder` to add middleware. The method accepts optional `key`, `before`, and `after` parameters for positioning. - -### Append to the end of the pipeline - -When you call `UseRequest()` without positioning parameters, the middleware is appended to the end of the pipeline: - -```csharp -builder - .AddGraphQL() - .UseRequest(next => async context => - { - // Custom logic before the next middleware - await next(context); - // Custom logic after the next middleware - }); -``` - -### Insert before a specific middleware - -Use the `before` parameter with a `WellKnownRequestMiddleware` constant to insert your middleware before a built-in one: - -```csharp -builder - .AddGraphQL() - .UseRequest( - middleware: next => async context => - { - // Runs before document validation - await next(context); - }, - key: "MyPreValidationMiddleware", - before: WellKnownRequestMiddleware.DocumentValidationMiddleware); -``` - -### Insert after a specific middleware - -Use the `after` parameter to insert your middleware after a built-in one: - -```csharp -builder - .AddGraphQL() - .UseRequest( - middleware: next => async context => - { - // Runs after document parsing completes - await next(context); - }, - key: "MyPostParsingMiddleware", - after: WellKnownRequestMiddleware.DocumentParserMiddleware); -``` - -### Prevent duplicate registration - -Set `allowMultiple` to `false` (the default) so that if a middleware with the same key already exists, the registration is skipped: - -```csharp -builder - .AddGraphQL() - .UseRequest( - middleware: next => async context => - { - await next(context); - }, - key: "MyMiddleware", - after: WellKnownRequestMiddleware.ExceptionMiddleware, - allowMultiple: false); -``` - -> You can specify either `before` or `after`, but not both at the same time. If neither is specified, the middleware is appended to the end. - -## Using a Class-Based Middleware - -You can define middleware as a class instead of a delegate. The class receives the next `RequestDelegate` in its constructor: - -```csharp -public class MyRequestMiddleware -{ - private readonly RequestDelegate _next; - - public MyRequestMiddleware(RequestDelegate next) - { - _next = next; - } - - public async ValueTask InvokeAsync(RequestContext context) - { - // Pre-processing logic - - await _next(context); - - // Post-processing logic - } -} -``` - -Register it with precise positioning using the generic `UseRequest()` overload: - -```csharp -builder - .AddGraphQL() - .UseRequest( - key: "MyRequestMiddleware", - after: WellKnownRequestMiddleware.DocumentValidationMiddleware); -``` - -## WellKnownRequestMiddleware Constants - -The `WellKnownRequestMiddleware` static class provides constants for all built-in middleware keys: - -| Constant | Description | -| ----------------------------------------------- | -------------------------------------- | -| `InstrumentationMiddleware` | Diagnostic events and telemetry | -| `ExceptionMiddleware` | Unhandled exception handling | -| `TimeoutMiddleware` | Execution timeout enforcement | -| `DocumentCacheMiddleware` | Document cache lookup | -| `DocumentParserMiddleware` | GraphQL document parsing | -| `DocumentValidationMiddleware` | Document validation | -| `OperationCacheMiddleware` | Compiled operation cache lookup | -| `OperationResolverMiddleware` | Operation resolution and compilation | -| `SkipWarmupExecutionMiddleware` | Warmup request short-circuit | -| `OperationVariableCoercionMiddleware` | Variable coercion | -| `OperationExecutionMiddleware` | Operation execution | -| `ReadPersistedOperationMiddleware` | Persisted operation lookup | -| `WritePersistedOperationMiddleware` | Persisted operation storage | -| `PersistedOperationNotFoundMiddleware` | Persisted operation not-found handling | -| `AutomaticPersistedOperationNotFoundMiddleware` | APQ not-found handling | -| `OnlyPersistedOperationsAllowed` | Enforce persisted-operations-only mode | -| `AuthorizeRequestMiddleware` | Request authorization | -| `PrepareAuthorizationMiddleware` | Authorization preparation | -| `CostAnalyzerMiddleware` | Cost analysis | - -# Field Middleware - -Field middleware runs during field resolution, allowing you to add logic before or after a resolver executes. Field middleware is separate from request middleware and operates at the field level. - -[Learn more about field middleware](/docs/hotchocolate/v16/execution-engine/field-middleware) - -# Resolver Compiler - -The resolver compiler builds an optimized resolver pipeline for each field. You can customize it by providing parameter expression builders. - -# Next Steps - -- [Field middleware](/docs/hotchocolate/v16/execution-engine/field-middleware) for per-field processing -- [Instrumentation](/docs/hotchocolate/v16/server/instrumentation) for diagnostic events -- [Error handling](/docs/hotchocolate/v16/fetching-data/errors) for error filters and error builders diff --git a/website/src/docs/hotchocolate/v16/fetching-data/batching/batch-resolver.md b/website/src/docs/hotchocolate/v16/fetching-data/batching/batch-resolver.md new file mode 100644 index 00000000000..d1d654fc60d --- /dev/null +++ b/website/src/docs/hotchocolate/v16/fetching-data/batching/batch-resolver.md @@ -0,0 +1,117 @@ +--- +title: "Batch Resolvers" +--- + +Batch resolvers are an alternative to DataLoaders for cases where you want to resolve a field for multiple parent objects in a single method call without defining a separate DataLoader class. Instead of each resolver running independently and batching through a DataLoader, the execution engine collects all parent objects and calls your resolver once with the full list. + +# When to Use Batch Resolvers vs DataLoaders + +**Use a DataLoader** when the batched data is reused across multiple fields or resolvers. DataLoaders cache by key, so the same entity fetched in different parts of the query tree is only loaded once. + +**Use a batch resolver** when the resolved value is specific to one field and does not benefit from cross-field caching. Common examples: computed values, string formatting, or calling an external service that supports batch requests natively. + +# Defining a Batch Resolver + +Mark a method with `[BatchResolver]`. The `[Parent]` parameter and all arguments must be list types. The return type must also be a list, with one element per parent. + +**C# resolver** + +```csharp +[ObjectType] +public static partial class UserNode +{ + [BatchResolver] + public static List GetDisplayName([Parent] List users) + { + return users.Select(u => $"{u.FirstName} {u.LastName}").ToList(); + } +} +``` + +The execution engine collects all `User` parent objects being resolved in the current wave and calls `GetDisplayName` once with the full list. The returned list must have the same count and order as the input list. + +# Batch Resolvers with Services and Arguments + +Batch resolvers support dependency injection and field arguments. Arguments that are list types are collected from each parent context. + +```csharp +[ObjectType] +public static partial class UserNode +{ + [BatchResolver] + public static async Task> GetGreeting( + [Parent] List users, + GreetingService greetingService, + CancellationToken ct) + { + return await greetingService.GetGreetingsAsync( + users.Select(u => u.Id).ToList(), ct); + } +} +``` + +# Handling Errors in Batch Resolvers + +Use `ResolverResult` to return per-item errors without failing the entire batch. Each element in the returned list can be either a success or an error. + +```csharp +[ObjectType] +public static partial class UserNode +{ + [BatchResolver] + public static List GetVerificationStatus([Parent] List users) + { + return users.Select(user => + { + if (user.Email is null) + return ResolverResult.Fail( + ErrorBuilder.New() + .SetMessage("User has no email address.") + .Build()); + + return ResolverResult.Ok(user.IsVerified ? "verified" : "pending"); + }).ToList(); + } +} +``` + +# Code-First Batch Resolvers + +In the code-first approach, use `ResolveBatch` on the field descriptor. + +```csharp +public class UserType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field("displayName") + .Type() + .ResolveBatch(contexts => + { + var results = new ResolverResult[contexts.Count]; + + for (var i = 0; i < contexts.Count; i++) + { + var user = contexts[i].Parent(); + results[i] = ResolverResult.Ok($"{user.FirstName} {user.LastName}"); + } + + return new ValueTask>(results); + }); + } +} +``` + +You can also point to an external method with `ResolveBatchWith`. + +```csharp +descriptor + .Field("greeting") + .ResolveBatchWith(t => t.GetGreeting(default!)); +``` + +# Next Steps + +- **Need to understand the N+1 problem?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for key-based batching with caching. +- **Need to understand resolver basics?** See [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/dataloader.md b/website/src/docs/hotchocolate/v16/fetching-data/batching/dataloader.md similarity index 63% rename from website/src/docs/hotchocolate/v16/fetching-data/dataloader.md rename to website/src/docs/hotchocolate/v16/fetching-data/batching/dataloader.md index f899e75da9f..747b5671657 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/dataloader.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/batching/dataloader.md @@ -4,7 +4,7 @@ title: "DataLoader" DataLoaders solve the N+1 problem in GraphQL. When the execution engine resolves a list of objects and each object needs related data, a naive implementation fires one database query per object. A DataLoader collects all those individual requests, waits for the execution engine to finish the current batch of resolvers, and then sends one query for all requested keys at once. -This page covers the source-generated DataLoader (the recommended approach), manual DataLoader classes, and batch resolvers (a v16 alternative for simpler cases). If you are new to GraphQL data fetching, start with [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) first. +This page covers the source-generated DataLoader (the recommended approach) and manual DataLoader classes. If you are new to GraphQL data fetching, start with [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) first. # The N+1 Problem @@ -175,146 +175,6 @@ A DataLoader caches results for the duration of a single GraphQL request. If the The cache is not shared across requests. Each request gets a fresh DataLoader instance with an empty cache. -# Batch Resolvers - -Batch resolvers are an alternative to DataLoaders for cases where you want to resolve a field for multiple parent objects in a single method call without defining a separate DataLoader class. Instead of each resolver running independently and batching through a DataLoader, the execution engine collects all parent objects and calls your resolver once with the full list. - -## When to Use Batch Resolvers vs DataLoaders - -**Use a DataLoader** when the batched data is reused across multiple fields or resolvers. DataLoaders cache by key, so the same entity fetched in different parts of the query tree is only loaded once. - -**Use a batch resolver** when the resolved value is specific to one field and does not benefit from cross-field caching. Common examples: computed values, string formatting, or calling an external service that supports batch requests natively. - -## Defining a Batch Resolver - -Mark a method with `[BatchResolver]`. The `[Parent]` parameter and all arguments must be list types. The return type must also be a list, with one element per parent. - -**C# resolver** - -```csharp -[ObjectType] -public static partial class UserNode -{ - [BatchResolver] - public static List GetDisplayName([Parent] List users) - { - return users.Select(u => $"{u.FirstName} {u.LastName}").ToList(); - } -} -``` - -The execution engine collects all `User` parent objects being resolved in the current wave and calls `GetDisplayName` once with the full list. The returned list must have the same count and order as the input list. - -## Batch Resolvers with Services and Arguments - -Batch resolvers support dependency injection and field arguments. Arguments that are list types are collected from each parent context. - -```csharp -[ObjectType] -public static partial class UserNode -{ - [BatchResolver] - public static async Task> GetGreeting( - [Parent] List users, - GreetingService greetingService, - CancellationToken ct) - { - return await greetingService.GetGreetingsAsync( - users.Select(u => u.Id).ToList(), ct); - } -} -``` - -## Handling Errors in Batch Resolvers - -Use `ResolverResult` to return per-item errors without failing the entire batch. Each element in the returned list can be either a success or an error. - -```csharp -[ObjectType] -public static partial class UserNode -{ - [BatchResolver] - public static List GetVerificationStatus([Parent] List users) - { - return users.Select(user => - { - if (user.Email is null) - return ResolverResult.Fail( - ErrorBuilder.New() - .SetMessage("User has no email address.") - .Build()); - - return ResolverResult.Ok(user.IsVerified ? "verified" : "pending"); - }).ToList(); - } -} -``` - -## Code-First Batch Resolvers - -In the code-first approach, use `ResolveBatch` on the field descriptor. - -```csharp -public class UserType : ObjectType -{ - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .Field("displayName") - .Type() - .ResolveBatch(contexts => - { - var results = new ResolverResult[contexts.Count]; - - for (var i = 0; i < contexts.Count; i++) - { - var user = contexts[i].Parent(); - results[i] = ResolverResult.Ok($"{user.FirstName} {user.LastName}"); - } - - return new ValueTask>(results); - }); - } -} -``` - -You can also point to an external method with `ResolveBatchWith`. - -```csharp -descriptor - .Field("greeting") - .ResolveBatchWith(t => t.GetGreeting(default!)); -``` - -# Conditional Data Fetching with Field Selection - -The `[IsSelected]` attribute lets a resolver check whether specific fields are selected in the query. This is useful for skipping expensive data fetching when the client does not need the result. - -```csharp -[ObjectType] -public static partial class ProductNode -{ - public static async Task GetDetailsAsync( - [Parent] Product product, - [IsSelected(nameof(ProductDetails.Inventory))] bool inventorySelected, - IProductDetailsDataLoader detailsLoader, - IInventoryService inventoryService, - CancellationToken ct) - { - var details = await detailsLoader.LoadAsync(product.Id, ct); - - if (inventorySelected) - { - details.Inventory = await inventoryService.GetStockAsync(product.Id, ct); - } - - return details; - } -} -``` - -If the client query does not select the `inventory` field, `inventorySelected` is `false` and the inventory service call is skipped. - # Manual DataLoader Classes You can write DataLoader classes by hand when you need full control over the batching logic. This is rarely needed since the source generator covers most cases. @@ -351,7 +211,7 @@ public class BrandByIdDataLoader : BatchDataLoader # Next Steps +- **Need simpler batching without caching?** See [Batch Resolvers](/docs/hotchocolate/v16/fetching-data/batching/batch-resolver). - **Need to understand resolver basics?** See [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). - **Need pagination?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination) for cursor-based connections. -- **Need to filter or sort data?** See [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [Sorting](/docs/hotchocolate/v16/fetching-data/sorting). -- **Using Entity Framework?** See [Entity Framework](/docs/hotchocolate/v16/fetching-data/entity-framework) for integration patterns with DataLoaders. +- **Using Entity Framework?** See [Entity Framework](/docs/hotchocolate/v16/fetching-data/integrations/entity-framework) for integration patterns with DataLoaders. diff --git a/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md b/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md new file mode 100644 index 00000000000..a7d4e51fef3 --- /dev/null +++ b/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md @@ -0,0 +1,86 @@ +--- +title: Overview +--- + +Every field in a GraphQL schema is backed by a resolver function that produces the field's value. Understanding how resolvers compose into a tree is the key mental model for building efficient GraphQL APIs with Hot Chocolate. + +# The Resolver Tree + +When Hot Chocolate receives a query, it builds a resolver tree that mirrors the shape of the request. Consider this query: + +```graphql +query { + me { + name + company { + id + name + } + } +} +``` + +This produces the following resolver tree: + +```mermaid +graph LR + A(query: QueryType) --> B(me: UserType) + B --> C(name: StringType) + B --> D(company: CompanyType) + D --> E(id: IdType) + D --> F(name: StringType) +``` + +The execution engine traverses this tree starting from root resolvers. A child resolver can only execute after its parent has produced a value. Sibling resolvers at the same level run in parallel. Because of this parallel execution, resolvers (except top-level mutation fields) must be free of side effects. + +Execution completes when every resolver in the tree has produced a result. + +# Resolvers + +Resolvers are the building blocks of data fetching. A resolver can call a database, a REST API, a gRPC service, or any other data source. In Hot Chocolate, the source generator is the primary way to define resolvers. You write plain C# methods and the generator wires them into the schema. + +[Learn more about resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) + +# DataLoader + +DataLoaders deduplicate and batch requests to data sources. When multiple resolvers request the same entity in a single request, a DataLoader ensures only one call goes to the backing store. DataLoaders can significantly reduce the load on your databases and services. + +[Learn more about DataLoaders](/docs/hotchocolate/v16/fetching-data/dataloader) + +# Pagination + +Hot Chocolate provides cursor-based connection pagination out of the box. Connections follow the [Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm), giving clients a standardized way to page through large datasets. When backed by `IQueryable`, pagination translates directly to native database queries. + +[Learn more about pagination](/docs/hotchocolate/v16/fetching-data/pagination) + +# Filtering + +When you return a list of entities, clients often need to filter them by operations like `equals`, `contains`, or `startsWith`. Hot Chocolate generates the necessary filter input types from your .NET models and translates applied filters into native database queries. + +[Learn more about filtering](/docs/hotchocolate/v16/fetching-data/filtering) + +# Sorting + +Hot Chocolate generates sort input types from your .NET models, allowing clients to specify which fields to sort by and in which direction. Like filtering, sort operations translate to native database queries when backed by `IQueryable`. + +[Learn more about sorting](/docs/hotchocolate/v16/fetching-data/sorting) + +# Projections + +Projections optimize database queries by selecting only the columns that match the fields requested in the GraphQL query. If a client requests `name` and `id`, Hot Chocolate queries only those columns from the database. + +[Learn more about projections](/docs/hotchocolate/v16/fetching-data/projections) + +# Data Sources + +Hot Chocolate is not bound to a specific database or architecture. You can fetch data from any source in your resolvers. We provide specific guidance for the most common patterns: + +- [Fetching from databases](/docs/hotchocolate/v16/fetching-data/fetching-from-databases) +- [Fetching from REST APIs](/docs/hotchocolate/v16/fetching-data/fetching-from-rest) + +# Next Steps + +- **New to resolvers?** Start with [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). +- **Need to batch data access?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader). +- **Need to page through lists?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination). +- **Need to filter or sort?** See [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [Sorting](/docs/hotchocolate/v16/fetching-data/sorting). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md b/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md index 8392900d337..84fc85f458f 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md @@ -20,7 +20,7 @@ builder.Services # Implicit Service Injection -In v16, Hot Chocolate automatically recognizes types registered as services in the DI container and injects them into resolver method parameters without requiring any attribute. This works similarly to [Minimal APIs parameter binding](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding). +Hot Chocolate automatically recognizes types registered as services in the DI container and injects them into resolver method parameters without requiring any attribute. This works similarly to [Minimal APIs parameter binding](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding). When the execution engine encounters a resolver parameter whose type is registered in the DI container, it resolves the service automatically. You do not need to apply the `[Service]` attribute. @@ -240,6 +240,28 @@ Take a look at the implementation-first or code-first example. +# Accessing the HttpContext + +The [IHttpContextAccessor](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) allows you to access the [HttpContext](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext) of the current request from within your resolvers. This is useful when you need to read or set a header or cookie. + +First register the `IHttpContextAccessor` as a service. + +```csharp +builder.Services.AddHttpContextAccessor(); +``` + +Then inject it into your resolver. + +```csharp +public string Foo(string id, IHttpContextAccessor httpContextAccessor) +{ + if (httpContextAccessor.HttpContext is not null) + { + // Omitted code for brevity + } +} +``` + # Switching the Service Provider While Hot Chocolate's internals rely on Microsoft's dependency injection container, you are not required to manage your own dependencies using this container. By default, Hot Chocolate uses the request-scoped [`HttpContext.RequestServices`](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestservices) `IServiceProvider` to provide services to your resolvers. diff --git a/website/src/docs/hotchocolate/v16/fetching-data/errors.md b/website/src/docs/hotchocolate/v16/fetching-data/errors.md index 0bb3728efc5..56a861bb16e 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/errors.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/errors.md @@ -1,6 +1,6 @@ --- title: Errors -description: Learn how to handle, create, and filter GraphQL errors in Hot Chocolate v16. +description: Learn how to handle, create, and filter GraphQL errors in Hot Chocolate. --- Hot Chocolate provides several ways to report errors from your GraphQL resolvers. You can return `IError` instances, throw a `GraphQLException`, or use non-terminating field errors through `IResolverContext.ReportError`. diff --git a/website/src/docs/hotchocolate/v16/fetching-data/fetching-from-databases.md b/website/src/docs/hotchocolate/v16/fetching-data/fetching-from-databases.md deleted file mode 100644 index b238b366553..00000000000 --- a/website/src/docs/hotchocolate/v16/fetching-data/fetching-from-databases.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: "Fetching from Databases" ---- - -Hot Chocolate is not bound to a specific database, pattern, or architecture. You can call any data source from your resolvers. This page shows a practical example of fetching data from a database and exposing it through a GraphQL API. - -[Hot Chocolate provides integrations](/docs/hotchocolate/v16/integrations) for Entity Framework Core, MongoDB, and other databases. These integrations add convenience on top of the core resolver model but are not required. - -# Fetching from MongoDB - -In this example, you inject a MongoDB collection into a resolver and query it directly. - - - - -```csharp -public class Book -{ - public Guid Id { get; set; } - public string Title { get; set; } - public string Author { get; set; } -} - -[QueryType] -public static partial class BookQueries -{ - public static async Task GetBookByIdAsync( - Guid id, - IMongoCollection collection, - CancellationToken ct) - => await collection.Find(x => x.Id == id).FirstOrDefaultAsync(ct); -} -``` - -```csharp -builder - .AddGraphQL() - .AddTypes(); -``` - - - - -```csharp -public class Book -{ - public Guid Id { get; set; } - public string Title { get; set; } - public string Author { get; set; } -} - -public class BookQueries -{ - public async Task GetBookByIdAsync( - Guid id, - IMongoCollection collection, - CancellationToken ct) - => await collection.Find(x => x.Id == id).FirstOrDefaultAsync(ct); -} - -public class BookQueriesType : ObjectType -{ - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .Field(f => f.GetBookByIdAsync(default, default!, default)) - .Type(); - } -} -``` - -```csharp -builder - .AddGraphQL() - .AddQueryType(); -``` - - - - -# Fetching from Entity Framework Core - -When using EF Core, inject your `DbContext` directly into resolvers. Hot Chocolate's EF Core integration registers the context correctly for concurrent resolver execution. - -```csharp -[QueryType] -public static partial class BookQueries -{ - public static async Task GetBookByIdAsync( - int id, - CatalogContext db, - CancellationToken ct) - => await db.Books.FindAsync([id], ct); - - [UsePaging] - [UseProjection] - [UseFiltering] - [UseSorting] - public static IQueryable GetBooks(CatalogContext db) - => db.Books; -} -``` - -When you return `IQueryable`, the pagination, projection, filtering, and sorting middleware translate to native SQL queries. The database handles the heavy lifting. - -[Learn more about the Entity Framework integration](/docs/hotchocolate/v16/fetching-data/entity-framework) - -# Next Steps - -- **Need to batch database calls?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader). -- **Need to optimize SQL queries?** See [Projections](/docs/hotchocolate/v16/fetching-data/projections). -- **Need to integrate with MongoDB?** See [MongoDB Integration](/docs/hotchocolate/v16/fetching-data/mongodb). -- **Need to integrate with EF Core?** See [Entity Framework Integration](/docs/hotchocolate/v16/fetching-data/entity-framework). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/fetching-from-rest.md b/website/src/docs/hotchocolate/v16/fetching-data/fetching-from-rest.md deleted file mode 100644 index c73ec141ad2..00000000000 --- a/website/src/docs/hotchocolate/v16/fetching-data/fetching-from-rest.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: "Fetching from REST" ---- - -GraphQL requires knowledge of the types it returns at build time. When wrapping a REST API, the most reliable approach is to generate a typed .NET client from an OpenAPI specification and inject it into your resolvers. - -# Generating a Client from OpenAPI - -If your REST endpoint exposes an OpenAPI specification (Swagger), you can generate a fully typed .NET client for it. - -## Step 1: Get the OpenAPI Specification - -Download the `swagger.json` from your REST endpoint: - -```bash -curl -o swagger.json http://localhost:5000/swagger/v1/swagger.json -``` - -## Step 2: Generate the Client - -Use the NSwag CLI tool to generate a C# client: - -```bash -dotnet new tool-manifest -dotnet tool install NSwag.ConsoleCore -dotnet nswag swagger2csclient /input:swagger.json /classname:TodoService /namespace:TodoReader /output:TodoService.cs -``` - -This generates a `TodoService.cs` file with a typed client for your REST API. The generated client requires `Newtonsoft.Json`: - - - -# Exposing the REST API - -Register the generated client in your DI container and inject it into your resolvers. - - - - -```csharp -[QueryType] -public static partial class TodoQueries -{ - public static async Task> GetTodosAsync( - TodoService service, - CancellationToken ct) - => await service.GetAllAsync(ct); - - public static async Task GetTodoByIdAsync( - long id, - TodoService service, - CancellationToken ct) - => await service.GetByIdAsync(id, ct); -} -``` - -```csharp -builder.Services.AddHttpClient(); - -builder - .AddGraphQL() - .AddTypes(); -``` - - - - -```csharp -public class TodoQueries -{ - public async Task> GetTodosAsync( - TodoService service, - CancellationToken ct) - => await service.GetAllAsync(ct); - - public async Task GetTodoByIdAsync( - long id, - TodoService service, - CancellationToken ct) - => await service.GetByIdAsync(id, ct); -} - -public class TodoQueriesType : ObjectType -{ - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .Field(f => f.GetTodoByIdAsync(default, default!, default)) - .Type(); - - descriptor - .Field(f => f.GetTodosAsync(default!, default)) - .Type>(); - } -} -``` - -```csharp -builder.Services.AddHttpClient(); - -builder - .AddGraphQL() - .AddQueryType(); -``` - - - - -You can now open Nitro on your GraphQL server at `/graphql` and query your REST data: - -```graphql -{ - todoById(id: 1) { - id - isComplete - name - } - todos { - id - isComplete - name - } -} -``` - -# Using DataLoaders with REST - -When multiple GraphQL fields resolve data from the same REST endpoint, use a [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader) to batch and deduplicate calls. This prevents sending redundant HTTP requests for the same resource. - -```csharp -public class TodoByIdDataLoader : BatchDataLoader -{ - private readonly TodoService _service; - - public TodoByIdDataLoader( - TodoService service, - IBatchScheduler batchScheduler, - DataLoaderOptions? options = null) - : base(batchScheduler, options) - { - _service = service; - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken ct) - { - var todos = await _service.GetByIdsAsync(keys, ct); - return todos.ToDictionary(t => t.Id); - } -} -``` - -# Next Steps - -- **Need to batch REST calls?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader). -- **Need to fetch from a database instead?** See [Fetching from Databases](/docs/hotchocolate/v16/fetching-data/fetching-from-databases). -- **Need to understand resolvers?** See [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). diff --git a/website/src/docs/hotchocolate/v16/execution-engine/field-middleware.md b/website/src/docs/hotchocolate/v16/fetching-data/field-middleware.md similarity index 98% rename from website/src/docs/hotchocolate/v16/execution-engine/field-middleware.md rename to website/src/docs/hotchocolate/v16/fetching-data/field-middleware.md index 7021db0c783..87a14f78ae2 100644 --- a/website/src/docs/hotchocolate/v16/execution-engine/field-middleware.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/field-middleware.md @@ -1,6 +1,6 @@ --- title: Field Middleware -description: Learn how to create and apply field middleware in Hot Chocolate v16 to run reusable logic before or after field resolvers. +description: Learn how to create and apply field middleware in Hot Chocolate to run reusable logic before or after field resolvers. --- Field middleware is one of the fundamental components in Hot Chocolate. It allows you to create reusable logic that runs before or after a field resolver. Field middleware is composable: you can specify multiple middleware, and they execute in order. The field resolver is always the last element in the middleware chain. @@ -284,7 +284,6 @@ descriptor # Next Steps -- [Execution engine overview](/docs/hotchocolate/v16/execution-engine) for request-level middleware - [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) for field resolution - [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [sorting](/docs/hotchocolate/v16/fetching-data/sorting) middleware - [Pagination](/docs/hotchocolate/v16/fetching-data/pagination) middleware diff --git a/website/src/docs/hotchocolate/v16/fetching-data/index.md b/website/src/docs/hotchocolate/v16/fetching-data/index.md index cb3251e43dd..a7d4e51fef3 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/index.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/index.md @@ -37,7 +37,7 @@ Execution completes when every resolver in the tree has produced a result. # Resolvers -Resolvers are the building blocks of data fetching. A resolver can call a database, a REST API, a gRPC service, or any other data source. In Hot Chocolate v16, the source generator is the primary way to define resolvers. You write plain C# methods and the generator wires them into the schema. +Resolvers are the building blocks of data fetching. A resolver can call a database, a REST API, a gRPC service, or any other data source. In Hot Chocolate, the source generator is the primary way to define resolvers. You write plain C# methods and the generator wires them into the schema. [Learn more about resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) diff --git a/website/src/docs/hotchocolate/v16/fetching-data/projections.md b/website/src/docs/hotchocolate/v16/fetching-data/projections.md index ff85c8904ff..de5e8c6c413 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/projections.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/projections.md @@ -82,7 +82,7 @@ The projection middleware creates a `Select` expression for the entire subtree o # QueryContext<T> Pattern -In v16, `QueryContext` provides an alternative to the `[UseProjection]` middleware. Instead of applying projections as middleware, you return a `QueryContext` from your resolver and Hot Chocolate applies projections, filtering, and sorting at execution time. +`QueryContext` provides an alternative to the `[UseProjection]` middleware. Instead of applying projections as middleware, you return a `QueryContext` from your resolver and Hot Chocolate applies projections, filtering, and sorting at execution time. ```csharp [QueryType] diff --git a/website/src/docs/hotchocolate/v16/fetching-data/resolvers.md b/website/src/docs/hotchocolate/v16/fetching-data/resolvers.md index 9c1f88db29c..e5d9712318b 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/resolvers.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/resolvers.md @@ -2,319 +2,329 @@ title: "Resolvers" --- -A resolver is a function that produces the value for a field in your GraphQL schema. Every field, whether it maps to a database column, a computed value, or an API call, is backed by a resolver. In Hot Chocolate v16, the source generator is the primary way to define resolvers. You write plain C# methods and the generator handles schema wiring at build time. +In the simplest terms, **a resolver is a generic function that produces a value for a particular field.** -# Properties as Resolvers +You can think of each field in our query as a method of the previous type which returns the next type. -Any public property with a `get` accessor on a type is automatically treated as a resolver that returns its value. +## Resolver Tree -```csharp -public class User -{ - public int Id { get; set; } - public string Name { get; set; } - public string Email { get; set; } +A resolver tree is a projection of a GraphQL operation that is prepared for execution. + +For better understanding, let's imagine we have a simple GraphQL query like the following, where we select some fields of the currently logged-in user. + +```graphql +query { + me { + name + company { + id + name + } + } } ``` -When `User` is exposed in the schema, the `id`, `name`, and `email` fields each resolve by returning the corresponding property value. No additional configuration is needed. +In Hot Chocolate, this query results in the following resolver tree. + +```mermaid +graph LR + A(query: QueryType) --> B(me: UserType) + B --> C(name: StringType) + B --> D(company: CompanyType) + D --> E(id: IdType) + D --> F(name: StringType) +``` + +This tree will be traversed by the execution engine, starting with one or more root resolvers. In the above example the `me` field represents the only root resolver. + +Field resolvers that are sub-selections of a field, can only be executed after a value has been resolved for their _parent_ field. In the case of the above example this means that the `name` and `company` resolvers can only run, after the `me` resolver has finished. Resolvers of field sub-selections can and will be executed in parallel. + +**Because of this it is important that resolvers, with the exception of top level mutation field resolvers, do not contain side-effects, since their execution order may vary.** + +The execution of a request finishes, once each resolver of the selected fields has produced a result. -# Methods as Resolvers +_This is of course an oversimplification that differs from the actual implementation._ -Public methods on your types become resolvers. The source generator strips the `Get` prefix and `Async` suffix when generating the GraphQL field name. +# Defining a Resolver + +Resolvers can be defined in a way that should feel very familiar to C# developers, as they either translate to methods or delegates. +In the implementation-first approach, a public method is automatically inferred as a resolver. This means the method defines both the field in your schema and the logic to resolve its value. + ```csharp [QueryType] -public static partial class BookQueries +public partial class Query { - public static Book GetBook() - => new Book { Title = "C# in depth", Author = "Jon Skeet" }; + public static string Foo() => "Bar"; } ``` -This produces a `book` field on the Query type. +This generates the following schema: - - +```sdl +type Query { + foo: String! +} +``` + +Resolvers do not have to be methods. Public properties are also inferred as resolvers and exposed as fields in your schema. ```csharp -public class BookQueries +[QueryType] +public partial class Query { - public Book GetBook() - => new Book { Title = "C# in depth", Author = "Jon Skeet" }; + public static User User => new User("Ted"); } -public class BookQueriesType : ObjectType +public record User(string Name); +``` + +In this case, the property `Name` of the `User` object is also inferred as a resolver. + + + + +In the code-first approach, you define a resolver by assigning a resolver delegate to a field. This delegate contains the logic for resolving the field's value. + +```csharp +public class QueryType : ObjectType { - protected override void Configure(IObjectTypeDescriptor descriptor) + protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor - .Field(f => f.GetBook()) - .Type(); + .Field("foo") + .Type>() + .Resolve(ctx => "bar"); } } ``` -```csharp -builder - .AddGraphQL() - .AddQueryType(); -``` - -You can also provide a resolver delegate using the `Resolve` method: +You can also use `ObjectType` with a backing POCO. Public methods and properties on the POCO are bound as fields automatically. Use the `Field` method with a lambda expression to configure individual fields. ```csharp -descriptor - .Field("book") - .Resolve(context => +public class Query +{ + public string Foo() => "Bar"; +} + +public class QueryType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) { - return new Book { Title = "C# in depth", Author = "Jon Skeet" }; - }); + descriptor + .Field(f => f.Foo()) + .Type>(); + } +} ``` -# Async Resolvers +## Async Resolver -Most data fetching operations are asynchronous. Mark your resolver methods as `async` or return `Task` and Hot Chocolate handles the rest. +Resolvers can be synchronous or asynchronous. Most data fetching operations, such as calling a service or database, are asynchronous. -Add a `CancellationToken` parameter to your resolver and Hot Chocolate automatically cancels it if the request is aborted. This prevents wasted work when clients disconnect. +The most important aspect of async resolvers is to honor the CancellationToken. This allows execution to be cancelled if the client abandons the request, preventing unnecessary work and resource usage. +When using the implementation-first approach, you can add a `CancellationToken` parameter to your resolver method. The execution engine will automatically inject the request’s cancellation token. + ```csharp -[QueryType] -public static partial class BookQueries +public class Query { - public static async Task GetBookByIdAsync( - int id, - CatalogContext db, - CancellationToken ct) - => await db.Books.FindAsync([id], ct); + public async Task GetProductByIdAsync( + int id, + ProductService productService, + CancellationToken cancellationToken) + => await productService.GetAsync(cancellationToken); } ``` -```csharp -public class BookQueries -{ - public async Task GetBookByIdAsync( - int id, - CatalogContext db, - CancellationToken ct) - => await db.Books.FindAsync([id], ct); -} -``` - -When using a delegate resolver, the `CancellationToken` is passed as the second argument: +When using the code-first approach, you can access the `CancellationToken` through the `IResolverContext` provided to your resolver. ```csharp descriptor - .Field("book") - .Resolve(async (context, ct) => + .Field("foo") + .Resolve(context => { - var db = context.Service(); - var id = context.ArgumentValue("id"); - return await db.Books.FindAsync([id], ct); + CancellationToken ct = context.RequestAborted; + + // Omitted code for brevity }); ``` -# Accessing Parent Values +# Arguments -When you define a resolver on a type, you often need the value that was resolved for the parent field. For example, a `friends` resolver on a `User` type needs the user's ID to look up their friends. +In GraphQL, fields are conceptually similar to methods in C#. Just like methods, fields can have arguments, and you can access these argument values directly in your resolvers. -In the implementation-first approach, you can access parent properties through the `this` keyword when the resolver is defined on the type itself: +When using the implementation-first approach, any parameter in your resolver method that is not a service, a `CancellationToken`, or specially annotated is treated as a GraphQL argument. The execution engine will inject the argument value from the query into these parameters. For example, in the method below, the `id` parameter is recognized as an argument, while `ProductService` is injected as a service from the DI container. ```csharp -public class User +public class Query { - public int Id { get; set; } - public string Name { get; set; } - - public async Task> GetFriendsAsync( - FriendService friendService, - CancellationToken ct) - { - return await friendService.GetFriendsForUserAsync(this.Id, ct); - } -} -``` - -When the resolver is defined elsewhere (such as in a type extension), use the `[Parent]` attribute to inject the parent value: - -```csharp -[ExtendObjectType] -public static partial class UserExtensions -{ - public static async Task> GetFriendsAsync( - [Parent] User user, - FriendService friendService, - CancellationToken ct) - => await friendService.GetFriendsForUserAsync(user.Id, ct); + public async Task GetProductByIdAsync( + int id, + ProductService productService, + CancellationToken cancellationToken) + => await productService.GetAsync(cancellationToken); } ``` -Use the `[Parent]` attribute on a parameter, or access the parent through `IResolverContext`: +When using the code-first approach, you can access field arguments using the resolver context. ```csharp -public class UserType : ObjectType -{ - protected override void Configure(IObjectTypeDescriptor descriptor) +descriptor + .Field("foo") + .Argument("id", a => a.Type>()) + .Resolve(context => { - descriptor - .Field("friends") - .Resolve(async context => - { - var user = context.Parent(); - var friendService = context.Service(); - return await friendService.GetFriendsForUserAsync(user.Id); - }); - } -} + var id = context.ArgumentValue("id"); + + // Omitted code for brevity + }); ``` -# Dependency Injection +[Learn more about arguments](/docs/hotchocolate/v15/defining-a-schema/arguments) -In Hot Chocolate v16, registered services are automatically recognized as service parameters without needing the `[Service]` attribute. If a parameter type is registered in the DI container, Hot Chocolate injects it. +# Injecting Services + +Hot Chocolate automatically recognizes types registered in the DI container and injects them into resolver parameters. ```csharp -[QueryType] -public static partial class BookQueries +public class Query { - public static async Task> GetBooksAsync( - CatalogService catalogService, - CancellationToken ct) - => await catalogService.GetAllBooksAsync(ct); + public List GetUsers(UserService userService) + => userService.GetUsers(); } ``` -```csharp -builder.Services.AddScoped(); +While you can take attributes to annotate services, you do not have to for non-keyed services. -builder - .AddGraphQL() - .AddQueryType(); +```csharp +public class Query +{ + public List GetUsers([Service] UserService userService) + => userService.GetUsers(); +} ``` -Hot Chocolate resolves `CatalogService` from the DI container at execution time. This works for scoped, transient, and singleton services. - [Learn more about dependency injection](/docs/hotchocolate/v16/fetching-data/dependency-injection) -## Accessing the HttpContext +# Accessing parent values -Use `IHttpContextAccessor` when you need access to HTTP-specific details like headers or cookies: +Each field resolver has access to the value that was resolved for its parent type. -```csharp -builder.Services.AddHttpContextAccessor(); +For example, consider the following schema: + +```sdl +type Query { + me: User!; +} + +type User { + id: ID!; + friends: [User!]!; +} ``` +The `User` schema type is represented by a `User` runtime class. The `id` field is a property on this class. + ```csharp -[QueryType] -public static partial class BookQueries +public class User { - public static string GetCorrelationId(IHttpContextAccessor httpContextAccessor) - { - return httpContextAccessor.HttpContext?.Request.Headers["X-Correlation-ID"] - .FirstOrDefault() ?? "unknown"; - } + public string Id { get; set; } } ``` -# Batch Resolvers +The `friends` resolver, by contrast, is independent: it is not declared on the `User` type and uses the user's `Id` to compute its result. +From the `friends` resolver's perspective, the `User` runtime object is its _parent_. -Batch resolvers allow you to resolve a field for multiple parent objects in a single call. This is useful when you need to load related data efficiently without a separate DataLoader class. +Access the parent value like this: -## The [BatchResolver] Attribute + + -Mark a static method with `[BatchResolver]` to define an inline batch resolver. The method receives all parent objects at once and returns results keyed by parent. +In the implementation-first approach, the parent object can be injected as a resolver parameter: ```csharp -[ExtendObjectType] -public static partial class UserExtensions +[ObjectType] +public static partial class UserNode { - [BatchResolver] - public static async Task> GetAddressAsync( - IReadOnlyList users, - AddressService addressService, - CancellationToken ct) + public static Task> GetFriendsAsync( + [Parent] User user, + UserService userService, + CancellationToken cancellationToken) { - var userIds = users.Select(u => u.Id).ToList(); - var addresses = await addressService.GetByUserIdsAsync(userIds, ct); - return addresses.ToDictionary(a => a.UserId); + // Omitted code for brevity } } ``` -## ResolveBatch and ResolverResult - -For more control, use `ResolveBatch()` in code-first to define a batch resolver inline. Use `ResolverResult` when your batch resolver needs to return partial results or errors: +If database projections are enabled, the parent object may only contain the fields requested by the client. To ensure the projections engine also loads properties required by the resolver, declare those requirements on the parent parameter: ```csharp -public class UserType : ObjectType +[ObjectType] +public static partial class UserNode { - protected override void Configure(IObjectTypeDescriptor descriptor) + public static Task> GetFriendsAsync( + [Parent(requires: nameof(User.Id))] User user, + UserService userService, + CancellationToken cancellationToken) { - descriptor - .Field("address") - .ResolveBatch(async (users, ct) => - { - var service = users.First().GetService(); - var ids = users.Select(u => u.Parent().Id).ToList(); - var addresses = await service.GetByUserIdsAsync(ids, ct); - return addresses.ToDictionary(a => a.UserId); - }); + // Omitted code for brevity } } ``` -# Conditional Data Fetching with [IsSelected] +Use `nameof` to make this requirement refactoring-safe. -The `[IsSelected]` attribute lets you check whether a particular field was requested in the query. Use this to skip expensive data loading when the client does not need certain fields. + + + +In the code-first approach, the parent object is available via the `IResolverContext`. ```csharp -[QueryType] -public static partial class UserQueries +public class User { - public static async Task GetUserByIdAsync( - int id, - [IsSelected("address")] bool includeAddress, - UserService userService, - CancellationToken ct) + public string Id { get; set; } +} + +public class UserType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) { - if (includeAddress) - { - return await userService.GetUserWithAddressAsync(id, ct); - } + descriptor + .Field("friends") + .Resolve(context => + { + User parent = context.Parent(); - return await userService.GetUserAsync(id, ct); + // Omitted code for brevity + }); } } ``` -When the client query includes the `address` field, `includeAddress` is `true` and the resolver loads the address eagerly. Otherwise, it skips the additional work. - -# Next Steps - -- **Need to batch data access?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader). -- **Need to page through results?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination). -- **Need to filter or sort?** See [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [Sorting](/docs/hotchocolate/v16/fetching-data/sorting). -- **Need to understand type extensions?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/extending-types). + + diff --git a/website/src/docs/hotchocolate/v16/fetching-data/sorting.md b/website/src/docs/hotchocolate/v16/fetching-data/sorting.md index c5e01800058..3bfa3c37f6e 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/sorting.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/sorting.md @@ -100,7 +100,7 @@ query { # NullOrdering Enum -In v16, the `NullOrdering` enum controls how `null` values sort relative to non-null values. This is relevant when sorting on nullable fields. Set this through `PagingOptions` at the global level: +The `NullOrdering` enum controls how `null` values sort relative to non-null values. This is relevant when sorting on nullable fields. Set this through `PagingOptions` at the global level: ```csharp builder diff --git a/website/src/docs/hotchocolate/v16/fetching-data/spatial-data.md b/website/src/docs/hotchocolate/v16/fetching-data/spatial-data.md index b35c3ddc86e..6872a129f78 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/spatial-data.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/spatial-data.md @@ -1,6 +1,6 @@ --- title: Spatial Data -description: Learn how to expose NetTopologySuite spatial types as GeoJSON in Hot Chocolate v16. +description: Learn how to expose NetTopologySuite spatial types as GeoJSON in Hot Chocolate. --- > Experimental: This feature is community-driven and not yet finalized. The core team has limited experience with spatial data and welcomes your feedback to guide next steps. While we try not to introduce breaking changes, we reserve the possibility to adjust the API in future releases. diff --git a/website/src/docs/hotchocolate/v16/fusion/index.md b/website/src/docs/hotchocolate/v16/fusion/index.md deleted file mode 100644 index 1590b8e6223..00000000000 --- a/website/src/docs/hotchocolate/v16/fusion/index.md +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: "Fusion" ---- diff --git a/website/src/docs/hotchocolate/v16/guides/federation.md b/website/src/docs/hotchocolate/v16/guides/federation.md index e01b61a8fde..d137b5400b2 100644 --- a/website/src/docs/hotchocolate/v16/guides/federation.md +++ b/website/src/docs/hotchocolate/v16/guides/federation.md @@ -1,6 +1,6 @@ --- title: Apollo Federation Subgraph Support -description: Learn how to create Apollo Federated subgraphs using Hot Chocolate v16. +description: Learn how to create Apollo Federated subgraphs using Hot Chocolate. --- > For more about Apollo Federation concepts, see the [Apollo Federation documentation](https://www.apollographql.com/docs/federation/). Many of the core principles referenced here are documented there. diff --git a/website/src/docs/hotchocolate/v16/guides/performance.md b/website/src/docs/hotchocolate/v16/guides/performance.md index 7af4205888d..6c295bd760d 100644 --- a/website/src/docs/hotchocolate/v16/guides/performance.md +++ b/website/src/docs/hotchocolate/v16/guides/performance.md @@ -44,7 +44,7 @@ builder }); ``` -In v16, each cache is scoped to a single schema instance. If your application hosts multiple schemas, each schema maintains its own caches. +Each cache is scoped to a single schema instance. If your application hosts multiple schemas, each schema maintains its own caches. For APIs with a known set of operations, consider using [persisted operations](/docs/hotchocolate/v16/performance/trusted-documents) to eliminate parsing and validation entirely. @@ -156,7 +156,7 @@ builder }); ``` -In v16, the default incremental delivery wire format is v0.2, which uses `pending`, `incremental`, and `completed` fields to track deferred fragments. +The default incremental delivery wire format is v0.2, which uses `pending`, `incremental`, and `completed` fields to track deferred fragments. [Learn more about incremental delivery](/docs/hotchocolate/v16/server/http-transport) diff --git a/website/src/docs/hotchocolate/v16/guides/public-api.md b/website/src/docs/hotchocolate/v16/guides/public-api.md index 382ea6069f2..5ffd486228a 100644 --- a/website/src/docs/hotchocolate/v16/guides/public-api.md +++ b/website/src/docs/hotchocolate/v16/guides/public-api.md @@ -280,7 +280,7 @@ Rate limiting and cost analysis complement each other. Rate limiting caps the nu Request batching allows a client to send multiple GraphQL operations in a single HTTP request. For internal APIs where you trust the client, this can improve performance. For public APIs, batching lets a client bypass your per-request rate limits by packing many expensive operations into one request. -In Hot Chocolate v16, request batching is disabled by default. If you have explicitly enabled it, disable it for your public API: +In Hot Chocolate, request batching is disabled by default. If you have explicitly enabled it, disable it for your public API: ```csharp builder diff --git a/website/src/docs/hotchocolate/v16/fetching-data/entity-framework.md b/website/src/docs/hotchocolate/v16/integrations/entity-framework.md similarity index 99% rename from website/src/docs/hotchocolate/v16/fetching-data/entity-framework.md rename to website/src/docs/hotchocolate/v16/integrations/entity-framework.md index dc5540577c5..b1c0bac2fda 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/entity-framework.md +++ b/website/src/docs/hotchocolate/v16/integrations/entity-framework.md @@ -1,6 +1,6 @@ --- title: Entity Framework Core -description: Learn how to integrate Entity Framework Core with Hot Chocolate v16, including DbContext injection and factory patterns. +description: Learn how to integrate Entity Framework Core with Hot Chocolate, including DbContext injection and factory patterns. --- [Entity Framework Core](https://docs.microsoft.com/ef/core/) is a powerful object-relational mapping framework that has become a staple when working with SQL-based databases in .NET applications. diff --git a/website/src/docs/hotchocolate/v16/fetching-data/extending-filtering.md b/website/src/docs/hotchocolate/v16/integrations/extending-filtering.md similarity index 99% rename from website/src/docs/hotchocolate/v16/fetching-data/extending-filtering.md rename to website/src/docs/hotchocolate/v16/integrations/extending-filtering.md index 33ee2215950..1d4235a0b98 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/extending-filtering.md +++ b/website/src/docs/hotchocolate/v16/integrations/extending-filtering.md @@ -1,6 +1,6 @@ --- title: Extending Filtering -description: Learn how to extend the filtering system in Hot Chocolate v16 with custom conventions, providers, and field handlers. +description: Learn how to extend the filtering system in Hot Chocolate with custom conventions, providers, and field handlers. --- The `HotChocolate.Data` package works with all databases that support `IQueryable`. The default settings include all filter operations that work over `IQueryable` on all databases. In some cases, this is not enough. Some databases might not support `IQueryable`. Others may have technology-specific operations (e.g., SQL `LIKE`). The filtering system is designed with extensibility in mind. diff --git a/website/src/docs/hotchocolate/v16/fetching-data/executable.md b/website/src/docs/hotchocolate/v16/integrations/index.md similarity index 98% rename from website/src/docs/hotchocolate/v16/fetching-data/executable.md rename to website/src/docs/hotchocolate/v16/integrations/index.md index aa61f615f10..277b3d704f0 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/executable.md +++ b/website/src/docs/hotchocolate/v16/integrations/index.md @@ -1,6 +1,6 @@ --- title: Executable -description: Learn how to use the IExecutable interface to abstract data sources in Hot Chocolate v16. +description: Learn how to use the IExecutable interface to abstract data sources in Hot Chocolate. --- The `IExecutable` and `IExecutable` interfaces abstract data sources in Hot Chocolate. Your data or domain layer can wrap a data source in an executable and pass it to the GraphQL layer. A resolver that returns `IExecutable` is recognized as a list. diff --git a/website/src/docs/hotchocolate/v16/fetching-data/marten.md b/website/src/docs/hotchocolate/v16/integrations/marten.md similarity index 93% rename from website/src/docs/hotchocolate/v16/fetching-data/marten.md rename to website/src/docs/hotchocolate/v16/integrations/marten.md index 59f6fc346b7..aa58fd52498 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/marten.md +++ b/website/src/docs/hotchocolate/v16/integrations/marten.md @@ -1,6 +1,6 @@ --- title: Marten -description: Learn how to integrate Marten with Hot Chocolate v16 for filtering, sorting, projections, and pagination. +description: Learn how to integrate Marten with Hot Chocolate for filtering, sorting, projections, and pagination. --- The `HotChocolate.Data` package generally works with any LINQ provider that provides an `IQueryable`. However, Marten requires special handling. Pagination and projections work out of the box, but filtering and sorting need LINQ expressions translated into a format that the Marten LINQ provider can process. This integration provides custom configurations for that purpose. diff --git a/website/src/docs/hotchocolate/v16/fetching-data/mongodb.md b/website/src/docs/hotchocolate/v16/integrations/mongodb.md similarity index 97% rename from website/src/docs/hotchocolate/v16/fetching-data/mongodb.md rename to website/src/docs/hotchocolate/v16/integrations/mongodb.md index 18f7702e302..67542fd6a0b 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/mongodb.md +++ b/website/src/docs/hotchocolate/v16/integrations/mongodb.md @@ -1,6 +1,6 @@ --- title: MongoDB -description: Learn how to integrate MongoDB with Hot Chocolate v16, including filtering, sorting, projections, and pagination. +description: Learn how to integrate MongoDB with Hot Chocolate, including filtering, sorting, projections, and pagination. --- Hot Chocolate has a data integration for MongoDB. With this integration, you can translate paging, filtering, sorting, and projections directly into native MongoDB queries. diff --git a/website/src/docs/hotchocolate/v16/server/command-line.md b/website/src/docs/hotchocolate/v16/server/command-line.md index 649a292e592..43cc0e38a62 100644 --- a/website/src/docs/hotchocolate/v16/server/command-line.md +++ b/website/src/docs/hotchocolate/v16/server/command-line.md @@ -20,7 +20,7 @@ app.MapGraphQL(); return await app.RunWithGraphQLCommandsAsync(args); ``` -In v16, `RunWithGraphQLCommandsAsync` returns a `Task` (and the synchronous `RunWithGraphQLCommands` returns `int`). Return this exit code from your `Program.cs` so that command failures signal an error to shell scripts, CI/CD pipelines, and other tools. +`RunWithGraphQLCommandsAsync` returns a `Task` (and the synchronous `RunWithGraphQLCommands` returns `int`). Return this exit code from your `Program.cs` so that command failures signal an error to shell scripts, CI/CD pipelines, and other tools. # Commands @@ -36,7 +36,7 @@ dotnet run -- schema export --output schema.graphql - `--output`: The path to the file where the schema is exported. If no output path is specified, the schema prints to the console. - `--schema-name`: The name of the schema to export. If no schema name is specified, the default schema is exported. -- `--semantic-non-null`: Rewrites the exported schema to strip non-null wrappers from output fields and apply the `@semanticNonNull` directive instead. Useful for clients that still rely on `@semanticNonNull` annotations after the experimental support was removed in v16. +- `--semantic-non-null`: Rewrites the exported schema to strip non-null wrappers from output fields and apply the `@semanticNonNull` directive instead. Useful for clients that still rely on `@semanticNonNull` annotations. # Next Steps diff --git a/website/src/docs/hotchocolate/v16/server/files.md b/website/src/docs/hotchocolate/v16/server/files.md index ce73b481b82..3bcdf31ba21 100644 --- a/website/src/docs/hotchocolate/v16/server/files.md +++ b/website/src/docs/hotchocolate/v16/server/files.md @@ -144,7 +144,7 @@ If you need to upload a list of files, use a `List` or `ListType`: +The `ActivityEnricher` constructor does not require an `ObjectPool`: ```csharp public class CustomActivityEnricher : ActivityEnricher diff --git a/website/src/docs/hotchocolate/v16/server/options.md b/website/src/docs/hotchocolate/v16/server/options.md index 3eca389399b..cf31c90d96a 100644 --- a/website/src/docs/hotchocolate/v16/server/options.md +++ b/website/src/docs/hotchocolate/v16/server/options.md @@ -1,6 +1,6 @@ --- title: Options Reference -description: Comprehensive reference for all configuration options in Hot Chocolate v16, including schema, request, parser, cost, server, socket, subscription, paging, global object identification, and cache control options. +description: Comprehensive reference for all configuration options in Hot Chocolate, including schema, request, parser, cost, server, socket, subscription, paging, global object identification, and cache control options. --- Hot Chocolate provides several option groups that control different aspects of the GraphQL server. You configure them through methods on the `IRequestExecutorBuilder`. @@ -92,7 +92,7 @@ Refer to the cost analysis documentation for the full list of configurable prope # Server Options (ModifyServerOptions) -Server options control HTTP-level behavior such as GET requests, batching, multipart requests, and schema retrieval. This is new in v16. Configure with `ModifyServerOptions`: +Server options control HTTP-level behavior such as GET requests, batching, multipart requests, and schema retrieval. Configure with `ModifyServerOptions`: ```csharp builder @@ -264,7 +264,7 @@ builder # Next Steps -- [Execution engine](/docs/hotchocolate/v16/execution-engine) for pipeline configuration +- [Field middleware](/docs/hotchocolate/v16/fetching-data/field-middleware) for pipeline configuration - [Cache Control](/docs/hotchocolate/v16/server/cache-control) for CDN and HTTP caching behavior - [Pagination](/docs/hotchocolate/v16/fetching-data/pagination) for paging setup - [Persisted operations](/docs/hotchocolate/v16/performance/trusted-documents) for operation caching From bc7ef8f9aab99330a974605dafe8245a24ebe557 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 12 May 2026 17:25:09 -0400 Subject: [PATCH 2/6] edits --- .../v16/defining-a-schema/dynamic-schemas.md | 2 +- .../v16/defining-a-schema/index.md | 4 +- .../v16/defining-a-schema/interfaces.md | 2 +- .../v16/defining-a-schema/mutations.md | 2 +- .../v16/defining-a-schema/object-types.md | 4 +- .../v16/defining-a-schema/queries.md | 2 +- .../v16/defining-a-schema/relay.md | 6 +- .../v16/defining-a-schema/versioning.md | 2 +- .../v16/fetching-data/batching/index.md | 7 +- .../v16/fetching-data/dependency-injection.md | 127 +++++------------- .../hotchocolate/v16/fetching-data/index.md | 7 +- .../integrations/entity-framework.md | 2 +- .../integrations/extending-filtering.md | 5 +- .../{ => fetching-data}/integrations/index.md | 4 +- .../integrations/marten.md | 0 .../integrations/mongodb.md | 2 +- .../v16/fetching-data/projections.md | 2 +- .../v16/fetching-data/resolvers.md | 2 +- .../v16/fetching-data/spatial-data.md | 2 +- .../get-started-with-graphql-in-net-core.md | 2 +- .../v16/guides/dynamic-schemas.md | 2 +- .../hotchocolate/v16/guides/error-handling.md | 4 +- .../hotchocolate/v16/guides/federation.md | 4 +- .../hotchocolate/v16/guides/performance.md | 4 +- .../docs/hotchocolate/v16/guides/testing.md | 2 +- website/src/docs/hotchocolate/v16/index.md | 2 +- .../v16/security/cost-analysis.md | 2 +- .../docs/hotchocolate/v16/server/endpoints.md | 4 +- 28 files changed, 71 insertions(+), 139 deletions(-) rename website/src/docs/hotchocolate/v16/{ => fetching-data}/integrations/entity-framework.md (98%) rename website/src/docs/hotchocolate/v16/{ => fetching-data}/integrations/extending-filtering.md (97%) rename website/src/docs/hotchocolate/v16/{ => fetching-data}/integrations/index.md (95%) rename website/src/docs/hotchocolate/v16/{ => fetching-data}/integrations/marten.md (100%) rename website/src/docs/hotchocolate/v16/{ => fetching-data}/integrations/mongodb.md (98%) diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/dynamic-schemas.md b/website/src/docs/hotchocolate/v16/defining-a-schema/dynamic-schemas.md index 0788935bfdd..b73744eb08b 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/dynamic-schemas.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/dynamic-schemas.md @@ -219,6 +219,6 @@ builder # Next Steps -- **Need to extend existing types?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/extending-types). +- **Need to extend existing types?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/object-types). - **Need to define types with the descriptor API?** See [Object Types](/docs/hotchocolate/v16/defining-a-schema/object-types). - **Need to understand type modules in depth?** Explore the `ITypeModule` interface in the Hot Chocolate source code under `src/HotChocolate/Core/src/Types/`. diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/index.md b/website/src/docs/hotchocolate/v16/defining-a-schema/index.md index ca3a9b8790f..1fb35186449 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/index.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/index.md @@ -177,7 +177,7 @@ Organize your schema for maintainability and support advanced modeling scenarios | Topic | Use it for | Learn more | | --------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------ | -| Type extensions | Split large object or root type definitions across classes. Extensions are merged into the final schema. | [Extending Types](./extending-types) | +| Type extensions | Split large object or root type definitions across classes. Extensions are merged into the final schema. | [Object Types](./object-types) | | Relay helpers | Use stable IDs, global object identification, `node`, `nodes`, `[ID]`, `[Node]`, and `[NodeResolver]`. | [Relay](./relay) | | Dynamic schemas | Generate types from CMS, multi-tenant, or configuration-driven metadata with `ITypeModule`. | [Dynamic Schemas](./dynamic-schemas) | @@ -199,4 +199,4 @@ Use type extensions for static modularity. Choose dynamic schemas only when the - Learn how returned shapes are inferred and configured in [Object Types](./object-types). - Add field inputs with [Arguments](./arguments) and [Input Object Types](./input-object-types). - Strengthen the contract with [Lists and Non-Null](./lists). -- Move to runtime behavior with [Resolvers](../fetching-data/resolvers) and [DataLoader](../fetching-data/dataloader) after the schema shape is clear. +- Move to runtime behavior with [Resolvers](../fetching-data/resolvers) and [DataLoader](../fetching-data/batching/dataloader) after the schema shape is clear. diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/interfaces.md b/website/src/docs/hotchocolate/v16/defining-a-schema/interfaces.md index a9cc5fc3f72..ddbf833f2e0 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/interfaces.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/interfaces.md @@ -366,5 +366,5 @@ Object types can override an inherited resolver by defining their own resolver f - **Need types without shared fields?** See [Unions](/docs/hotchocolate/v16/defining-a-schema/unions). - **Need to define output types?** See [Object Types](/docs/hotchocolate/v16/defining-a-schema/object-types). -- **Need to extend an existing type?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/extending-types). +- **Need to extend an existing type?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/object-types). - **Need to document interface fields?** See [Documentation](/docs/hotchocolate/v16/defining-a-schema/documentation). diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/mutations.md b/website/src/docs/hotchocolate/v16/defining-a-schema/mutations.md index 3d55fd365a8..4eceafe6a97 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/mutations.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/mutations.md @@ -393,4 +393,4 @@ builder - **Need to read data?** See [Queries](/docs/hotchocolate/v16/defining-a-schema/queries). - **Need real-time updates?** See [Subscriptions](/docs/hotchocolate/v16/defining-a-schema/subscriptions). - **Need to understand input types?** See [Input Object Types](/docs/hotchocolate/v16/defining-a-schema/input-object-types). -- **Need to fetch data efficiently?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader). +- **Need to fetch data efficiently?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader). diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/object-types.md b/website/src/docs/hotchocolate/v16/defining-a-schema/object-types.md index 741826766e8..4001fd08d8e 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/object-types.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/object-types.md @@ -643,6 +643,6 @@ This works with any key and value types. For example, `Dictionary` - **Need to define query entry points?** See [Queries](/docs/hotchocolate/v16/defining-a-schema/queries). - **Need to understand resolver patterns?** See [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). -- **Need to compose types from multiple classes?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/extending-types). +- **Need to compose types from multiple classes?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/object-types). - **Need to define input for mutations?** See [Input Object Types](/docs/hotchocolate/v16/defining-a-schema/input-object-types). -- **Need to fetch data efficiently?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader). +- **Need to fetch data efficiently?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader). diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/queries.md b/website/src/docs/hotchocolate/v16/defining-a-schema/queries.md index 4707a8aa564..38bac9a8909 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/queries.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/queries.md @@ -165,4 +165,4 @@ public static List GetBooks() => /* ... */; - **Need to write data?** See [Mutations](/docs/hotchocolate/v16/defining-a-schema/mutations). - **Need real-time updates?** See [Subscriptions](/docs/hotchocolate/v16/defining-a-schema/subscriptions). - **Need to understand how types map to the schema?** See [Object Types](/docs/hotchocolate/v16/defining-a-schema/object-types). -- **Need to fetch data efficiently?** See [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) and [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader). +- **Need to fetch data efficiently?** See [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) and [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader). diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/relay.md b/website/src/docs/hotchocolate/v16/defining-a-schema/relay.md index 32a3cb8c9ef..d816764835f 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/relay.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/relay.md @@ -288,7 +288,7 @@ descriptor -Node resolvers are ideal places to use [DataLoaders](/docs/hotchocolate/v16/fetching-data/dataloader) for efficient batched fetching. +Node resolvers are ideal places to use [DataLoaders](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for efficient batched fetching. ## Node with Type Extensions @@ -365,7 +365,7 @@ builder # Next Steps -- **Need to fetch data efficiently?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader). +- **Need to fetch data efficiently?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader). - **Need pagination?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination). - **Need to understand ID types?** See [Scalars](/docs/hotchocolate/v16/defining-a-schema/scalars). -- **Need to extend types?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/extending-types). +- **Need to extend types?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/object-types). diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/versioning.md b/website/src/docs/hotchocolate/v16/defining-a-schema/versioning.md index 3bb212c6ef2..b8d0bb6c53b 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/versioning.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/versioning.md @@ -202,4 +202,4 @@ Consumers query feature stability through introspection: - **Need to add descriptions?** See [Documentation](/docs/hotchocolate/v16/defining-a-schema/documentation). - **Need to create custom directives?** See [Directives](/docs/hotchocolate/v16/defining-a-schema/directives). -- **Need to understand schema evolution?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/extending-types). +- **Need to understand schema evolution?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/object-types). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md b/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md index a7d4e51fef3..3d8bde04c4d 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md @@ -45,7 +45,7 @@ Resolvers are the building blocks of data fetching. A resolver can call a databa DataLoaders deduplicate and batch requests to data sources. When multiple resolvers request the same entity in a single request, a DataLoader ensures only one call goes to the backing store. DataLoaders can significantly reduce the load on your databases and services. -[Learn more about DataLoaders](/docs/hotchocolate/v16/fetching-data/dataloader) +[Learn more about DataLoaders](/docs/hotchocolate/v16/fetching-data/batching/dataloader) # Pagination @@ -75,12 +75,9 @@ Projections optimize database queries by selecting only the columns that match t Hot Chocolate is not bound to a specific database or architecture. You can fetch data from any source in your resolvers. We provide specific guidance for the most common patterns: -- [Fetching from databases](/docs/hotchocolate/v16/fetching-data/fetching-from-databases) -- [Fetching from REST APIs](/docs/hotchocolate/v16/fetching-data/fetching-from-rest) - # Next Steps - **New to resolvers?** Start with [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). -- **Need to batch data access?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader). +- **Need to batch data access?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader). - **Need to page through lists?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination). - **Need to filter or sort?** See [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [Sorting](/docs/hotchocolate/v16/fetching-data/sorting). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md b/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md index 84fc85f458f..b9b41495e6b 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md @@ -2,12 +2,11 @@ title: Dependency Injection --- -If you are unfamiliar with dependency injection, the following articles provide a good starting point: +If you're unfamiliar with dependency injection, the ASP.NET Core documentation is a good starting point: -- [Dependency injection in .NET](https://docs.microsoft.com/dotnet/core/extensions/dependency-injection) -- [Dependency injection in ASP.NET Core](https://docs.microsoft.com/aspnet/core/fundamentals/dependency-injection) +- [Dependency injection in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/dependency-injection) -Dependency injection with Hot Chocolate works almost the same as with a regular ASP.NET Core application. Nothing changes about how you add services to the DI container. +Dependency injection in Hot Chocolate works the same as in a regular ASP.NET Core application: register services with the DI container as usual. ```csharp var builder = WebApplication.CreateBuilder(args); @@ -18,22 +17,26 @@ builder.Services .AddTransient(); ``` -# Implicit Service Injection - Hot Chocolate automatically recognizes types registered as services in the DI container and injects them into resolver method parameters without requiring any attribute. This works similarly to [Minimal APIs parameter binding](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding). When the execution engine encounters a resolver parameter whose type is registered in the DI container, it resolves the service automatically. You do not need to apply the `[Service]` attribute. -# Resolver Injection - -Inject dependencies into your resolvers as method arguments. This is the recommended approach. +```csharp +public static async Task GetBookByIdAsync( + Guid id, + BookService bookService) +{ + return await bookService.GetBookAsync(id); +} +``` -[Learn more about why constructor injection into GraphQL types is a bad idea](#constructor-injection) +# Resolver Injection -Injecting dependencies at the method level has several benefits: +Injecting services at the method level has several benefits: - The execution engine can optimize the resolver and adjust the execution strategy based on the needs of a specific service. - Refactoring (moving the resolver method between classes) becomes easier because the resolver does not depend on its outer class. +- If many resolvers are colocated in a single class we only need to resolve the services that are needed for resolvers that are actually being executed In the following example, `BookService` is injected automatically when it is registered as a service in the DI container: @@ -42,7 +45,7 @@ In the following example, `BookService` is injected automatically when it is reg ```csharp [QueryType] -public static class Query +public static partial class Query { public static async Task GetBookByIdAsync( Guid id, @@ -75,18 +78,15 @@ public sealed class QueryType : ObjectType ``` - - -Take a look at the implementation-first or code-first example. - - -## Default Scope +## Service Scoping + +In GraphQL, resolvers are expected to be side-effect free. The execution engine may run them in parallel or out of order, so relying on shared mutable state within services can lead to issues. For example, if multiple resolvers use the same Entity Framework DbContext concurrently, it can cause thread-safety problems and execution errors. -By default, scoped services are scoped to the resolver for queries and DataLoaders, and to the current request for mutations. This means that each execution of a query or DataLoader that accepts a scoped service receives a **separate** instance, avoiding threading issues with services that do not support multi-threading (for example, Entity Framework DbContexts). Since mutations are executed sequentially, they receive the **same** request-scoped instance. +To avoid this, Hot Chocolate creates a new service scope for each async resolver and each DataLoader dispatch. This ensures every resolver or DataLoader execution receives its own service instance. -You can change these defaults globally: +If you want to change the default scoping behavior, you can update the GraphQL options. ```csharp builder @@ -136,68 +136,32 @@ public sealed class QueryType : ObjectType ``` - - -Take a look at the implementation-first or code-first example. - - -# Application Services in Schema Services - -Hot Chocolate maintains a separate internal service provider for schema services. If you need application services to be available within schema-level components (such as diagnostic event listeners, error filters, or interceptors), you must cross-register them using `AddApplicationService()`: - -```csharp -builder.Services.AddSingleton(); - -builder - .AddGraphQL() - .AddApplicationService() - .AddDiagnosticEventListener(); -``` - -Services registered via `AddApplicationService()` are resolved once during schema initialization from the application service provider and registered as singletons in the schema service provider. - -The following configuration APIs require `AddApplicationService()` for any application services they depend on: - -- `AddHttpRequestInterceptor` -- `AddSocketSessionInterceptor` -- `AddErrorFilter` -- `AddDiagnosticEventListener` -- `AddOperationCompilerOptimizer` -- `AddTransactionScopeHandler` -- `AddRedisOperationDocumentStorage` -- `AddAzureBlobStorageOperationDocumentStorage` -- `AddInstrumentation` with a custom `ActivityEnricher` - -> Note: Service injection into resolvers is not affected by this. Resolvers continue to use the application service provider directly. - # Constructor Injection -When starting out with Hot Chocolate you might be inclined to inject dependencies into your GraphQL type definitions using the constructor. - -You should avoid doing this, because: +If you're coming from an ASP.NET core controller-based workflow, you might be used to injecting services into your types via the constructor. In Hot Chocolate, you should avoid injecting services into GraphQL type definitions for these reasons: -- GraphQL type definitions are singletons and your injected dependency will also become a singleton. -- Access to this dependency cannot be synchronized by Hot Chocolate during request execution. +- GraphQL type definitions are registered as singletons, so constructor-injected services would also become singletons. +- Hot Chocolate cannot synchronize access to those services during request execution, which can cause issues with services that are not thread-safe (such as EF Core's DbContext). -This does not apply within your own dependencies. Your `ServiceA` class can still inject `ServiceB` through the constructor. - -When you need to access dependency injection services in your resolvers, use the [method-level dependency injection approach](#resolver-injection) described above. +> _Note:_ This guidance does not apply to your own application services. For example, `ServiceA` can still inject `ServiceB` via constructor injection. # Keyed Services -A keyed service registered like this: +A keyed service is a service registered in the DI container with an associated key or name. This allows you to register multiple instances of the same service type, each identified by a unique key. Keyed services are useful when you need different configurations or implementations of the same service type within your application. + +You can register a keyed service like this: ```csharp builder.Services.AddKeyedScoped("bookService"); ``` -...can be accessed in your resolver with the `[Service]` attribute specifying the key: - +You can then access the keyed service in your resolver by applying the `[Service]` attribute with the key: + ```csharp [QueryType] public static class Query @@ -214,6 +178,8 @@ public static class Query +You can also access the keyed service in your resolver by passing the key to the `Service` helper on the resolver context. + ```csharp public sealed class QueryType : ObjectType { @@ -233,40 +199,13 @@ public sealed class QueryType : ObjectType ``` - - -Take a look at the implementation-first or code-first example. - - -# Accessing the HttpContext - -The [IHttpContextAccessor](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) allows you to access the [HttpContext](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext) of the current request from within your resolvers. This is useful when you need to read or set a header or cookie. - -First register the `IHttpContextAccessor` as a service. - -```csharp -builder.Services.AddHttpContextAccessor(); -``` - -Then inject it into your resolver. - -```csharp -public string Foo(string id, IHttpContextAccessor httpContextAccessor) -{ - if (httpContextAccessor.HttpContext is not null) - { - // Omitted code for brevity - } -} -``` - # Switching the Service Provider -While Hot Chocolate's internals rely on Microsoft's dependency injection container, you are not required to manage your own dependencies using this container. By default, Hot Chocolate uses the request-scoped [`HttpContext.RequestServices`](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestservices) `IServiceProvider` to provide services to your resolvers. +While Hot Chocolate's internals rely on Microsoft's dependency injection container, you are not required to manage your own dependencies using Microsoft`s container. By default, Hot Chocolate uses the request-scoped [`HttpContext.RequestServices`](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestservices) `IServiceProvider` to provide services to your resolvers. -You can switch out the service provider used for GraphQL requests, as long as your DI container implements the [`IServiceProvider`](https://docs.microsoft.com/dotnet/api/system.iserviceprovider) interface. +You can switch out the service provider used for GraphQL requests, as long as your DI container implements the [`IServiceProvider`](https://docs.microsoft.com/dotnet/api/system.iserviceprovider) interface and supports scoping. To switch the service provider, call [`SetServices`](/docs/hotchocolate/v16/server/interceptors#setservices) on the [`OperationRequestBuilder`](/docs/hotchocolate/v16/server/interceptors#operationrequestbuilder) in both the [`IHttpRequestInterceptor`](/docs/hotchocolate/v16/server/interceptors#ihttprequestinterceptor) and the [`ISocketSessionInterceptor`](/docs/hotchocolate/v16/server/interceptors#isocketsessioninterceptor). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/index.md b/website/src/docs/hotchocolate/v16/fetching-data/index.md index a7d4e51fef3..3d8bde04c4d 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/index.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/index.md @@ -45,7 +45,7 @@ Resolvers are the building blocks of data fetching. A resolver can call a databa DataLoaders deduplicate and batch requests to data sources. When multiple resolvers request the same entity in a single request, a DataLoader ensures only one call goes to the backing store. DataLoaders can significantly reduce the load on your databases and services. -[Learn more about DataLoaders](/docs/hotchocolate/v16/fetching-data/dataloader) +[Learn more about DataLoaders](/docs/hotchocolate/v16/fetching-data/batching/dataloader) # Pagination @@ -75,12 +75,9 @@ Projections optimize database queries by selecting only the columns that match t Hot Chocolate is not bound to a specific database or architecture. You can fetch data from any source in your resolvers. We provide specific guidance for the most common patterns: -- [Fetching from databases](/docs/hotchocolate/v16/fetching-data/fetching-from-databases) -- [Fetching from REST APIs](/docs/hotchocolate/v16/fetching-data/fetching-from-rest) - # Next Steps - **New to resolvers?** Start with [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). -- **Need to batch data access?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader). +- **Need to batch data access?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader). - **Need to page through lists?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination). - **Need to filter or sort?** See [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [Sorting](/docs/hotchocolate/v16/fetching-data/sorting). diff --git a/website/src/docs/hotchocolate/v16/integrations/entity-framework.md b/website/src/docs/hotchocolate/v16/fetching-data/integrations/entity-framework.md similarity index 98% rename from website/src/docs/hotchocolate/v16/integrations/entity-framework.md rename to website/src/docs/hotchocolate/v16/fetching-data/integrations/entity-framework.md index b1c0bac2fda..1e4741bb417 100644 --- a/website/src/docs/hotchocolate/v16/integrations/entity-framework.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/integrations/entity-framework.md @@ -228,5 +228,5 @@ Take a look at the annotation-based or code-first example. # Next Steps - [Dependency Injection](/docs/hotchocolate/v16/fetching-data/dependency-injection) for DI scope configuration -- [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader) for batching patterns +- [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for batching patterns - [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) for applying filters to EF Core queries diff --git a/website/src/docs/hotchocolate/v16/integrations/extending-filtering.md b/website/src/docs/hotchocolate/v16/fetching-data/integrations/extending-filtering.md similarity index 97% rename from website/src/docs/hotchocolate/v16/integrations/extending-filtering.md rename to website/src/docs/hotchocolate/v16/fetching-data/integrations/extending-filtering.md index 1d4235a0b98..b789342219b 100644 --- a/website/src/docs/hotchocolate/v16/integrations/extending-filtering.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/integrations/extending-filtering.md @@ -132,7 +132,7 @@ Like the convention, a provider is configured through a fluent interface. Every The provider translates an incoming query into a database query by traversing an input object and executing the handlers on the fields. The output is always some kind of _filter definition_. For `IQueryable`, this is an expression. For MongoDB, this is a `FilterDefinition`. -To inspect and analyze the input object, the provider uses a visitor. See [Visitors](/docs/hotchocolate/v16/api-reference/visitors) for details on how visitors work. +To inspect and analyze the input object, the provider uses a visitor. ## Provider Descriptor @@ -288,5 +288,4 @@ builder # Next Steps - [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) for using built-in filtering -- [Visitors](/docs/hotchocolate/v16/api-reference/visitors) for understanding the visitor pattern -- [MongoDB integration](/docs/hotchocolate/v16/fetching-data/mongodb) for MongoDB-specific filtering +- [MongoDB integration](/docs/hotchocolate/v16/fetching-data/integrations/mongodb) for MongoDB-specific filtering diff --git a/website/src/docs/hotchocolate/v16/integrations/index.md b/website/src/docs/hotchocolate/v16/fetching-data/integrations/index.md similarity index 95% rename from website/src/docs/hotchocolate/v16/integrations/index.md rename to website/src/docs/hotchocolate/v16/fetching-data/integrations/index.md index 277b3d704f0..9a00ce88887 100644 --- a/website/src/docs/hotchocolate/v16/integrations/index.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/integrations/index.md @@ -115,6 +115,6 @@ public class EntityFrameworkExecutable : QueryableExecutable # Next Steps -- [Entity Framework integration](/docs/hotchocolate/v16/fetching-data/entity-framework) for EF Core executables -- [MongoDB integration](/docs/hotchocolate/v16/fetching-data/mongodb) for MongoDB executables +- [Entity Framework integration](/docs/hotchocolate/v16/fetching-data/integrations/entity-framework) for EF Core executables +- [MongoDB integration](/docs/hotchocolate/v16/fetching-data/integrations/mongodb) for MongoDB executables - [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) for applying filters to executables diff --git a/website/src/docs/hotchocolate/v16/integrations/marten.md b/website/src/docs/hotchocolate/v16/fetching-data/integrations/marten.md similarity index 100% rename from website/src/docs/hotchocolate/v16/integrations/marten.md rename to website/src/docs/hotchocolate/v16/fetching-data/integrations/marten.md diff --git a/website/src/docs/hotchocolate/v16/integrations/mongodb.md b/website/src/docs/hotchocolate/v16/fetching-data/integrations/mongodb.md similarity index 98% rename from website/src/docs/hotchocolate/v16/integrations/mongodb.md rename to website/src/docs/hotchocolate/v16/fetching-data/integrations/mongodb.md index 67542fd6a0b..09177ee5e19 100644 --- a/website/src/docs/hotchocolate/v16/integrations/mongodb.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/integrations/mongodb.md @@ -220,6 +220,6 @@ public IExecutable GetPersonById( - [Pagination](/docs/hotchocolate/v16/fetching-data/pagination) for pagination setup - [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) for filtering concepts -- [Executable](/docs/hotchocolate/v16/fetching-data/executable) for the `IExecutable` abstraction +- [Executable](/docs/hotchocolate/v16/fetching-data/integrations) for the `IExecutable` abstraction diff --git a/website/src/docs/hotchocolate/v16/fetching-data/projections.md b/website/src/docs/hotchocolate/v16/fetching-data/projections.md index de5e8c6c413..5bd891606d7 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/projections.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/projections.md @@ -264,4 +264,4 @@ public class UserType : ObjectType - **Need to filter results?** See [Filtering](/docs/hotchocolate/v16/fetching-data/filtering). - **Need to sort results?** See [Sorting](/docs/hotchocolate/v16/fetching-data/sorting). - **Need to page through results?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination). -- **Need to integrate with Entity Framework?** See [Entity Framework Integration](/docs/hotchocolate/v16/fetching-data/entity-framework). +- **Need to integrate with Entity Framework?** See [Entity Framework Integration](/docs/hotchocolate/v16/fetching-data/integrations/entity-framework). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/resolvers.md b/website/src/docs/hotchocolate/v16/fetching-data/resolvers.md index e5d9712318b..f965966fb4d 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/resolvers.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/resolvers.md @@ -205,7 +205,7 @@ descriptor -[Learn more about arguments](/docs/hotchocolate/v15/defining-a-schema/arguments) +[Learn more about arguments](/docs/hotchocolate/v16/defining-a-schema/arguments) # Injecting Services diff --git a/website/src/docs/hotchocolate/v16/fetching-data/spatial-data.md b/website/src/docs/hotchocolate/v16/fetching-data/spatial-data.md index 6872a129f78..65913d31569 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/spatial-data.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/spatial-data.md @@ -321,6 +321,6 @@ The negation is `nwithin`. - [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) for general filtering concepts - [Projections](/docs/hotchocolate/v16/fetching-data/projections) for projection setup -- [Entity Framework integration](/docs/hotchocolate/v16/fetching-data/entity-framework) for EF Core setup +- [Entity Framework integration](/docs/hotchocolate/v16/fetching-data/integrations/entity-framework) for EF Core setup diff --git a/website/src/docs/hotchocolate/v16/get-started-with-graphql-in-net-core.md b/website/src/docs/hotchocolate/v16/get-started-with-graphql-in-net-core.md index fcf9bf05064..f11c4babbb0 100644 --- a/website/src/docs/hotchocolate/v16/get-started-with-graphql-in-net-core.md +++ b/website/src/docs/hotchocolate/v16/get-started-with-graphql-in-net-core.md @@ -176,7 +176,7 @@ Your GraphQL server is running and responding to queries. - **"I want to learn about the type system."** See [Defining a Schema](/docs/hotchocolate/v16/defining-a-schema) for queries, mutations, subscriptions, and all the GraphQL types. -- **"I want to fetch data from a database."** See [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader) for batched data fetching, or [Entity Framework](/docs/hotchocolate/v16/fetching-data/entity-framework) for EF Core integration. +- **"I want to fetch data from a database."** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for batched data fetching, or [Entity Framework](/docs/hotchocolate/v16/fetching-data/integrations/entity-framework) for EF Core integration. - **"I want a deeper tutorial."** Check out the [GraphQL Workshop](https://github.com/ChilliCream/graphql-workshop) for a hands-on walkthrough covering types, resolvers, DataLoaders, filtering, and more. diff --git a/website/src/docs/hotchocolate/v16/guides/dynamic-schemas.md b/website/src/docs/hotchocolate/v16/guides/dynamic-schemas.md index 4ba9574b76a..9027a44aa0d 100644 --- a/website/src/docs/hotchocolate/v16/guides/dynamic-schemas.md +++ b/website/src/docs/hotchocolate/v16/guides/dynamic-schemas.md @@ -219,7 +219,7 @@ builder # Next Steps -- **Extending existing types:** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/extending-types). +- **Extending existing types:** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/object-types). - **Defining types with the descriptor API:** See [Object Types](/docs/hotchocolate/v16/defining-a-schema/object-types). - **Warmup after schema rebuilds:** See [Warmup](/docs/hotchocolate/v16/server/warmup) for pre-populating caches when the schema changes. - **Type module source code:** Explore the `ITypeModule` interface in the Hot Chocolate source code under `src/HotChocolate/Core/src/Types/`. diff --git a/website/src/docs/hotchocolate/v16/guides/error-handling.md b/website/src/docs/hotchocolate/v16/guides/error-handling.md index 4b271818a5c..78aa703960d 100644 --- a/website/src/docs/hotchocolate/v16/guides/error-handling.md +++ b/website/src/docs/hotchocolate/v16/guides/error-handling.md @@ -529,5 +529,5 @@ public class UserByEmailResultType : UnionType # Next Steps - **Need mutation conventions?** See [Mutations](/docs/hotchocolate/v16/defining-a-schema/mutations) for the full pattern including inputs, payloads, and naming customization. -- **Need to build a schema?** See [Schema Basics](/docs/hotchocolate/v16/defining-a-schema/schema-basics) for an overview of how types, queries, and mutations fit together. -- **Need to fetch data?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader) for efficient data fetching patterns. +- **Need to build a schema?** See [Schema Basics](/docs/hotchocolate/v16/defining-a-schema) for an overview of how types, queries, and mutations fit together. +- **Need to fetch data?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for efficient data fetching patterns. diff --git a/website/src/docs/hotchocolate/v16/guides/federation.md b/website/src/docs/hotchocolate/v16/guides/federation.md index d137b5400b2..82041e70bf2 100644 --- a/website/src/docs/hotchocolate/v16/guides/federation.md +++ b/website/src/docs/hotchocolate/v16/guides/federation.md @@ -228,7 +228,7 @@ Key details: -> We recommend using a [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader) in reference resolvers. This helps avoid [the N+1 problem](https://www.apollographql.com/docs/federation/entities-advanced#handling-the-n1-problem). +> We recommend using a [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) in reference resolvers. This helps avoid [the N+1 problem](https://www.apollographql.com/docs/federation/entities-advanced#handling-the-n1-problem). ## Register the Entity @@ -417,5 +417,5 @@ For creating a supergraph, see the [Apollo Router documentation](https://www.apo # Next Steps - [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) for resolver patterns -- [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader) for batching in reference resolvers +- [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for batching in reference resolvers - [Apollo Federation docs](https://www.apollographql.com/docs/federation/) for supergraph configuration diff --git a/website/src/docs/hotchocolate/v16/guides/performance.md b/website/src/docs/hotchocolate/v16/guides/performance.md index 6c295bd760d..f170eca9124 100644 --- a/website/src/docs/hotchocolate/v16/guides/performance.md +++ b/website/src/docs/hotchocolate/v16/guides/performance.md @@ -82,7 +82,7 @@ public class ProductByIdDataLoader : BatchDataLoader For most applications, the source-generated DataLoader approach (using the `[DataLoader]` attribute) is the recommended starting point. -[Learn more about DataLoaders](/docs/hotchocolate/v16/fetching-data/dataloader) +[Learn more about DataLoaders](/docs/hotchocolate/v16/fetching-data/batching/dataloader) # Projections and Database Efficiency @@ -221,7 +221,7 @@ Diagnostic event handlers execute synchronously as part of the GraphQL request. - **Server warmup:** [Warmup](/docs/hotchocolate/v16/server/warmup) covers custom warmup tasks and lazy initialization. - **Persisted operations:** [Persisted Operations](/docs/hotchocolate/v16/performance/trusted-documents) covers both pre-stored and automatic persisted operations. -- **DataLoaders:** [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader) covers source-generated DataLoaders, manual DataLoader classes, and batch resolvers. +- **DataLoaders:** [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) covers source-generated DataLoaders, manual DataLoader classes, and batch resolvers. - **Projections:** [Projections](/docs/hotchocolate/v16/fetching-data/projections) covers the `[UseProjection]` middleware and `QueryContext`. - **Cost analysis:** [Cost Analysis](/docs/hotchocolate/v16/security/cost-analysis) covers custom weights, filtering and sorting costs, and the tuning guide. - **Instrumentation:** [Instrumentation](/docs/hotchocolate/v16/server/instrumentation) covers diagnostic event listeners and OpenTelemetry integration. diff --git a/website/src/docs/hotchocolate/v16/guides/testing.md b/website/src/docs/hotchocolate/v16/guides/testing.md index 75637403d4d..1d62c65d92b 100644 --- a/website/src/docs/hotchocolate/v16/guides/testing.md +++ b/website/src/docs/hotchocolate/v16/guides/testing.md @@ -93,7 +93,7 @@ public async Task Get_Product_By_Id() # Snapshot Testing with CookieCrumble -Asserting on individual fields works for small results, but GraphQL responses can be large and nested. Snapshot testing captures the entire response and compares it against a stored baseline. Hot Chocolate uses [CookieCrumble](/docs/hotchocolate/v16/testing) for this. +Asserting on individual fields works for small results, but GraphQL responses can be large and nested. Snapshot testing captures the entire response and compares it against a stored baseline. Hot Chocolate uses [CookieCrumble](/docs/hotchocolate/v16/guides/testing) for this. ## File-Based Snapshots diff --git a/website/src/docs/hotchocolate/v16/index.md b/website/src/docs/hotchocolate/v16/index.md index 5fa111c961c..fbe69fcf0ab 100644 --- a/website/src/docs/hotchocolate/v16/index.md +++ b/website/src/docs/hotchocolate/v16/index.md @@ -117,7 +117,7 @@ Where you go from here depends on what you need: - **"I want to understand the schema system."** Read [Defining a Schema](/docs/hotchocolate/v16/defining-a-schema). It covers queries, mutations, subscriptions, and all the GraphQL types. -- **"I need to fetch data efficiently."** Go to [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader) for batching and caching, or [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) for the full resolver API. +- **"I need to fetch data efficiently."** Go to [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for batching and caching, or [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) for the full resolver API. - **"I need to secure my API."** See [Securing Your API](/docs/hotchocolate/v16/security) for authentication, authorization, cost analysis, and trusted documents. diff --git a/website/src/docs/hotchocolate/v16/security/cost-analysis.md b/website/src/docs/hotchocolate/v16/security/cost-analysis.md index 74ef71b34d2..8d3da29bc51 100644 --- a/website/src/docs/hotchocolate/v16/security/cost-analysis.md +++ b/website/src/docs/hotchocolate/v16/security/cost-analysis.md @@ -390,5 +390,5 @@ builder - **Need to restrict access to fields?** See [Authorization](/docs/hotchocolate/v16/security/authorization). - **Building a private API?** See [Trusted Documents](/docs/hotchocolate/v16/performance/trusted-documents). -- **Need to limit query depth?** See [Query Depth](/docs/hotchocolate/v16/security/query-depth). +- **Need to limit query depth?** See [Request Limits](/docs/hotchocolate/v16/security/request-limits). - **Need an overview of security options?** See [Security Overview](/docs/hotchocolate/v16/security). diff --git a/website/src/docs/hotchocolate/v16/server/endpoints.md b/website/src/docs/hotchocolate/v16/server/endpoints.md index 690d4f01f8b..a0fd945ad8c 100644 --- a/website/src/docs/hotchocolate/v16/server/endpoints.md +++ b/website/src/docs/hotchocolate/v16/server/endpoints.md @@ -334,7 +334,7 @@ app.MapGraphQLPersistedOperations(requireOperationName: true); When enabled, requests to `/{operationId}` without an operation name return a `400 Bad Request` response. -For details on storing and managing persisted operations, see [Trusted Documents](/docs/hotchocolate/v16/security/trusted-documents). +For details on storing and managing persisted operations, see [Trusted Documents](/docs/hotchocolate/v16/performance/trusted-documents). # AddGraphQL Parameters @@ -458,5 +458,5 @@ Per-endpoint `WithOptions` overrides take precedence over schema-level defaults. - [HTTP Transport](/docs/hotchocolate/v16/server/http-transport) for details on request formats, response formats, WebSocket transport, and SSE. - [Interceptors](/docs/hotchocolate/v16/server/interceptors) for hooking into request processing. -- [Trusted Documents](/docs/hotchocolate/v16/security/trusted-documents) for the full persisted operations workflow. +- [Trusted Documents](/docs/hotchocolate/v16/performance/trusted-documents) for the full persisted operations workflow. - [Cost Analysis](/docs/hotchocolate/v16/security/cost-analysis) for understanding the default security cost analyzer. From b0a3f1b1eb38d9fd48eaf0b4d6ca54481a118aca Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 12 May 2026 18:01:12 -0400 Subject: [PATCH 3/6] fixed errors --- website/src/docs/docs.json | 8 +- .../v16/fetching-data/dependency-injection.md | 2 +- .../hotchocolate/v16/fetching-data/errors.md | 190 ++++++++++++++---- 3 files changed, 157 insertions(+), 43 deletions(-) diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index dbe0cf60028..7bc8cab4304 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -576,6 +576,10 @@ "path": "dependency-injection", "title": "Dependency Injection" }, + { + "path": "errors", + "title": "Errors" + }, { "path": "field-middleware", "title": "Field Middleware" @@ -614,10 +618,6 @@ } ] }, - { - "path": "errors", - "title": "Errors" - }, { "path": "integrations", "title": "Integrations", diff --git a/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md b/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md index b9b41495e6b..f60ea06091b 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md @@ -203,7 +203,7 @@ public sealed class QueryType : ObjectType # Switching the Service Provider -While Hot Chocolate's internals rely on Microsoft's dependency injection container, you are not required to manage your own dependencies using Microsoft`s container. By default, Hot Chocolate uses the request-scoped [`HttpContext.RequestServices`](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestservices) `IServiceProvider` to provide services to your resolvers. +While Hot Chocolate's internals rely on Microsoft's dependency injection container, you are not required to manage your own dependencies using Microsoft's container. By default, Hot Chocolate uses the request-scoped [`HttpContext.RequestServices`](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestservices) `IServiceProvider` to provide services to your resolvers. You can switch out the service provider used for GraphQL requests, as long as your DI container implements the [`IServiceProvider`](https://docs.microsoft.com/dotnet/api/system.iserviceprovider) interface and supports scoping. diff --git a/website/src/docs/hotchocolate/v16/fetching-data/errors.md b/website/src/docs/hotchocolate/v16/fetching-data/errors.md index 56a861bb16e..acda6fbf839 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/errors.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/errors.md @@ -3,77 +3,191 @@ title: Errors description: Learn how to handle, create, and filter GraphQL errors in Hot Chocolate. --- -Hot Chocolate provides several ways to report errors from your GraphQL resolvers. You can return `IError` instances, throw a `GraphQLException`, or use non-terminating field errors through `IResolverContext.ReportError`. +In GraphQL, errors are not all-or-nothing. If a resolver fails, the rest of the query can still return data. This is called a _field error_ (or non-terminating error). When this happens, the failed field returns `null`, and an entry is added to the `errors` array. Other fields continue to resolve as usual. -# Returning Errors +# Exceptions -Return an `IError` or `IEnumerable` from a field resolver to report errors in the GraphQL response. +The easiest way to signal an error is to throw an exception. Hot Chocolate will catch any exception thrown during resolver execution and automatically turn it into a GraphQL error. -Throw a `GraphQLException` from any resolver, and the execution engine catches it and translates it into a field error. + + -Call `IResolverContext.ReportError` to raise a non-terminating error. This allows you to return a result and report an error for the same field. +```csharp +[QueryType] +public static partial class Query +{ + public static Book GetBook() + { + throw new InvalidOperationException("Something went wrong."); + } +} +``` -> To log errors, see the [instrumentation documentation](/docs/hotchocolate/v16/server/instrumentation) for connecting your logging framework. + + -# Error Builder +```csharp +public class QueryType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field("book") + .Type() + .Resolve(ctx => + { + throw new InvalidOperationException("Something went wrong."); + }); + } +} +``` -Errors can have many properties. The `ErrorBuilder` provides a fluent API for constructing them: + + -```csharp -var error = ErrorBuilder - .New() - .SetMessage("This is my error.") - .SetCode("FOO_BAR") - .Build(); +By default, Hot Chocolate does **not** send exception details to the client. Instead, the response contains a generic message to avoid exposing internal information. + +```json +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [{ "line": 1, "column": 3 }], + "path": ["book"] + } + ], + "data": { + "book": null + } +} ``` # Error Filters -When an unexpected exception is thrown during execution, the engine creates an `IError` with the message **Unexpected Execution Error** and attaches the original exception. Exception details are not serialized by default, so the user sees only the generic message. +If you use typed exceptions and want to return specific GraphQL errors, you can implement error filters. Error filters let you catch errors before they reach the client and rewrite them as needed. -To translate exceptions into errors with useful information, implement an `IErrorFilter` and register it: +For example, if your service throws a `NotFoundException`, you can map it to a clear error message and code: ```csharp -builder.Services.AddErrorFilter(); +builder + .AddGraphQL() + .AddErrorFilter(error => + { + if (error.Exception is NotFoundException ex) + { + return ErrorBuilder + .FromError(error) + .SetMessage(ex.Message) + .SetCode("NOT_FOUND") + .Build(); + } + + return error; + }); ``` -You can also register a filter as a delegate: +Now, when a resolver throws a `NotFoundException`, the client receives a structured error instead of a generic message: -```csharp -builder.Services.AddErrorFilter(error => +```json { - if (error.Exception is NullReferenceException) + "errors": [ { - return error.WithCode("NullRef"); + "message": "The book with ID '123' was not found.", + "locations": [{ "line": 1, "column": 3 }], + "path": ["book"], + "extensions": { + "code": "NOT_FOUND" + } } + ], + "data": { + "book": null + } +} +``` + +> **Note:** Errors are immutable. Methods like `WithMessage`, `WithCode`, and `RemoveExtension` return a new error instance. Use `ErrorBuilder.FromError(error)` to change multiple properties at once. - return error; -}); +# GraphQLException + +If you want full control over the error in a resolver, throw a `GraphQLException`. Unlike regular exceptions, these errors are sent to the client as-is, without being wrapped in a generic message. + + + + +```csharp +[QueryType] +public static partial class Query +{ + public static Book GetBook() + { + throw new GraphQLException("The book could not be found."); + } +} ``` -Errors are immutable. Helper methods like `WithMessage`, `WithCode`, and others return a new error with the desired properties. You can also create a builder from an existing error to modify multiple properties: + + ```csharp -return ErrorBuilder - .FromError(error) - .SetMessage("This is my error.") - .SetCode("FOO_BAR") - .Build(); +public class QueryType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field("book") + .Type() + .Resolve(ctx => + { + throw new GraphQLException("The book could not be found."); + }); + } +} ``` -# Exception Details + + -To include exception details in GraphQL errors automatically, enable the `IncludeExceptionDetails` option. By default, this is enabled when the debugger is attached: +```json +{ + "errors": [ + { + "message": "The book could not be found.", + "locations": [{ "line": 1, "column": 3 }], + "path": ["book"] + } + ], + "data": { + "book": null + } +} +``` + +You can also use `GraphQLException` with an `ErrorBuilder` to add a code, extensions, or multiple errors: ```csharp -builder - .AddGraphQL() - .ModifyRequestOptions( - o => o.IncludeExceptionDetails = - builder.Environment.IsDevelopment()); +throw new GraphQLException( + ErrorBuilder + .New() + .SetMessage("The book could not be found.") + .SetCode("BOOK_NOT_FOUND") + .Build()); ``` -> Do not enable `IncludeExceptionDetails` in production. Exception details can leak security-sensitive information. +```json +{ + "errors": [ + { + "message": "The book could not be found.", + "locations": [{ "line": 1, "column": 3 }], + "path": ["book"], + "extensions": { + "code": "BOOK_NOT_FOUND" + } + } + ] +} +``` # Next Steps From 5f1c7c78c364c85e90ec79ffa0c9fefd6520058a Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 12 May 2026 18:05:29 -0400 Subject: [PATCH 4/6] middleware --- .../v16/fetching-data/field-middleware.md | 80 +++++++++---------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/website/src/docs/hotchocolate/v16/fetching-data/field-middleware.md b/website/src/docs/hotchocolate/v16/fetching-data/field-middleware.md index 87a14f78ae2..65d311c79da 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/field-middleware.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/field-middleware.md @@ -3,21 +3,19 @@ title: Field Middleware description: Learn how to create and apply field middleware in Hot Chocolate to run reusable logic before or after field resolvers. --- -Field middleware is one of the fundamental components in Hot Chocolate. It allows you to create reusable logic that runs before or after a field resolver. Field middleware is composable: you can specify multiple middleware, and they execute in order. The field resolver is always the last element in the middleware chain. +Field middleware in Hot Chocolate lets you add reusable logic to a field, either before or after the field resolver runs. This is useful for adding features like logging, validation, or authorization. You can stack multiple middleware, and they run in the order you declare them. The field resolver always comes last in the chain. -Each field middleware only knows about the next element in the chain and can choose to: +Each middleware only knows about the next step in the chain. It can: -- Execute logic before it -- Execute logic after all later components (including the field resolver) have run -- Skip the next component entirely +- Run logic before the next step +- Run logic after the next step (including after the resolver) +- Skip the next step entirely -Each field middleware has access to an `IMiddlewareContext`. This interface extends `IResolverContext`, so you can use all of the `IResolverContext` APIs in your middleware, the same way you would in a resolver. The `IMiddlewareContext` also provides special properties like `Result`, which holds the resolver or middleware computed result. +Every middleware receives an `IMiddlewareContext`. This extends `IResolverContext`, so you can use all the same APIs as in a resolver. The `IMiddlewareContext` also has properties like `Result`, which holds the value returned by the resolver or another middleware. # Middleware Order -If you have used Hot Chocolate's data middleware before, you may have encountered warnings about the order of middleware. The order matters because it determines the sequence in which the resolver result is processed. - -Take `UsePaging` and `UseFiltering` for example: filtering must happen before pagination. That is why the correct declaration order is `UsePaging` before `UseFiltering`. +The order in which you add middleware matters. It controls how the resolver result is processed. For example, if you use both `UsePaging` and `UseFiltering`, filtering should happen before paging. So, you declare `UsePaging` before `UseFiltering`. ```csharp descriptor @@ -29,7 +27,7 @@ descriptor }); ``` -But this looks like the opposite order. The following diagram shows why it is correct: +At first, this might look backwards. The diagram below explains why this order works: ```mermaid sequenceDiagram @@ -39,17 +37,17 @@ sequenceDiagram UseFiltering->>UsePaging: Result of UseFiltering ``` -The result of the resolver flows backward through the middleware. The middleware is first invoked in declaration order, but the result produced by the resolver travels back through the chain in reverse order. +Middleware runs in the order you declare it, but the result from the resolver travels back through the chain in reverse. This is why the order is important. # Defining Field Middleware -You can define field middleware either as a delegate or as a separate class. In both cases you gain access to a `FieldDelegate` (which invokes the next middleware) and the `IMiddlewareContext`. +You can create field middleware as either a delegate or a class. In both cases, you get a `FieldDelegate` (which calls the next middleware) and the `IMiddlewareContext`. -By awaiting the `FieldDelegate`, you wait for all subsequent middleware and the field resolver to complete. +When you await the `FieldDelegate`, you let all later middleware and the resolver run before your code continues. ## Delegate-based middleware -Define a field middleware delegate using code-first APIs: +To define middleware as a delegate, use the code-first API: ```csharp public class QueryType : ObjectType @@ -62,10 +60,10 @@ public class QueryType : ObjectType { // Runs before the next middleware and the field resolver - // Invoke the next middleware or the field resolver + // Call the next middleware or the resolver await next(context); - // Runs after all later middleware and the field resolver + // Runs after all later middleware and the resolver }) .Resolve(context => { @@ -77,7 +75,7 @@ public class QueryType : ObjectType ### Reusing the middleware delegate -The example above applies the middleware to a single field. To reuse it across multiple fields, create an extension method on `IObjectFieldDescriptor`: +The example above adds middleware to one field. To reuse it, create an extension method on `IObjectFieldDescriptor`: ```csharp public static class MyMiddlewareObjectFieldDescriptorExtension @@ -98,9 +96,9 @@ public static class MyMiddlewareObjectFieldDescriptorExtension } ``` -> We recommend prepending `Use` to your extension method name to indicate that it applies middleware. +> It's a good idea to start your extension method name with `Use` to show it adds middleware. -You can now use this middleware across your schema: +Now you can use this middleware on any field in your schema: ```csharp public class QueryType : ObjectType @@ -120,7 +118,7 @@ public class QueryType : ObjectType ## Class-based middleware -If you prefer a class over a delegate, create a dedicated middleware class: +If you prefer, you can write middleware as a class. Create a class that takes a `FieldDelegate` in its constructor and has a method called `InvokeAsync` or `Invoke`. ```csharp public class MyMiddleware @@ -132,19 +130,18 @@ public class MyMiddleware _next = next; } - // This method must be called InvokeAsync or Invoke public async Task InvokeAsync(IMiddlewareContext context) { - // Runs before the next middleware and the field resolver + // Runs before the next middleware and the resolver await _next(context); - // Runs after all later middleware and the field resolver + // Runs after all later middleware and the resolver } } ``` -You can inject services through the constructor (for singletons) or as parameters on the `InvokeAsync` method (for scoped or transient services): +You can inject services into the constructor (for singletons) or as parameters on `InvokeAsync` (for scoped or transient services): ```csharp public class MyMiddleware @@ -166,11 +163,11 @@ public class MyMiddleware } ``` -The ability to add additional arguments to the `InvokeAsync` method is the reason there is no interface or base class for field middleware. +This flexibility is why there is no interface or base class for field middleware. ### Applying class-based middleware -Apply the class-based middleware to a field using `Use()`: +To use your class-based middleware, call `Use()` on the field: ```csharp public class QueryType : ObjectType @@ -188,9 +185,9 @@ public class QueryType : ObjectType } ``` -We still recommend wrapping `Use()` in an extension method like `UseMyMiddleware()`. This makes future changes to the middleware easier without modifying every call site. +It's still a good idea to wrap `Use()` in an extension method like `UseMyMiddleware()`. This makes it easier to change the middleware later without updating every field. -If you need to pass a custom argument to the middleware, use the factory overload: +If you need to pass a custom argument, use the factory overload: ```csharp descriptor @@ -201,9 +198,9 @@ descriptor # Using Middleware as an Attribute -To apply middleware to resolvers defined using the annotation-based approach, create an attribute inheriting from `ObjectFieldDescriptorAttribute` and call your middleware in the `OnConfigure` method. +You can also add middleware to resolvers that use attributes. To do this, create an attribute that inherits from `ObjectFieldDescriptorAttribute` and call your middleware in the `OnConfigure` method. -> Attribute order is not guaranteed in C#, so middleware attributes use the `CallerLineNumberAttribute` to inject the C# line number at compile time. The line number determines the ordering. Avoid inheriting middleware attributes from a base method or property, as this can lead to confusion about ordering. Always pass through the `order` argument when inheriting from middleware attributes. Prepend `Use` to your attribute name to indicate it applies middleware. +> In C#, attribute order is not guaranteed. Middleware attributes use the `CallerLineNumberAttribute` to capture the line number at compile time, which sets the order. Avoid inheriting middleware attributes from a base method or property, as this can make the order unclear. Always pass the `order` argument if you inherit from another middleware attribute. Start your attribute name with `Use` to show it adds middleware. ```csharp public class UseMyMiddlewareAttribute : ObjectFieldDescriptorAttribute @@ -221,7 +218,7 @@ public class UseMyMiddlewareAttribute : ObjectFieldDescriptorAttribute } ``` -Apply the attribute to a resolver: +Now you can add the attribute to a resolver: ```csharp public class Query @@ -236,7 +233,7 @@ public class Query # Accessing the Resolver Result -The `IMiddlewareContext` provides a `Result` property that you can use to read or modify the field resolver result: +The `IMiddlewareContext` has a `Result` property you can use to read or change the value returned by the resolver: ```csharp descriptor @@ -244,11 +241,10 @@ descriptor { await next(context); - // Access the result after calling next(context), - // after the field resolver and any later middleware have finished + // After calling next(context), you can access the result object? result = context.Result; - // Narrow down the type using pattern matching + // You can check the type and work with it if (result is string stringResult) { // Work with the stringResult @@ -256,13 +252,13 @@ descriptor }); ``` -A middleware can also set or override the result by assigning `context.Result`. +Middleware can also set or override the result by assigning to `context.Result`. -> The field resolver only executes if no preceding middleware has set the `Result` property on the `IMiddlewareContext`. If any middleware sets the `Result`, the field resolver is skipped. +> The field resolver only runs if no earlier middleware has set the `Result` property. If any middleware sets `Result`, the resolver is skipped. # Short-Circuiting -In some cases you may want to short-circuit the middleware chain and skip the field resolver. To do this, do not call the `FieldDelegate` (`next`): +Sometimes, you may want to stop the middleware chain and skip the field resolver. To do this, simply do not call the `FieldDelegate` (`next`). ```csharp descriptor @@ -272,7 +268,7 @@ descriptor { context.Result = dict[context.Field.Name]; - // The remaining middleware and field resolver do not execute + // The rest of the middleware and the resolver will not run return Task.CompletedTask; } else @@ -284,6 +280,6 @@ descriptor # Next Steps -- [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) for field resolution -- [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [sorting](/docs/hotchocolate/v16/fetching-data/sorting) middleware -- [Pagination](/docs/hotchocolate/v16/fetching-data/pagination) middleware +- Learn more about [resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) for field resolution +- Explore [filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [sorting](/docs/hotchocolate/v16/fetching-data/sorting) middleware +- Read about [pagination](/docs/hotchocolate/v16/fetching-data/pagination) middleware From 920d925c95c80e02d55ccefabd5529ee30a21a67 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 12 May 2026 18:24:02 -0400 Subject: [PATCH 5/6] Restructure --- website/src/docs/docs.json | 14 +++- .../v16/defining-a-schema/arguments.md | 2 +- .../v16/defining-a-schema/index.md | 2 +- .../v16/defining-a-schema/object-types.md | 2 +- .../v16/defining-a-schema/queries.md | 2 +- .../fetching-data/batching/batch-resolver.md | 2 +- .../v16/fetching-data/batching/dataloader.md | 4 +- .../v16/fetching-data/batching/index.md | 4 +- .../hotchocolate/v16/fetching-data/index.md | 64 ++++--------------- .../integrations/entity-framework.md | 8 +-- .../hotchocolate/v16/guides/federation.md | 2 +- website/src/docs/hotchocolate/v16/index.md | 2 +- .../dependency-injection.md | 0 .../{fetching-data => resolvers}/errors.md | 0 .../field-middleware.md | 2 +- .../docs/hotchocolate/v16/resolvers/index.md | 43 +++++++++++++ .../{fetching-data => resolvers}/resolvers.md | 2 +- .../hotchocolate/v16/server/global-state.md | 2 +- .../src/docs/hotchocolate/v16/server/index.md | 2 +- .../hotchocolate/v16/server/interceptors.md | 2 +- .../docs/hotchocolate/v16/server/options.md | 2 +- 21 files changed, 89 insertions(+), 74 deletions(-) rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers}/dependency-injection.md (100%) rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers}/errors.md (100%) rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers}/field-middleware.md (98%) create mode 100644 website/src/docs/hotchocolate/v16/resolvers/index.md rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers}/resolvers.md (98%) diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index 7bc8cab4304..034864e42e6 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -561,8 +561,8 @@ ] }, { - "path": "fetching-data", - "title": "Resolvers and Data", + "path": "resolvers", + "title": "Resolvers", "items": [ { "path": "index", @@ -583,6 +583,16 @@ { "path": "field-middleware", "title": "Field Middleware" + } + ] + }, + { + "path": "fetching-data", + "title": "Fetching Data", + "items": [ + { + "path": "index", + "title": "Overview" }, { "path": "pagination", diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/arguments.md b/website/src/docs/hotchocolate/v16/defining-a-schema/arguments.md index 1cf88f11d49..ba24e0a8ebb 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/arguments.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/arguments.md @@ -219,4 +219,4 @@ type Query { - **Need structured input?** See [Input Object Types](/docs/hotchocolate/v16/defining-a-schema/input-object-types). - **Need to understand nullability?** See [Non-Null](/docs/hotchocolate/v16/defining-a-schema/non-null). - **Need global IDs?** See [Relay](/docs/hotchocolate/v16/defining-a-schema/relay). -- **Need to set up resolvers?** See [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). +- **Need to set up resolvers?** See [Resolvers](/docs/hotchocolate/v16/resolvers/resolvers). diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/index.md b/website/src/docs/hotchocolate/v16/defining-a-schema/index.md index 1fb35186449..d148f310c7d 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/index.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/index.md @@ -199,4 +199,4 @@ Use type extensions for static modularity. Choose dynamic schemas only when the - Learn how returned shapes are inferred and configured in [Object Types](./object-types). - Add field inputs with [Arguments](./arguments) and [Input Object Types](./input-object-types). - Strengthen the contract with [Lists and Non-Null](./lists). -- Move to runtime behavior with [Resolvers](../fetching-data/resolvers) and [DataLoader](../fetching-data/batching/dataloader) after the schema shape is clear. +- Move to runtime behavior with [Resolvers](../resolvers/resolvers) and [DataLoader](../fetching-data/batching/dataloader) after the schema shape is clear. diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/object-types.md b/website/src/docs/hotchocolate/v16/defining-a-schema/object-types.md index 4001fd08d8e..8cac30a5096 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/object-types.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/object-types.md @@ -642,7 +642,7 @@ This works with any key and value types. For example, `Dictionary` # Next Steps - **Need to define query entry points?** See [Queries](/docs/hotchocolate/v16/defining-a-schema/queries). -- **Need to understand resolver patterns?** See [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). +- **Need to understand resolver patterns?** See [Resolvers](/docs/hotchocolate/v16/resolvers/resolvers). - **Need to compose types from multiple classes?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/object-types). - **Need to define input for mutations?** See [Input Object Types](/docs/hotchocolate/v16/defining-a-schema/input-object-types). - **Need to fetch data efficiently?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader). diff --git a/website/src/docs/hotchocolate/v16/defining-a-schema/queries.md b/website/src/docs/hotchocolate/v16/defining-a-schema/queries.md index 38bac9a8909..2c15cca7924 100644 --- a/website/src/docs/hotchocolate/v16/defining-a-schema/queries.md +++ b/website/src/docs/hotchocolate/v16/defining-a-schema/queries.md @@ -165,4 +165,4 @@ public static List GetBooks() => /* ... */; - **Need to write data?** See [Mutations](/docs/hotchocolate/v16/defining-a-schema/mutations). - **Need real-time updates?** See [Subscriptions](/docs/hotchocolate/v16/defining-a-schema/subscriptions). - **Need to understand how types map to the schema?** See [Object Types](/docs/hotchocolate/v16/defining-a-schema/object-types). -- **Need to fetch data efficiently?** See [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) and [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader). +- **Need to fetch data efficiently?** See [Resolvers](/docs/hotchocolate/v16/resolvers/resolvers) and [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/batching/batch-resolver.md b/website/src/docs/hotchocolate/v16/fetching-data/batching/batch-resolver.md index d1d654fc60d..d2f16f6e3ca 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/batching/batch-resolver.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/batching/batch-resolver.md @@ -114,4 +114,4 @@ descriptor # Next Steps - **Need to understand the N+1 problem?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for key-based batching with caching. -- **Need to understand resolver basics?** See [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). +- **Need to understand resolver basics?** See [Resolvers](/docs/hotchocolate/v16/resolvers/resolvers). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/batching/dataloader.md b/website/src/docs/hotchocolate/v16/fetching-data/batching/dataloader.md index 747b5671657..e1dda5874d1 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/batching/dataloader.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/batching/dataloader.md @@ -4,7 +4,7 @@ title: "DataLoader" DataLoaders solve the N+1 problem in GraphQL. When the execution engine resolves a list of objects and each object needs related data, a naive implementation fires one database query per object. A DataLoader collects all those individual requests, waits for the execution engine to finish the current batch of resolvers, and then sends one query for all requested keys at once. -This page covers the source-generated DataLoader (the recommended approach) and manual DataLoader classes. If you are new to GraphQL data fetching, start with [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) first. +This page covers the source-generated DataLoader (the recommended approach) and manual DataLoader classes. If you are new to GraphQL data fetching, start with [Resolvers](/docs/hotchocolate/v16/resolvers/resolvers) first. # The N+1 Problem @@ -212,6 +212,6 @@ public class BrandByIdDataLoader : BatchDataLoader # Next Steps - **Need simpler batching without caching?** See [Batch Resolvers](/docs/hotchocolate/v16/fetching-data/batching/batch-resolver). -- **Need to understand resolver basics?** See [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). +- **Need to understand resolver basics?** See [Resolvers](/docs/hotchocolate/v16/resolvers/resolvers). - **Need pagination?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination) for cursor-based connections. - **Using Entity Framework?** See [Entity Framework](/docs/hotchocolate/v16/fetching-data/integrations/entity-framework) for integration patterns with DataLoaders. diff --git a/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md b/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md index 3d8bde04c4d..89845fa0c21 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md @@ -39,7 +39,7 @@ Execution completes when every resolver in the tree has produced a result. Resolvers are the building blocks of data fetching. A resolver can call a database, a REST API, a gRPC service, or any other data source. In Hot Chocolate, the source generator is the primary way to define resolvers. You write plain C# methods and the generator wires them into the schema. -[Learn more about resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) +[Learn more about resolvers](/docs/hotchocolate/v16/resolvers/resolvers) # DataLoader @@ -77,7 +77,7 @@ Hot Chocolate is not bound to a specific database or architecture. You can fetch # Next Steps -- **New to resolvers?** Start with [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). +- **New to resolvers?** Start with [Resolvers](/docs/hotchocolate/v16/resolvers/resolvers). - **Need to batch data access?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader). - **Need to page through lists?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination). - **Need to filter or sort?** See [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [Sorting](/docs/hotchocolate/v16/fetching-data/sorting). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/index.md b/website/src/docs/hotchocolate/v16/fetching-data/index.md index 3d8bde04c4d..98f4e92acda 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/index.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/index.md @@ -2,50 +2,7 @@ title: Overview --- -Every field in a GraphQL schema is backed by a resolver function that produces the field's value. Understanding how resolvers compose into a tree is the key mental model for building efficient GraphQL APIs with Hot Chocolate. - -# The Resolver Tree - -When Hot Chocolate receives a query, it builds a resolver tree that mirrors the shape of the request. Consider this query: - -```graphql -query { - me { - name - company { - id - name - } - } -} -``` - -This produces the following resolver tree: - -```mermaid -graph LR - A(query: QueryType) --> B(me: UserType) - B --> C(name: StringType) - B --> D(company: CompanyType) - D --> E(id: IdType) - D --> F(name: StringType) -``` - -The execution engine traverses this tree starting from root resolvers. A child resolver can only execute after its parent has produced a value. Sibling resolvers at the same level run in parallel. Because of this parallel execution, resolvers (except top-level mutation fields) must be free of side effects. - -Execution completes when every resolver in the tree has produced a result. - -# Resolvers - -Resolvers are the building blocks of data fetching. A resolver can call a database, a REST API, a gRPC service, or any other data source. In Hot Chocolate, the source generator is the primary way to define resolvers. You write plain C# methods and the generator wires them into the schema. - -[Learn more about resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) - -# DataLoader - -DataLoaders deduplicate and batch requests to data sources. When multiple resolvers request the same entity in a single request, a DataLoader ensures only one call goes to the backing store. DataLoaders can significantly reduce the load on your databases and services. - -[Learn more about DataLoaders](/docs/hotchocolate/v16/fetching-data/batching/dataloader) +Hot Chocolate provides data middleware that applies common operations directly to your `IQueryable` or `IExecutable` data sources. Instead of implementing pagination, filtering, sorting, and projections by hand, you declare them on your fields and Hot Chocolate generates the corresponding GraphQL types and applies the operations at execution time. # Pagination @@ -71,13 +28,18 @@ Projections optimize database queries by selecting only the columns that match t [Learn more about projections](/docs/hotchocolate/v16/fetching-data/projections) -# Data Sources +# Batching + +DataLoaders and batch resolvers solve the N+1 problem in GraphQL. When the execution engine resolves a list of objects and each needs related data, a DataLoader collects all individual requests and sends a single query for all keys at once. + +- [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for key-based batching with deduplication and caching. +- [Batch Resolvers](/docs/hotchocolate/v16/fetching-data/batching/batch-resolver) for simpler cases where caching is not needed. -Hot Chocolate is not bound to a specific database or architecture. You can fetch data from any source in your resolvers. We provide specific guidance for the most common patterns: +# Integrations -# Next Steps +Hot Chocolate is not bound to a specific database. The data middleware works with any `IQueryable` provider. We provide specific guidance for the most common data sources: -- **New to resolvers?** Start with [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers). -- **Need to batch data access?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader). -- **Need to page through lists?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination). -- **Need to filter or sort?** See [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [Sorting](/docs/hotchocolate/v16/fetching-data/sorting). +- [Entity Framework](/docs/hotchocolate/v16/fetching-data/integrations/entity-framework) for EF Core DbContext patterns and pooling. +- [MongoDB](/docs/hotchocolate/v16/fetching-data/integrations/mongodb) for the MongoDB driver integration. +- [Marten](/docs/hotchocolate/v16/fetching-data/integrations/marten) for Marten document database support. +- [Extending Filtering](/docs/hotchocolate/v16/fetching-data/integrations/extending-filtering) for building custom filter providers. diff --git a/website/src/docs/hotchocolate/v16/fetching-data/integrations/entity-framework.md b/website/src/docs/hotchocolate/v16/fetching-data/integrations/entity-framework.md index 1e4741bb417..d5124d8273a 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/integrations/entity-framework.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/integrations/entity-framework.md @@ -7,14 +7,14 @@ description: Learn how to integrate Entity Framework Core with Hot Chocolate, in # Resolver Injection of a DbContext -When using the [default scope](/docs/hotchocolate/v16/fetching-data/dependency-injection#default-scope) for queries, each resolver that accepts a scoped `DbContext` receives a **separate** instance. This avoids [threading issues](https://learn.microsoft.com/en-gb/ef/core/dbcontext-configuration/#avoiding-dbcontext-threading-issues). +When using the [default scope](/docs/hotchocolate/v16/resolvers/dependency-injection#default-scope) for queries, each resolver that accepts a scoped `DbContext` receives a **separate** instance. This avoids [threading issues](https://learn.microsoft.com/en-gb/ef/core/dbcontext-configuration/#avoiding-dbcontext-threading-issues). ```csharp public static async Task GetBookByIdAsync( ApplicationDbContext dbContext) => // ... ``` -When using the [default scope](/docs/hotchocolate/v16/fetching-data/dependency-injection#default-scope) for mutations, each mutation resolver that accepts a scoped `DbContext` receives the **same** request-scoped instance, as mutations execute sequentially. +When using the [default scope](/docs/hotchocolate/v16/resolvers/dependency-injection#default-scope) for mutations, each mutation resolver that accepts a scoped `DbContext` receives the **same** request-scoped instance, as mutations execute sequentially. ```csharp public static async Task AddBookAsync( @@ -22,7 +22,7 @@ public static async Task AddBookAsync( AppDbContext dbContext) => // ... ``` -See the [Dependency Injection](/docs/hotchocolate/v16/fetching-data/dependency-injection) documentation for more details. +See the [Dependency Injection](/docs/hotchocolate/v16/resolvers/dependency-injection) documentation for more details. > Warning: Changing the default scope for queries will likely result in the error "A second operation started on this context before a previous operation completed", because Entity Framework Core does not support multiple parallel operations on the same `DbContext` instance. @@ -227,6 +227,6 @@ Take a look at the annotation-based or code-first example. # Next Steps -- [Dependency Injection](/docs/hotchocolate/v16/fetching-data/dependency-injection) for DI scope configuration +- [Dependency Injection](/docs/hotchocolate/v16/resolvers/dependency-injection) for DI scope configuration - [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for batching patterns - [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) for applying filters to EF Core queries diff --git a/website/src/docs/hotchocolate/v16/guides/federation.md b/website/src/docs/hotchocolate/v16/guides/federation.md index 82041e70bf2..eacb4810026 100644 --- a/website/src/docs/hotchocolate/v16/guides/federation.md +++ b/website/src/docs/hotchocolate/v16/guides/federation.md @@ -416,6 +416,6 @@ For creating a supergraph, see the [Apollo Router documentation](https://www.apo # Next Steps -- [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) for resolver patterns +- [Resolvers](/docs/hotchocolate/v16/resolvers/resolvers) for resolver patterns - [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for batching in reference resolvers - [Apollo Federation docs](https://www.apollographql.com/docs/federation/) for supergraph configuration diff --git a/website/src/docs/hotchocolate/v16/index.md b/website/src/docs/hotchocolate/v16/index.md index fbe69fcf0ab..1b5590df8d8 100644 --- a/website/src/docs/hotchocolate/v16/index.md +++ b/website/src/docs/hotchocolate/v16/index.md @@ -117,7 +117,7 @@ Where you go from here depends on what you need: - **"I want to understand the schema system."** Read [Defining a Schema](/docs/hotchocolate/v16/defining-a-schema). It covers queries, mutations, subscriptions, and all the GraphQL types. -- **"I need to fetch data efficiently."** Go to [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for batching and caching, or [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) for the full resolver API. +- **"I need to fetch data efficiently."** Go to [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader) for batching and caching, or [Resolvers](/docs/hotchocolate/v16/resolvers/resolvers) for the full resolver API. - **"I need to secure my API."** See [Securing Your API](/docs/hotchocolate/v16/security) for authentication, authorization, cost analysis, and trusted documents. diff --git a/website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md b/website/src/docs/hotchocolate/v16/resolvers/dependency-injection.md similarity index 100% rename from website/src/docs/hotchocolate/v16/fetching-data/dependency-injection.md rename to website/src/docs/hotchocolate/v16/resolvers/dependency-injection.md diff --git a/website/src/docs/hotchocolate/v16/fetching-data/errors.md b/website/src/docs/hotchocolate/v16/resolvers/errors.md similarity index 100% rename from website/src/docs/hotchocolate/v16/fetching-data/errors.md rename to website/src/docs/hotchocolate/v16/resolvers/errors.md diff --git a/website/src/docs/hotchocolate/v16/fetching-data/field-middleware.md b/website/src/docs/hotchocolate/v16/resolvers/field-middleware.md similarity index 98% rename from website/src/docs/hotchocolate/v16/fetching-data/field-middleware.md rename to website/src/docs/hotchocolate/v16/resolvers/field-middleware.md index 65d311c79da..35f12ed0b6f 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/field-middleware.md +++ b/website/src/docs/hotchocolate/v16/resolvers/field-middleware.md @@ -280,6 +280,6 @@ descriptor # Next Steps -- Learn more about [resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) for field resolution +- Learn more about [resolvers](/docs/hotchocolate/v16/resolvers/resolvers) for field resolution - Explore [filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [sorting](/docs/hotchocolate/v16/fetching-data/sorting) middleware - Read about [pagination](/docs/hotchocolate/v16/fetching-data/pagination) middleware diff --git a/website/src/docs/hotchocolate/v16/resolvers/index.md b/website/src/docs/hotchocolate/v16/resolvers/index.md new file mode 100644 index 00000000000..d8f379206f4 --- /dev/null +++ b/website/src/docs/hotchocolate/v16/resolvers/index.md @@ -0,0 +1,43 @@ +--- +title: Overview +--- + +Every field in a GraphQL schema is backed by a resolver function that produces the field's value. Understanding how resolvers compose into a tree is the key mental model for building efficient GraphQL APIs with Hot Chocolate. + +# The Resolver Tree + +When Hot Chocolate receives a query, it builds a resolver tree that mirrors the shape of the request. Consider this query: + +```graphql +query { + me { + name + company { + id + name + } + } +} +``` + +This produces the following resolver tree: + +```mermaid +graph LR + A(query: QueryType) --> B(me: UserType) + B --> C(name: StringType) + B --> D(company: CompanyType) + D --> E(id: IdType) + D --> F(name: StringType) +``` + +The execution engine traverses this tree starting from root resolvers. A child resolver can only execute after its parent has produced a value. Sibling resolvers at the same level run in parallel. Because of this parallel execution, resolvers (except top-level mutation fields) must be free of side effects. + +Execution completes when every resolver in the tree has produced a result. + +# What's in This Section + +- [Resolvers](/docs/hotchocolate/v16/resolvers/resolvers) covers how to define resolvers, handle arguments, access parent values, and work with async operations. +- [Dependency Injection](/docs/hotchocolate/v16/resolvers/dependency-injection) explains how services are injected into resolvers, scoping behavior, keyed services, and switching the service provider. +- [Errors](/docs/hotchocolate/v16/resolvers/errors) covers how exceptions become GraphQL errors, error filters for mapping domain exceptions, and throwing `GraphQLException` for explicit errors. +- [Field Middleware](/docs/hotchocolate/v16/resolvers/field-middleware) shows how to build reusable middleware that runs before or after resolvers, including ordering, class-based middleware, and attribute-based middleware. diff --git a/website/src/docs/hotchocolate/v16/fetching-data/resolvers.md b/website/src/docs/hotchocolate/v16/resolvers/resolvers.md similarity index 98% rename from website/src/docs/hotchocolate/v16/fetching-data/resolvers.md rename to website/src/docs/hotchocolate/v16/resolvers/resolvers.md index f965966fb4d..a6d44f09b8c 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/resolvers.md +++ b/website/src/docs/hotchocolate/v16/resolvers/resolvers.md @@ -229,7 +229,7 @@ public class Query } ``` -[Learn more about dependency injection](/docs/hotchocolate/v16/fetching-data/dependency-injection) +[Learn more about dependency injection](/docs/hotchocolate/v16/resolvers/dependency-injection) # Accessing parent values diff --git a/website/src/docs/hotchocolate/v16/server/global-state.md b/website/src/docs/hotchocolate/v16/server/global-state.md index 9ff247be271..31e7057b20c 100644 --- a/website/src/docs/hotchocolate/v16/server/global-state.md +++ b/website/src/docs/hotchocolate/v16/server/global-state.md @@ -126,4 +126,4 @@ Take a look at the implementation-first or code-first example. # Next Steps - [Interceptors](/docs/hotchocolate/v16/server/interceptors) for initializing state before request execution. -- [Dependency Injection](/docs/hotchocolate/v16/fetching-data/dependency-injection) for injecting services into resolvers. +- [Dependency Injection](/docs/hotchocolate/v16/resolvers/dependency-injection) for injecting services into resolvers. diff --git a/website/src/docs/hotchocolate/v16/server/index.md b/website/src/docs/hotchocolate/v16/server/index.md index 1de9d972dcc..8e9e09cd68a 100644 --- a/website/src/docs/hotchocolate/v16/server/index.md +++ b/website/src/docs/hotchocolate/v16/server/index.md @@ -34,7 +34,7 @@ For WebSockets, the interceptor also handles lifecycle events such as when a cli Hot Chocolate recognizes services registered in your DI container and injects them into resolvers automatically. Services are resolved implicitly without requiring the `[Service]` attribute. -[Learn more about dependency injection](/docs/hotchocolate/v16/fetching-data/dependency-injection) +[Learn more about dependency injection](/docs/hotchocolate/v16/resolvers/dependency-injection) # Warmup diff --git a/website/src/docs/hotchocolate/v16/server/interceptors.md b/website/src/docs/hotchocolate/v16/server/interceptors.md index 2f947b95a36..682ba716410 100644 --- a/website/src/docs/hotchocolate/v16/server/interceptors.md +++ b/website/src/docs/hotchocolate/v16/server/interceptors.md @@ -343,5 +343,5 @@ requestBuilder.AllowIntrospection(); # Next Steps - [Global State](/docs/hotchocolate/v16/server/global-state) for sharing per-request data between resolvers. -- [Dependency Injection](/docs/hotchocolate/v16/fetching-data/dependency-injection) for details on service injection and switching providers. +- [Dependency Injection](/docs/hotchocolate/v16/resolvers/dependency-injection) for details on service injection and switching providers. - [Introspection](/docs/hotchocolate/v16/security/introspection) for controlling introspection on a per-request basis. diff --git a/website/src/docs/hotchocolate/v16/server/options.md b/website/src/docs/hotchocolate/v16/server/options.md index cf31c90d96a..10498da0990 100644 --- a/website/src/docs/hotchocolate/v16/server/options.md +++ b/website/src/docs/hotchocolate/v16/server/options.md @@ -264,7 +264,7 @@ builder # Next Steps -- [Field middleware](/docs/hotchocolate/v16/fetching-data/field-middleware) for pipeline configuration +- [Field middleware](/docs/hotchocolate/v16/resolvers/field-middleware) for pipeline configuration - [Cache Control](/docs/hotchocolate/v16/server/cache-control) for CDN and HTTP caching behavior - [Pagination](/docs/hotchocolate/v16/fetching-data/pagination) for paging setup - [Persisted operations](/docs/hotchocolate/v16/performance/trusted-documents) for operation caching From ef75c0ba3a9a3dc4a7780d7c9de46360847f0340 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 12 May 2026 23:33:03 -0400 Subject: [PATCH 6/6] edits --- .../articles/doc-article-navigation.tsx | 41 +- website/src/docs/docs.json | 56 +-- .../v16/fetching-data/batching/index.md | 80 +-- .../v16/fetching-data/pagination.md | 458 ++++++++++++------ .../v16/fetching-data/projections.md | 353 ++++++++------ .../docs/hotchocolate/v16/resolvers/index.md | 289 ++++++++++- .../hotchocolate/v16/resolvers/resolvers.md | 330 ------------- 7 files changed, 845 insertions(+), 762 deletions(-) delete mode 100644 website/src/docs/hotchocolate/v16/resolvers/resolvers.md diff --git a/website/src/components/articles/doc-article-navigation.tsx b/website/src/components/articles/doc-article-navigation.tsx index 676c1fc8ce2..6977662bcb6 100644 --- a/website/src/components/articles/doc-article-navigation.tsx +++ b/website/src/components/articles/doc-article-navigation.tsx @@ -42,6 +42,10 @@ interface DocArticleNavigationProduct { items?: Array<{ path?: string | null; title?: string | null; + items?: Array<{ + path?: string | null; + title?: string | null; + } | null> | null; } | null> | null; } | null> | null; } | null> | null; @@ -110,21 +114,28 @@ export const DocArticleNavigation: FC = ({ const hasVersions = !activeProduct?.versions || activeProduct.versions.length > 1; - const subItems: Item[] = - activeVersion?.items - ?.filter((item) => !!item) - .map((item) => ({ - path: item!.path!, - title: item!.title!, - items: item!.items - ? item?.items - .filter((item) => !!item) - .map((item) => ({ - path: item!.path!, - title: item!.title!, - })) - : undefined, - })) ?? []; + function mapItems( + raw: + | Array<{ + path?: string | null; + title?: string | null; + items?: any; + } | null> + | null + | undefined + ): Item[] { + return ( + raw + ?.filter((item) => !!item) + .map((item) => ({ + path: item!.path!, + title: item!.title!, + items: item!.items ? mapItems(item!.items) : undefined, + })) ?? [] + ); + } + + const subItems: Item[] = mapItems(activeVersion?.items); const basePath = `/docs/${activeProduct!.path!}${ !!activeVersion?.path?.length ? "/" + activeVersion.path : "" diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index 034864e42e6..d9d653ec01f 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -566,11 +566,7 @@ "items": [ { "path": "index", - "title": "Overview" - }, - { - "path": "resolvers", - "title": "Resolvers" + "title": "Introduction" }, { "path": "dependency-injection", @@ -632,10 +628,6 @@ "path": "integrations", "title": "Integrations", "items": [ - { - "path": "index", - "title": "Overview" - }, { "path": "entity-framework", "title": "Entity Framework" @@ -656,52 +648,6 @@ } ] }, - { - "path": "guides", - "title": "Guides", - "items": [ - { - "path": "public-api", - "title": "Building a Public API" - }, - { - "path": "private-api", - "title": "Building a Private API" - }, - { - "path": "error-handling", - "title": "Error Handling" - }, - { - "path": "schema-evolution", - "title": "Schema Evolution" - }, - { - "path": "testing", - "title": "Testing" - }, - { - "path": "performance", - "title": "Performance" - }, - { - "path": "dynamic-schemas", - "title": "Dynamic Schemas" - }, - { - "path": "mcp-adapter", - "title": "MCP Adapter" - }, - { - "path": "openapi-adapter", - "title": "OpenAPI Adapter" - }, - { - "path": "federation", - "title": "Federation" - } - ] - }, { "path": "server", "title": "Server", diff --git a/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md b/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md index 89845fa0c21..4d867e79b18 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/batching/index.md @@ -2,82 +2,4 @@ title: Overview --- -Every field in a GraphQL schema is backed by a resolver function that produces the field's value. Understanding how resolvers compose into a tree is the key mental model for building efficient GraphQL APIs with Hot Chocolate. - -# The Resolver Tree - -When Hot Chocolate receives a query, it builds a resolver tree that mirrors the shape of the request. Consider this query: - -```graphql -query { - me { - name - company { - id - name - } - } -} -``` - -This produces the following resolver tree: - -```mermaid -graph LR - A(query: QueryType) --> B(me: UserType) - B --> C(name: StringType) - B --> D(company: CompanyType) - D --> E(id: IdType) - D --> F(name: StringType) -``` - -The execution engine traverses this tree starting from root resolvers. A child resolver can only execute after its parent has produced a value. Sibling resolvers at the same level run in parallel. Because of this parallel execution, resolvers (except top-level mutation fields) must be free of side effects. - -Execution completes when every resolver in the tree has produced a result. - -# Resolvers - -Resolvers are the building blocks of data fetching. A resolver can call a database, a REST API, a gRPC service, or any other data source. In Hot Chocolate, the source generator is the primary way to define resolvers. You write plain C# methods and the generator wires them into the schema. - -[Learn more about resolvers](/docs/hotchocolate/v16/resolvers/resolvers) - -# DataLoader - -DataLoaders deduplicate and batch requests to data sources. When multiple resolvers request the same entity in a single request, a DataLoader ensures only one call goes to the backing store. DataLoaders can significantly reduce the load on your databases and services. - -[Learn more about DataLoaders](/docs/hotchocolate/v16/fetching-data/batching/dataloader) - -# Pagination - -Hot Chocolate provides cursor-based connection pagination out of the box. Connections follow the [Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm), giving clients a standardized way to page through large datasets. When backed by `IQueryable`, pagination translates directly to native database queries. - -[Learn more about pagination](/docs/hotchocolate/v16/fetching-data/pagination) - -# Filtering - -When you return a list of entities, clients often need to filter them by operations like `equals`, `contains`, or `startsWith`. Hot Chocolate generates the necessary filter input types from your .NET models and translates applied filters into native database queries. - -[Learn more about filtering](/docs/hotchocolate/v16/fetching-data/filtering) - -# Sorting - -Hot Chocolate generates sort input types from your .NET models, allowing clients to specify which fields to sort by and in which direction. Like filtering, sort operations translate to native database queries when backed by `IQueryable`. - -[Learn more about sorting](/docs/hotchocolate/v16/fetching-data/sorting) - -# Projections - -Projections optimize database queries by selecting only the columns that match the fields requested in the GraphQL query. If a client requests `name` and `id`, Hot Chocolate queries only those columns from the database. - -[Learn more about projections](/docs/hotchocolate/v16/fetching-data/projections) - -# Data Sources - -Hot Chocolate is not bound to a specific database or architecture. You can fetch data from any source in your resolvers. We provide specific guidance for the most common patterns: - -# Next Steps - -- **New to resolvers?** Start with [Resolvers](/docs/hotchocolate/v16/resolvers/resolvers). -- **Need to batch data access?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/batching/dataloader). -- **Need to page through lists?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination). -- **Need to filter or sort?** See [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [Sorting](/docs/hotchocolate/v16/fetching-data/sorting). +Under Construction diff --git a/website/src/docs/hotchocolate/v16/fetching-data/pagination.md b/website/src/docs/hotchocolate/v16/fetching-data/pagination.md index bcc8c421308..951e328bbc5 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/pagination.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/pagination.md @@ -2,11 +2,15 @@ title: "Pagination" --- -When a dataset is too large to return in a single response, you need pagination. Hot Chocolate implements cursor-based connection pagination following the [Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm). Connections give clients a standardized way to traverse pages using opaque cursors, and they translate directly to efficient database queries when backed by `IQueryable`. +When a dataset is too large to return in a single response, you need pagination. Hot Chocolate implements cursor-based connection pagination following the [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). Connections give clients a standardized way to traverse pages using opaque cursors. + +GraphQL models data as a graph of related entities. When one entity relates to a list of other entities, that relationship is called a _connection_. A `UsersConnection` for instance represents the connection between `Query` and `User`. Each _edge_ in that connection links one `User` to the parent, and carries a cursor that marks the user's position in the list. + +This is more than just naming. Traditional offset pagination (`skip: 20, take: 10`) breaks when data changes between pages: inserts and deletes shift items, causing duplicates or gaps. Cursors avoid this because they point to a stable position rather than a numeric offset. The database can seek directly to the cursor position, which also means pagination performance stays constant regardless of how deep into the list the client navigates. # How Connections Work -Instead of returning a flat list, a paginated field returns a Connection. The connection wraps the data with page metadata and cursors for navigation. +Instead of returning a flat list, a paginated field returns a Connection. The connection wraps the data with page metadata, cursors for navigation and optionally aggregations. ```graphql type Query { @@ -36,87 +40,36 @@ Clients use `first`/`after` to page forward and `last`/`before` to page backward # Adding Pagination -Apply the `[UsePaging]` attribute to a resolver that returns `IEnumerable` or `IQueryable`. The middleware handles slicing the result, computing cursors, and building the `PageInfo`. - -```csharp -[QueryType] -public static partial class UserQueries -{ - [UsePaging] - public static IQueryable GetUsers(CatalogContext db) - => db.Users.OrderBy(u => u.Id); -} -``` - - - +To use pagination register the paging arguments with the GraphQL builder. ```csharp -public class UserQueriesType : ObjectType -{ - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .Field("users") - .UsePaging() - .Resolve(context => - { - var db = context.Service(); - return db.Users.OrderBy(u => u.Id); - }); - } -} +builder + .AddGraphQL() + .AddPagingArguments(); ``` - - - -When backed by `IQueryable`, the pagination operations translate directly to native database queries. Hot Chocolate does not load the entire dataset into memory. - -# The Connection<T> Type - -When you need full control over the pagination process, return a `Connection` from your resolver. This is useful when you build cursors from an external API, implement a custom data source, or need to control the page info values. - - - +Hot Chocolate by default builds on top of the `Page` which describes a single page in a dataset. A page can be used to construct a `PageConnection`. ```csharp [QueryType] public static partial class UserQueries { - [UsePaging] - public static async Task> GetUsersAsync( - string? after, - int? first, - UserService userService, - CancellationToken ct) - { - var result = await userService.GetUsersPageAsync(after, first, ct); - - var edges = result.Items - .Select(u => new Edge(u, u.Id.ToString())) - .ToList(); - - var pageInfo = new ConnectionPageInfo( - result.HasNextPage, - result.HasPreviousPage, - edges.FirstOrDefault()?.Cursor, - edges.LastOrDefault()?.Cursor); - - return new Connection( - edges, - pageInfo, - totalCount: _ => ValueTask.FromResult(result.TotalCount)); - } + public static async Task> GetUsersAsync( + PagingArguments pagingArgs, + CatalogContext db, + CancellationToken cancellationToken) + => await db.Users.OrderBy(u => u.Id).ToPageAsync(pagingArgs, cancellationToken); } ``` +To use connection-based pagination with code-first, use the `ToPageAsync` extension and map the resulting page to a `Connection`. + ```csharp public class UserQueriesType : ObjectType { @@ -127,26 +80,10 @@ public class UserQueriesType : ObjectType .UsePaging() .Resolve(async context => { - var after = context.ArgumentValue("after"); - var first = context.ArgumentValue("first"); - var userService = context.Service(); - - var result = await userService.GetUsersPageAsync(after, first); - - var edges = result.Items - .Select(u => new Edge(u, u.Id.ToString())) - .ToList(); - - var pageInfo = new ConnectionPageInfo( - result.HasNextPage, - result.HasPreviousPage, - edges.FirstOrDefault()?.Cursor, - edges.LastOrDefault()?.Cursor); - - return new Connection( - edges, - pageInfo, - totalCount: _ => ValueTask.FromResult(result.TotalCount)); + var db = context.Service(); + return await db.Users.OrderBy(u => u.Id) + .ToPageAsync(pagingArgs, context.RequestAborted) + .ToConnectionAsync(); }); } } @@ -155,6 +92,13 @@ public class UserQueriesType : ObjectType +The `ToPageAsync` extension method is located in one of the following packages: + +- GreenDonut.Data.EntityFramework +- GreenDonut.Data.Raven +- GreenDonut.Data.Marten +- GreenDonut.Data.Mongo + # Pagination Options You can configure pagination behavior per field or globally. @@ -168,9 +112,12 @@ You can configure pagination behavior per field or globally. [QueryType] public static partial class UserQueries { - [UsePaging(MaxPageSize = 100, DefaultPageSize = 25, IncludeTotalCount = true)] - public static IQueryable GetUsers(CatalogContext db) - => db.Users.OrderBy(u => u.Id); + [UseConnection(MaxPageSize = 100, DefaultPageSize = 25, IncludeTotalCount = true)] + public static async Task> GetUsersAsync( + PagingArguments pagingArgs, + CatalogContext db, + CancellationToken cancellationToken) + => await db.Users.OrderBy(u => u.Id).ToPageAsync(pagingArgs, cancellationToken); } ``` @@ -180,7 +127,7 @@ public static partial class UserQueries ```csharp descriptor .Field("users") - .UsePaging(options: new PagingOptions + .UsePaging(new PagingOptions { MaxPageSize = 100, DefaultPageSize = 25, @@ -235,9 +182,16 @@ Override the name with `ConnectionName`: ```csharp -[UsePaging(ConnectionName = "TeamMembers")] -public static IQueryable GetUsers(CatalogContext db) - => db.Users.OrderBy(u => u.Id); +[QueryType] +public static partial class UserQueries +{ + [UseConnection(ConnectionName = "TeamMembers")] + public static async Task> GetUsersAsync( + PagingArguments pagingArgs, + CatalogContext db, + CancellationToken cancellationToken) + => await db.Users.OrderBy(u => u.Id).ToPageAsync(pagingArgs, cancellationToken); +} ``` This produces `TeamMembersConnection` and `TeamMembersEdge`. @@ -262,9 +216,16 @@ Enable the `totalCount` field to let clients request the total number of items i ```csharp -[UsePaging(IncludeTotalCount = true)] -public static IQueryable GetUsers(CatalogContext db) - => db.Users.OrderBy(u => u.Id); +[QueryType] +public static partial class UserQueries +{ + [UseConnection(IncludeTotalCount = true)] + public static async Task> GetUsersAsync( + PagingArguments pagingArgs, + CatalogContext db, + CancellationToken cancellationToken) + => await db.Users.OrderBy(u => u.Id).ToPageAsync(pagingArgs, cancellationToken); +} ``` @@ -279,91 +240,310 @@ descriptor -When your resolver returns `IEnumerable` or `IQueryable`, the total count is computed automatically. When you return a `Connection`, provide the count through the `totalCount` delegate: +# Relative Cursors + +Cursor-based pagination is great for infinite scrolling, but many applications need a traditional page bar that lets users jump to a specific page (e.g. "1 2 3 ... 10"). Relative cursors bridge this gap. They let you request cursors for surrounding pages so the frontend can render a page bar while still using cursor-based navigation under the hood. + +```text + [1] 2 3 4 5 ... 10 + ↑ ↑ ↑ ↑ + forward cursors +``` + +When a client requests `forwardCursors` or `backwardCursors` inside `pageInfo`, Hot Chocolate returns a list of `PageCursor` objects, each containing a `page` number and the opaque `cursor` to navigate there. The frontend can render these directly as page links. + +Enable relative cursors on a field with `EnableRelativeCursors`: + +```csharp +[QueryType] +public static partial class UserQueries +{ + [UseConnection(EnableRelativeCursors = true)] + public static async Task> GetUsersAsync( + PagingArguments pagingArgs, + CatalogContext db, + CancellationToken cancellationToken) + => await db.Users.OrderBy(u => u.Id).ToPageAsync(pagingArgs, cancellationToken); +} +``` + +Clients can then query the relative cursors: + +```graphql +query { + users(first: 10) { + nodes { + id + name + } + pageInfo { + hasNextPage + hasPreviousPage + forwardCursors { + page + cursor + } + backwardCursors { + page + cursor + } + } + } +} +``` + +The response includes cursors for surrounding pages: + +```json +{ + "data": { + "users": { + "nodes": [ ... ], + "pageInfo": { + "hasNextPage": true, + "hasPreviousPage": false, + "forwardCursors": [ + { "page": 2, "cursor": "ezB8MXw2fTIz" }, + { "page": 3, "cursor": "ezF8MXw2fTIz" }, + { "page": 4, "cursor": "ezJ8MXw2fTIz" } + ], + "backwardCursors": [] + } + } + } +} +``` + +To navigate to page 3, the client sends `users(first: 10, after: "ezF8MXw2fTIz")`. By default, up to 5 cursors are returned per direction. + +You can also enable relative cursors globally: ```csharp -var connection = new Connection( - edges, - pageInfo, - totalCount: ct => ValueTask.FromResult(totalItems)); +builder + .AddGraphQL() + .ModifyPagingOptions(opt => + { + opt.EnableRelativeCursors = true; + }); ``` -# Extending Connection and Edge Types +> Relative cursors are only available with the implementation-first approach. -Add fields to a Connection or Edge type using type extensions. This is useful for aggregation fields or metadata. +# Custom Connection Types + +## Extending PageConnection + +The simplest way to add fields to a connection is to inherit from `PageConnection`. Any public property or method you add becomes a GraphQL field on the connection type. ```csharp -[ExtendObjectType("UsersConnection")] -public class UsersConnectionExtension +public class ProductConnection : PageConnection { - public double GetAverageAge([Parent] Connection connection) + private readonly Page _page; + + public ProductConnection(Page page) : base(page) { - return connection.Edges.Average(e => e.Node.Age); + _page = page; } + + public decimal AveragePrice => _page.Average(p => p.Price); } ``` +Return the custom connection from your resolver instead of `PageConnection`: + ```csharp -[ExtendObjectType("UsersEdge")] -public class UsersEdgeExtension +[QueryType] +public static partial class ProductQueries { - public int GetIndex([Parent] Edge edge) + [UseConnection(IncludeTotalCount = true)] + public static async Task GetProductsAsync( + PagingArguments pagingArgs, + CatalogContext db, + CancellationToken cancellationToken) { - // Custom edge field logic - return int.Parse(edge.Cursor); + var page = await db.Products + .OrderBy(p => p.Id) + .ToPageAsync(pagingArgs, cancellationToken); + + return new ProductConnection(page); } } ``` -> If you use [projections](/docs/hotchocolate/v16/fetching-data/projections), some properties on your model may not be populated depending on what the client requested. +## ConnectionBase for Full Control -# Nullable Cursor Keys +When you need custom edge types or want to control how edges and page info are constructed, inherit from `ConnectionBase` directly. -When your cursor key field can be `null`, you must tell Hot Chocolate how the database orders null values so that cursor-based pagination produces correct results across pages. +Start by defining a custom edge. An edge implements `IEdge` and pairs a node with its cursor. -Set `NullOrdering` on `PagingOptions` to match your database: +```csharp +public class ProductsEdge(Page page, PageEntry entry) : IEdge +{ + public Product Node => entry.Item; -| Value | When to use | -| ------------------ | ------------------------------------------------------------------------------ | -| `Unspecified` | Default. The EF Core paging handler auto-detects ordering for known providers. | -| `NativeNullsFirst` | Nulls sort before non-null values (SQL Server, SQLite, in-memory LINQ). | -| `NativeNullsLast` | Nulls sort after non-null values (PostgreSQL default). | + object? IEdge.Node => Node; + + public string Cursor => page.CreateCursor(entry); +} +``` + +Then build the connection around it: ```csharp -builder - .AddGraphQL() - .ModifyPagingOptions(opt => opt.NullOrdering = NullOrdering.NativeNullsLast); +public class ProductConnection : ConnectionBase +{ + private readonly Page _page; + private ConnectionPageInfo? _pageInfo; + private ProductsEdge[]? _edges; + + public ProductConnection(Page page) + { + _page = page; + } + + public override IReadOnlyList? Edges + { + get + { + if (_edges is null) + { + var entries = _page.Entries; + var edges = new ProductsEdge[entries.Length]; + + for (var i = 0; i < entries.Length; i++) + { + edges[i] = new ProductsEdge(_page, entries[i]); + } + + _edges = edges; + } + + return _edges; + } + } + + public IReadOnlyList? Nodes => _page; + + public override ConnectionPageInfo PageInfo + { + get + { + if (_pageInfo is null) + { + var startCursor = _page.CreateStartCursor(); + var endCursor = _page.CreateEndCursor(); + + _pageInfo = new ConnectionPageInfo( + _page.HasNextPage, _page.HasPreviousPage, + startCursor, endCursor); + } + + return _pageInfo; + } + } + + public int TotalCount => _page.TotalCount ?? 0; +} ``` -When `NullOrdering` is `Unspecified` and the EF Core paging handler is used, ordering is detected automatically for PostgreSQL (`NativeNullsLast`) and SQL Server, SQLite, and in-memory (`NativeNullsFirst`). For unrecognized providers, an error is thrown when nullable cursor keys are present. Set `NullOrdering` explicitly to resolve it. +## Reusable Generic Connection + +If multiple entities share the same connection structure, define a generic connection and edge. Use the `[GraphQLName("{0}Connection")]` attribute so Hot Chocolate replaces `{0}` with the entity name (e.g. `CatalogConnection` becomes `BrandConnection`). + +```csharp +[GraphQLName("{0}Edge")] +public class CatalogEdge( + Page page, + PageEntry entry) : IEdge +{ + public TEntity Node => entry.Item; -# Pagination Providers + object? IEdge.Node => Node; -The `UsePaging` middleware provides a unified API that adapts to different data sources through pagination providers. The default provider supports `IEnumerable` and `IQueryable`. Other providers handle specific databases like MongoDB. + public string Cursor => page.CreateCursor(entry); +} +``` ```csharp -builder - .AddGraphQL() - .AddMongoDbPagingProviders(); +[GraphQLName("{0}Connection")] +public class CatalogConnection + : ConnectionBase, ConnectionPageInfo> +{ + private readonly Page _page; + private ConnectionPageInfo? _pageInfo; + private CatalogEdge[]? _edges; + + public CatalogConnection(Page page) + { + _page = page; + } + + public override IReadOnlyList> Edges + { + get + { + if (_edges is null) + { + var entries = _page.Entries; + var edges = new CatalogEdge[entries.Length]; + + for (var i = 0; i < entries.Length; i++) + { + edges[i] = new CatalogEdge(_page, entries[i]); + } + + _edges = edges; + } + + return _edges; + } + } + + public IReadOnlyList Nodes => _page; + + public override ConnectionPageInfo PageInfo + { + get + { + if (_pageInfo is null) + { + var startCursor = _page.CreateStartCursor(); + var endCursor = _page.CreateEndCursor(); + + _pageInfo = new ConnectionPageInfo( + _page.HasNextPage, _page.HasPreviousPage, + startCursor, endCursor); + } + + return _pageInfo; + } + } + + public int TotalCount => _page.TotalCount ?? 0; +} ``` -Name a provider to reference it explicitly on specific fields: +# Nullable Cursor Keys + +When your cursor key field can be `null`, you must tell Hot Chocolate how the database orders null values so that cursor-based pagination produces correct results across pages. + +Set `NullOrdering` on `PagingOptions` to match your database: + +| Value | When to use | +| ------------------ | ------------------------------------------------------------------------------ | +| `Unspecified` | Default. The EF Core paging handler auto-detects ordering for known providers. | +| `NativeNullsFirst` | Nulls sort before non-null values (SQL Server, SQLite, in-memory LINQ). | +| `NativeNullsLast` | Nulls sort after non-null values (PostgreSQL default). | ```csharp builder .AddGraphQL() - .AddMongoDbPagingProviders(providerName: "MongoDB"); -``` - -```csharp -[UsePaging(ProviderName = "MongoDB")] -public static IExecutable GetUsers(IMongoCollection collection) - => collection.AsExecutable(); + .ModifyPagingOptions(opt => opt.NullOrdering = NullOrdering.NativeNullsLast); ``` -If no `ProviderName` is specified, the correct provider is selected based on the return type. If it cannot be inferred, the first registered provider is used. +When `NullOrdering` is `Unspecified` and the EF Core paging handler is used, ordering is detected automatically for PostgreSQL (`NativeNullsLast`) and SQL Server, SQLite, and in-memory (`NativeNullsFirst`). For unrecognized providers, an error is thrown when nullable cursor keys are present. Set `NullOrdering` explicitly to resolve it. -[Learn more about database integrations](/docs/hotchocolate/v16/integrations) +[Learn more about database integrations](/docs/hotchocolate/v16/fetching-data/integrations) # Next Steps diff --git a/website/src/docs/hotchocolate/v16/fetching-data/projections.md b/website/src/docs/hotchocolate/v16/fetching-data/projections.md index 5bd891606d7..40d3505ee1b 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/projections.md +++ b/website/src/docs/hotchocolate/v16/fetching-data/projections.md @@ -21,9 +21,7 @@ FROM "Users" AS "u" LEFT JOIN "Address" AS "a" ON "u"."AddressId" = "a"."Id" ``` -Projections operate on `IQueryable` by default. Custom providers can extend this to other data sources. - -> Projections require a public setter on fields they operate on. Without a public setter, the default-constructed value is returned. +In Hot Chocolate v16, `QueryContext` is the recommended way to apply projections. It combines projection, filtering, and sorting into a single parameter that Hot Chocolate injects into your resolver automatically. You apply it to your `IQueryable` with the `.With()` extension method, giving you full control over your data pipeline. # Getting Started @@ -31,233 +29,306 @@ Projections are part of the `HotChocolate.Data` package. -Register projections on the schema: +Register filtering and sorting on the schema. This also registers `QueryContext` support automatically: ```csharp builder .AddGraphQL() - .AddProjections(); + .AddFiltering() + .AddSorting(); ``` -Apply the `[UseProjection]` attribute to a resolver that returns `IQueryable`: - - - +Add a `QueryContext` parameter to your resolver. Hot Chocolate constructs it at runtime from the GraphQL selection set, filter arguments, and sort arguments: ```csharp [QueryType] -public static partial class UserQueries -{ - [UseProjection] - public static IQueryable GetUsers(CatalogContext db) - => db.Users; -} -``` - - - - -```csharp -public class UserQueries +public static partial class ProductQueries { - public IQueryable GetUsers(CatalogContext db) - => db.Users; -} - -public class UserQueriesType : ObjectType -{ - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor.Field(f => f.GetUsers(default!)).UseProjection(); - } + [UseFiltering] + [UseSorting] + public static async Task> GetProductsAsync( + PagingArguments pagingArgs, + QueryContext query, + CatalogContext db, + CancellationToken cancellationToken) + => await db.Products + .With(query) + .ToPageAsync(pagingArgs, cancellationToken); } ``` - - - -The projection middleware creates a `Select` expression for the entire subtree of the field. Fields with custom resolvers are not projected to the database. If the middleware encounters a nested field that also specifies `UseProjection()`, that field is handled separately. +The `[UseFiltering]` and `[UseSorting]` attributes generate the `where` and `order` arguments in the schema. The `QueryContext` parameter receives the projection selector (from the GraphQL selection set), the filter predicate, and the sort definition. Calling `.With(query)` applies all three to the `IQueryable` in the correct order: filter, sort, then project. -> **Middleware order matters.** When combining multiple middleware, apply them in this order: `UsePaging` > `UseProjection` > `UseFiltering` > `UseSorting`. +# How QueryContext Works -# QueryContext<T> Pattern +`QueryContext` is a simple record with three properties: -`QueryContext` provides an alternative to the `[UseProjection]` middleware. Instead of applying projections as middleware, you return a `QueryContext` from your resolver and Hot Chocolate applies projections, filtering, and sorting at execution time. +| Property | Type | Source | +| ----------- | ---------------------------- | ------------------------------------- | +| `Selector` | `Expression>?` | Built from the GraphQL selection set | +| `Predicate` | `Expression>?` | Built from `[UseFiltering]` arguments | +| `Sorting` | `SortDefinition?` | Built from `[UseSorting]` arguments | -```csharp -[QueryType] -public static partial class UserQueries -{ - public static QueryContext GetUsers(CatalogContext db) - => db.Users.AsQueryContext(); -} -``` +When you call `.With(queryContext)` on an `IQueryable`, it applies these in order: -`QueryContext` integrates projection, filtering, and sorting into a single return type. This can reduce middleware stacking and make your resolver signatures cleaner. +1. **Filter** the data with `Where(predicate)` +2. **Sort** the results with `OrderBy(sorting)` +3. **Project** only the requested columns with `Select(selector)` -## HC0099 Analyzer Warning +This order is important for query efficiency. Filtering first reduces the dataset, sorting arranges the filtered results, and projecting last ensures only the needed columns are selected. -Do not combine `QueryContext` with `[UseProjection]` on the same field. The HC0099 analyzer warns when both are present because they conflict: each tries to apply its own `Select` expression, leading to unexpected behavior or runtime errors. +# Default Sort Order -**Incorrect:** +When users don't provide explicit sort arguments, you often want a stable default order (for example, for pagination). The `.With()` method accepts an optional sort modifier: ```csharp -// This triggers HC0099 -[UseProjection] -public static QueryContext GetUsers(CatalogContext db) - => db.Users.AsQueryContext(); +public async Task> GetProductsAsync( + PagingArguments pagingArgs, + QueryContext? query = null, + CancellationToken cancellationToken = default) + => await context.Products + .With(query, DefaultOrder) + .ToPageAsync(pagingArgs, cancellationToken); + +private static SortDefinition DefaultOrder(SortDefinition sort) + => sort.IfEmpty(o => o.AddDescending(t => t.Name)).AddAscending(t => t.Id); ``` -**Correct:** Use one approach or the other: +The `IfEmpty` method applies the default sort only when the client did not provide a sort argument. The `AddAscending(t => t.Id)` call always appends a tiebreaker to ensure stable cursor-based pagination. -```csharp -// Option 1: QueryContext (handles projections internally) -public static QueryContext GetUsers(CatalogContext db) - => db.Users.AsQueryContext(); - -// Option 2: [UseProjection] middleware -[UseProjection] -public static IQueryable GetUsers(CatalogContext db) - => db.Users; -``` +# Using QueryContext with Services -# Combining with Filtering, Sorting, and Pagination - -Projections work with filtering, sorting, and pagination. Maintain the correct middleware order: +A common pattern is to pass `QueryContext` from your resolver into a service layer. This keeps your resolvers thin and your data access logic reusable: ```csharp [QueryType] -public static partial class UserQueries +public static partial class ProductQueries { - [UsePaging] - [UseProjection] + [UseConnection(IncludeTotalCount = true, EnableRelativeCursors = true)] [UseFiltering] [UseSorting] - public static IQueryable GetUsers(CatalogContext db) - => db.Users; + public static async Task GetProductsAsync( + PagingArguments pagingArgs, + QueryContext query, + ProductService productService, + CancellationToken cancellationToken) + { + var page = await productService.GetProductsAsync( + pagingArgs, query, cancellationToken); + return new ProductConnection(page); + } } ``` -Filtering and sorting can project over relationships. Projections cannot project pagination over relationships. For nested collections that need filtering or sorting, apply those attributes to the collection property: +The service applies `QueryContext` to the EF Core `DbSet`: ```csharp -public class User +public class ProductService(CatalogContext context) { - public int Id { get; set; } - public string Name { get; set; } - - [UseFiltering] - [UseSorting] - public ICollection
Addresses { get; set; } + public async Task> GetProductsAsync( + PagingArguments pagingArgs, + QueryContext? query = null, + CancellationToken cancellationToken = default) + => await context.Products + .With(query, DefaultOrder) + .ToPageAsync(pagingArgs, cancellationToken); + + private static SortDefinition DefaultOrder( + SortDefinition sort) + => sort + .IfEmpty(o => o.AddDescending(t => t.Name)) + .AddAscending(t => t.Id); } ``` -```graphql +Making the `QueryContext` parameter nullable with a default of `null` allows you to call the service from places that don't have a GraphQL context, such as background jobs or unit tests. + +# Using QueryContext with DataLoaders + +`QueryContext` integrates with GreenDonut DataLoaders to enable batched data fetching with projections. Use `.With(query)` on a DataLoader to branch it with the current query context: + +```csharp +public class ProductService( + CatalogContext context, + IProductBatchingContext batchingContext) { - users(where: { name: { eq: "ChilliCream" } }, order: [{ name: DESC }]) { - nodes { - email - addresses(where: { street: { eq: "Sesame Street" } }) { - street - } - } - } + public async Task GetProductByIdAsync( + int id, + QueryContext? query = null, + CancellationToken cancellationToken = default) + => await batchingContext.ProductById + .With(query) + .LoadAsync(id, cancellationToken); } ``` -# FirstOrDefault / SingleOrDefault - -When you want a field to return a single entity instead of a list, use `[UseFirstOrDefault]` or `[UseSingleOrDefault]`. These rewrite the return type from `IQueryable` to `T?` and apply the corresponding LINQ operation: +The DataLoader itself receives `QueryContext` and applies it to the batch query: ```csharp -[QueryType] -public static partial class UserQueries +[DataLoaderGroup("ProductBatchingContext")] +internal static class ProductDataLoader { - [UseFirstOrDefault] - [UseProjection] - [UseFiltering] - public static IQueryable GetUser(CatalogContext db) - => db.Users; + [DataLoader] + public static async Task> GetProductByIdAsync( + IReadOnlyList ids, + QueryContext query, + CatalogContext context, + CancellationToken cancellationToken) + { + ids = ids.EnsureOrdered(); + return await context.Products + .Where(t => ids.Contains(t.Id)) + .With(query) + .ToDictionaryAsync(t => t.Id, cancellationToken); + } } ``` -This produces a schema field that returns a single `User` (or null) instead of a list: +For batched collection DataLoaders (for example, loading products by brand), you can combine `QueryContext` with pagination: -```graphql -type Query { - user(where: UserFilterInput): User +```csharp +[DataLoader] +public static async Task>> + GetProductsByBrandAsync( + IReadOnlyList brandIds, + PagingArguments pagingArgs, + QueryContext query, + CatalogContext context, + CancellationToken cancellationToken) +{ + brandIds = brandIds.EnsureOrdered(); + return await context.Products + .Where(t => brandIds.Contains(t.BrandId)) + .With(query, s => s.AddAscending(t => t.Id)) + .ToBatchPageAsync( + t => t.BrandId, + pagingArgs, + cancellationToken); } ``` -# Always Project Fields - -Resolvers on a type sometimes need data from the parent that the client did not request. Mark a field with `[IsProjected(true)]` to ensure it is always included in the database query: +# Nested Resolvers - - +In object type resolvers, `QueryContext` is typed to the entity being resolved, not the parent. This means each resolver gets the projection, filter, and sort context for its own return type: ```csharp -public class User +[ObjectType] +public static partial class ProductNode { - public int Id { get; set; } - public string Name { get; set; } - [IsProjected(true)] - public string Email { get; set; } - public Address Address { get; set; } + [BindMember(nameof(Product.BrandId))] + public static async Task GetBrandAsync( + [Parent(requires: nameof(Product.BrandId))] Product product, + QueryContext query, + BrandService brandService, + CancellationToken cancellationToken) + => await brandService.GetBrandByIdAsync( + product.BrandId, query, cancellationToken); } ``` - - +For nested connection fields, combine `QueryContext` with `[UseConnection]`, `[UseFiltering]`, and `[UseSorting]`: ```csharp -public class UserType : ObjectType +[ObjectType] +public static partial class BrandNode { - protected override void Configure(IObjectTypeDescriptor descriptor) + [UseConnection(EnableRelativeCursors = true)] + [UseFiltering] + [UseSorting] + public static async Task> GetProductsAsync( + [Parent(requires: nameof(Brand.Id))] Brand brand, + PagingArguments pagingArgs, + QueryContext query, + ProductService productService, + CancellationToken cancellationToken) { - descriptor.Field(f => f.Email).IsProjected(true); + var page = await productService.GetProductsByBrandAsync( + brand.Id, pagingArgs, query, cancellationToken); + return new PageConnection(page); } } ``` - - +# Including Additional Fields -Even if the client does not request `email`, the SQL query includes the `Email` column so that resolvers depending on it have the data they need. +Sometimes a DataLoader or batch resolver needs a field that the client didn't request (for example, the `Id` for dictionary keying). Use `.Include()` to ensure specific properties are always projected: -# Exclude Fields from Projection +```csharp +[BatchResolver] +public static async Task> GetSupplierAsync( + [Parent(requires: nameof(Brand.SupplierId))] List brands, + QueryContext query, + CatalogContext context, + CancellationToken cancellationToken) +{ + var supplierIds = brands + .Select(b => b.SupplierId) + .Distinct() + .ToList(); + + var suppliers = await context.Suppliers + .Where(s => supplierIds.Contains(s.Id)) + .With(query.Include(s => s.Id)) + .ToDictionaryAsync(s => s.Id, cancellationToken); + + return brands + .Select(b => suppliers.GetValueOrDefault(b.SupplierId)) + .ToList(); +} +``` -Use `[IsProjected(false)]` to exclude a field from projection. The field remains in the schema but is not included in the database query: +The `.Include(s => s.Id)` call adds the `Id` property to the projection selector so it is always available for the dictionary key, even if the client did not request it. - - +# Migrating from UseProjection + +If you are migrating from the `[UseProjection]` attribute approach, the key changes are: + +**Before (attribute-based):** ```csharp -public class User +[QueryType] +public static partial class ProductQueries { - public int Id { get; set; } - public string Name { get; set; } - [IsProjected(false)] - public string InternalNotes { get; set; } - public Address Address { get; set; } + [UsePaging] + [UseProjection] + [UseFiltering] + [UseSorting] + public static IQueryable GetProducts(CatalogContext db) + => db.Products; } ``` - - +**After (QueryContext):** ```csharp -public class UserType : ObjectType +[QueryType] +public static partial class ProductQueries { - protected override void Configure(IObjectTypeDescriptor descriptor) + [UseConnection] + [UseFiltering] + [UseSorting] + public static async Task GetProductsAsync( + PagingArguments pagingArgs, + QueryContext query, + CatalogContext db, + CancellationToken cancellationToken) { - descriptor.Field(f => f.InternalNotes).IsProjected(false); + var page = await db.Products + .With(query) + .ToPageAsync(pagingArgs, cancellationToken); + return new ProductConnection(page); } } ``` - - +The main differences: + +- **No `[UseProjection]` attribute.** `QueryContext` handles projections via the `Selector` it receives from the GraphQL selection set. +- **Explicit data pipeline.** You control when and how filtering, sorting, and projection are applied to your query through the `.With()` call. +- **Service layer friendly.** You can pass `QueryContext` into services and DataLoaders, making your data access logic reusable and testable. +- **No middleware ordering concerns.** With `[UseProjection]`, you had to maintain a strict attribute order (`UsePaging` > `UseProjection` > `UseFiltering` > `UseSorting`). With `QueryContext`, the `.With()` method applies operations in the correct order automatically. + +> Do not combine `QueryContext` with `[UseProjection]` on the same field. Each applies its own `Select` expression, leading to unexpected behavior. The HC0099 analyzer warns when both are present. # Next Steps diff --git a/website/src/docs/hotchocolate/v16/resolvers/index.md b/website/src/docs/hotchocolate/v16/resolvers/index.md index d8f379206f4..0d0d21a26f2 100644 --- a/website/src/docs/hotchocolate/v16/resolvers/index.md +++ b/website/src/docs/hotchocolate/v16/resolvers/index.md @@ -1,5 +1,5 @@ --- -title: Overview +title: Introduction --- Every field in a GraphQL schema is backed by a resolver function that produces the field's value. Understanding how resolvers compose into a tree is the key mental model for building efficient GraphQL APIs with Hot Chocolate. @@ -31,13 +31,296 @@ graph LR D --> F(name: StringType) ``` -The execution engine traverses this tree starting from root resolvers. A child resolver can only execute after its parent has produced a value. Sibling resolvers at the same level run in parallel. Because of this parallel execution, resolvers (except top-level mutation fields) must be free of side effects. +The execution engine traverses this tree starting from root resolvers. A child resolver can only execute after its parent has produced a value. Sibling resolvers at the same level run in parallel. Because of this parallel execution, resolvers (except top-level mutation field resolvers) must be free of side effects. Execution completes when every resolver in the tree has produced a result. +# Defining a Resolver + +Resolvers can be defined in a way that should feel very familiar to C# developers, as they either translate to methods or delegates. + + + + +In the implementation-first approach, a public method is automatically inferred as a resolver. This means the method defines both the field in your schema and the logic to resolve its value. + +```csharp +[QueryType] +public partial class Query +{ + public static string Foo() => "Bar"; +} +``` + +This generates the following schema: + +```sdl +type Query { + foo: String! +} +``` + +Resolvers do not have to be methods. Public properties are also inferred as resolvers and exposed as fields in your schema. + +```csharp +[QueryType] +public partial class Query +{ + public static User User => new User("Ted"); +} + +public record User(string Name); +``` + +In this case, the property `Name` of the `User` object is also inferred as a resolver. + + + + +In the code-first approach, you define a resolver by assigning a resolver delegate to a field. This delegate contains the logic for resolving the field's value. + +```csharp +public class QueryType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field("foo") + .Type>() + .Resolve(ctx => "bar"); + } +} +``` + +You can also use `ObjectType` with a backing POCO. Public methods and properties on the POCO are bound as fields automatically. Use the `Field` method with a lambda expression to configure individual fields. + +```csharp +public class Query +{ + public string Foo() => "Bar"; +} + +public class QueryType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(f => f.Foo()) + .Type>(); + } +} +``` + + + + +## Async Resolver + +Resolvers can be synchronous or asynchronous. Most data fetching operations, such as calling a service or database, are asynchronous. + +The most important aspect of async resolvers is to honor the CancellationToken. This allows execution to be cancelled if the client abandons the request, preventing unnecessary work and resource usage. + + + + +When using the implementation-first approach, you can add a `CancellationToken` parameter to your resolver method. The execution engine will automatically inject the request's cancellation token. + +```csharp +public class Query +{ + public async Task GetProductByIdAsync( + int id, + ProductService productService, + CancellationToken cancellationToken) + => await productService.GetAsync(cancellationToken); +} +``` + + + + +When using the code-first approach, you can access the `CancellationToken` through the `IResolverContext` provided to your resolver. + +```csharp +descriptor + .Field("foo") + .Resolve(context => + { + CancellationToken ct = context.RequestAborted; + + // Omitted code for brevity + }); +``` + + + + +# Arguments + +In GraphQL, fields are conceptually similar to methods in C#. Just like methods, fields can have arguments, and you can access these argument values directly in your resolvers. + + + + +When using the implementation-first approach, any parameter in your resolver method that is not a service, a `CancellationToken`, or specially annotated is treated as a GraphQL argument. The execution engine will inject the argument value from the query into these parameters. For example, in the method below, the `id` parameter is recognized as an argument, while `ProductService` is injected as a service from the DI container. + +```csharp +public class Query +{ + public async Task GetProductByIdAsync( + int id, + ProductService productService, + CancellationToken cancellationToken) + => await productService.GetAsync(cancellationToken); +} +``` + + + + +When using the code-first approach, you can access field arguments using the resolver context. + +```csharp +descriptor + .Field("foo") + .Argument("id", a => a.Type>()) + .Resolve(context => + { + var id = context.ArgumentValue("id"); + + // Omitted code for brevity + }); +``` + + + + +[Learn more about arguments](/docs/hotchocolate/v16/defining-a-schema/arguments) + +# Injecting Services + +Hot Chocolate automatically recognizes types registered in the DI container and injects them into resolver parameters. + +```csharp +public class Query +{ + public List GetUsers(UserService userService) + => userService.GetUsers(); +} +``` + +While you can take attributes to annotate services, you do not have to for non-keyed services. + +```csharp +public class Query +{ + public List GetUsers([Service] UserService userService) + => userService.GetUsers(); +} +``` + +[Learn more about dependency injection](/docs/hotchocolate/v16/resolvers/dependency-injection) + +# Accessing parent values + +Each field resolver has access to the value that was resolved for its parent type. + +For example, consider the following schema: + +```sdl +type Query { + me: User!; +} + +type User { + id: ID!; + friends: [User!]!; +} +``` + +The `User` schema type is represented by a `User` runtime class. The `id` field is a property on this class. + +```csharp +public class User +{ + public string Id { get; set; } +} +``` + +The `friends` resolver, by contrast, is independent: it is not declared on the `User` type and uses the user's `Id` to compute its result. +From the `friends` resolver's perspective, the `User` runtime object is its _parent_. + +Access the parent value like this: + + + + +In the implementation-first approach, the parent object can be injected as a resolver parameter: + +```csharp +[ObjectType] +public static partial class UserNode +{ + public static Task> GetFriendsAsync( + [Parent] User user, + UserService userService, + CancellationToken cancellationToken) + { + // Omitted code for brevity + } +} +``` + +If database projections are enabled, the parent object may only contain the fields requested by the client. To ensure the projections engine also loads properties required by the resolver, declare those requirements on the parent parameter: + +```csharp +[ObjectType] +public static partial class UserNode +{ + public static Task> GetFriendsAsync( + [Parent(requires: nameof(User.Id))] User user, + UserService userService, + CancellationToken cancellationToken) + { + // Omitted code for brevity + } +} +``` + +Use `nameof` to make this requirement refactoring-safe. + + + + +In the code-first approach, the parent object is available via the `IResolverContext`. + +```csharp +public class User +{ + public string Id { get; set; } +} + +public class UserType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field("friends") + .Resolve(context => + { + User parent = context.Parent(); + + // Omitted code for brevity + }); + } +} +``` + + + + # What's in This Section -- [Resolvers](/docs/hotchocolate/v16/resolvers/resolvers) covers how to define resolvers, handle arguments, access parent values, and work with async operations. - [Dependency Injection](/docs/hotchocolate/v16/resolvers/dependency-injection) explains how services are injected into resolvers, scoping behavior, keyed services, and switching the service provider. - [Errors](/docs/hotchocolate/v16/resolvers/errors) covers how exceptions become GraphQL errors, error filters for mapping domain exceptions, and throwing `GraphQLException` for explicit errors. - [Field Middleware](/docs/hotchocolate/v16/resolvers/field-middleware) shows how to build reusable middleware that runs before or after resolvers, including ordering, class-based middleware, and attribute-based middleware. diff --git a/website/src/docs/hotchocolate/v16/resolvers/resolvers.md b/website/src/docs/hotchocolate/v16/resolvers/resolvers.md deleted file mode 100644 index a6d44f09b8c..00000000000 --- a/website/src/docs/hotchocolate/v16/resolvers/resolvers.md +++ /dev/null @@ -1,330 +0,0 @@ ---- -title: "Resolvers" ---- - -In the simplest terms, **a resolver is a generic function that produces a value for a particular field.** - -You can think of each field in our query as a method of the previous type which returns the next type. - -## Resolver Tree - -A resolver tree is a projection of a GraphQL operation that is prepared for execution. - -For better understanding, let's imagine we have a simple GraphQL query like the following, where we select some fields of the currently logged-in user. - -```graphql -query { - me { - name - company { - id - name - } - } -} -``` - -In Hot Chocolate, this query results in the following resolver tree. - -```mermaid -graph LR - A(query: QueryType) --> B(me: UserType) - B --> C(name: StringType) - B --> D(company: CompanyType) - D --> E(id: IdType) - D --> F(name: StringType) -``` - -This tree will be traversed by the execution engine, starting with one or more root resolvers. In the above example the `me` field represents the only root resolver. - -Field resolvers that are sub-selections of a field, can only be executed after a value has been resolved for their _parent_ field. In the case of the above example this means that the `name` and `company` resolvers can only run, after the `me` resolver has finished. Resolvers of field sub-selections can and will be executed in parallel. - -**Because of this it is important that resolvers, with the exception of top level mutation field resolvers, do not contain side-effects, since their execution order may vary.** - -The execution of a request finishes, once each resolver of the selected fields has produced a result. - -_This is of course an oversimplification that differs from the actual implementation._ - -# Defining a Resolver - -Resolvers can be defined in a way that should feel very familiar to C# developers, as they either translate to methods or delegates. - - - - -In the implementation-first approach, a public method is automatically inferred as a resolver. This means the method defines both the field in your schema and the logic to resolve its value. - -```csharp -[QueryType] -public partial class Query -{ - public static string Foo() => "Bar"; -} -``` - -This generates the following schema: - -```sdl -type Query { - foo: String! -} -``` - -Resolvers do not have to be methods. Public properties are also inferred as resolvers and exposed as fields in your schema. - -```csharp -[QueryType] -public partial class Query -{ - public static User User => new User("Ted"); -} - -public record User(string Name); -``` - -In this case, the property `Name` of the `User` object is also inferred as a resolver. - - - - -In the code-first approach, you define a resolver by assigning a resolver delegate to a field. This delegate contains the logic for resolving the field's value. - -```csharp -public class QueryType : ObjectType -{ - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .Field("foo") - .Type>() - .Resolve(ctx => "bar"); - } -} -``` - -You can also use `ObjectType` with a backing POCO. Public methods and properties on the POCO are bound as fields automatically. Use the `Field` method with a lambda expression to configure individual fields. - -```csharp -public class Query -{ - public string Foo() => "Bar"; -} - -public class QueryType : ObjectType -{ - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .Field(f => f.Foo()) - .Type>(); - } -} -``` - - - - -## Async Resolver - -Resolvers can be synchronous or asynchronous. Most data fetching operations, such as calling a service or database, are asynchronous. - -The most important aspect of async resolvers is to honor the CancellationToken. This allows execution to be cancelled if the client abandons the request, preventing unnecessary work and resource usage. - - - - -When using the implementation-first approach, you can add a `CancellationToken` parameter to your resolver method. The execution engine will automatically inject the request’s cancellation token. - -```csharp -public class Query -{ - public async Task GetProductByIdAsync( - int id, - ProductService productService, - CancellationToken cancellationToken) - => await productService.GetAsync(cancellationToken); -} -``` - - - - -When using the code-first approach, you can access the `CancellationToken` through the `IResolverContext` provided to your resolver. - -```csharp -descriptor - .Field("foo") - .Resolve(context => - { - CancellationToken ct = context.RequestAborted; - - // Omitted code for brevity - }); -``` - - - - -# Arguments - -In GraphQL, fields are conceptually similar to methods in C#. Just like methods, fields can have arguments, and you can access these argument values directly in your resolvers. - - - - -When using the implementation-first approach, any parameter in your resolver method that is not a service, a `CancellationToken`, or specially annotated is treated as a GraphQL argument. The execution engine will inject the argument value from the query into these parameters. For example, in the method below, the `id` parameter is recognized as an argument, while `ProductService` is injected as a service from the DI container. - -```csharp -public class Query -{ - public async Task GetProductByIdAsync( - int id, - ProductService productService, - CancellationToken cancellationToken) - => await productService.GetAsync(cancellationToken); -} -``` - - - - -When using the code-first approach, you can access field arguments using the resolver context. - -```csharp -descriptor - .Field("foo") - .Argument("id", a => a.Type>()) - .Resolve(context => - { - var id = context.ArgumentValue("id"); - - // Omitted code for brevity - }); -``` - - - - -[Learn more about arguments](/docs/hotchocolate/v16/defining-a-schema/arguments) - -# Injecting Services - -Hot Chocolate automatically recognizes types registered in the DI container and injects them into resolver parameters. - -```csharp -public class Query -{ - public List GetUsers(UserService userService) - => userService.GetUsers(); -} -``` - -While you can take attributes to annotate services, you do not have to for non-keyed services. - -```csharp -public class Query -{ - public List GetUsers([Service] UserService userService) - => userService.GetUsers(); -} -``` - -[Learn more about dependency injection](/docs/hotchocolate/v16/resolvers/dependency-injection) - -# Accessing parent values - -Each field resolver has access to the value that was resolved for its parent type. - -For example, consider the following schema: - -```sdl -type Query { - me: User!; -} - -type User { - id: ID!; - friends: [User!]!; -} -``` - -The `User` schema type is represented by a `User` runtime class. The `id` field is a property on this class. - -```csharp -public class User -{ - public string Id { get; set; } -} -``` - -The `friends` resolver, by contrast, is independent: it is not declared on the `User` type and uses the user's `Id` to compute its result. -From the `friends` resolver's perspective, the `User` runtime object is its _parent_. - -Access the parent value like this: - - - - -In the implementation-first approach, the parent object can be injected as a resolver parameter: - -```csharp -[ObjectType] -public static partial class UserNode -{ - public static Task> GetFriendsAsync( - [Parent] User user, - UserService userService, - CancellationToken cancellationToken) - { - // Omitted code for brevity - } -} -``` - -If database projections are enabled, the parent object may only contain the fields requested by the client. To ensure the projections engine also loads properties required by the resolver, declare those requirements on the parent parameter: - -```csharp -[ObjectType] -public static partial class UserNode -{ - public static Task> GetFriendsAsync( - [Parent(requires: nameof(User.Id))] User user, - UserService userService, - CancellationToken cancellationToken) - { - // Omitted code for brevity - } -} -``` - -Use `nameof` to make this requirement refactoring-safe. - - - - -In the code-first approach, the parent object is available via the `IResolverContext`. - -```csharp -public class User -{ - public string Id { get; set; } -} - -public class UserType : ObjectType -{ - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .Field("friends") - .Resolve(context => - { - User parent = context.Parent(); - - // Omitted code for brevity - }); - } -} -``` - - -