From a0910100b9abc4ba51e605a0546e35fbd8c2eda4 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 18 Mar 2026 08:24:40 +0100 Subject: [PATCH 1/2] [website] Restructure docs --- website/src/docs/hotchocolate/v16/index.md | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/website/src/docs/hotchocolate/v16/index.md b/website/src/docs/hotchocolate/v16/index.md index 394fbfda8b6..9fd817fe6d3 100644 --- a/website/src/docs/hotchocolate/v16/index.md +++ b/website/src/docs/hotchocolate/v16/index.md @@ -60,30 +60,30 @@ Code-first is useful when you need to decouple the GraphQL schema shape from you Both approaches can be mixed in the same project. You can use implementation-first for most types and drop into code-first for specific cases that need more control. -# Two Paths for Securing Your API +# Public and Private GraphQL -How you secure your GraphQL server depends on who consumes it. Hot Chocolate supports two distinct security models that address different threat profiles. +Most GraphQL APIs fall into one of two categories, and the choice shapes how you configure Hot Chocolate. -## Public APIs (third-party consumers) +## Public GraphQL -If external developers or third-party clients query your API (like the GitHub GraphQL API), you cannot control what operations they send. Clients can construct arbitrary queries, including deeply nested or expensive ones. +A public API is consumed by third-party developers or external clients. GitHub's GraphQL API is the canonical example. You publish a schema, and external teams build applications against it. Because you do not control the clients, they can send any operation they want. -For this scenario, use **cost analysis** to assign weights to fields and limit the total cost of any single operation. Combine it with execution depth limits, pagination limits, and introspection controls. +Hot Chocolate provides **cost analysis** for this scenario. You assign weights to fields and connections, and the server rejects operations that exceed the budget before execution begins. -- [Cost analysis](/docs/hotchocolate/v16/security/cost-analysis) assigns field-level weights and enforces per-request budgets. -- [Controlling introspection](/docs/hotchocolate/v16/server/introspection) restricts schema exposure in production. -- [Authorization](/docs/hotchocolate/v16/security/authorization) limits access to specific types and fields based on roles or policies. +- [Cost analysis](/docs/hotchocolate/v16/security/cost-analysis) explains field weights, type costs, and budget configuration. +- [Authorization](/docs/hotchocolate/v16/security/authorization) limits access to types and fields based on roles or policies. +- [Controlling introspection](/docs/hotchocolate/v16/server/introspection) lets you restrict schema visibility in production. -## Private APIs (first-party consumers) +## Private GraphQL -If only your own applications query the API (like Meta's internal GraphQL usage), you control all the clients. You know every operation at build time. +A private API is consumed by your own applications. This is how Meta built and operates GraphQL internally. You control both the server and every client. You know every operation at build time. -For this scenario, use **trusted documents**. Extract all operations from your client applications during their build process, register them with the server, and reject any operation that is not pre-registered. This eliminates most attack vectors entirely: clients can only execute operations you have reviewed and approved. +Hot Chocolate provides **trusted documents** for this scenario. You extract all operations from your client applications during their build process, register them with the server, and the server only accepts pre-registered operations. -- [Trusted documents](/docs/hotchocolate/v16/performance/persisted-operations) explains the full workflow: extraction, registration, and enforcement. +- [Trusted documents](/docs/hotchocolate/v16/performance/persisted-operations) covers the full workflow: extraction, registration, and enforcement. - [Strawberry Shake](/docs/strawberryshake/v16) and [Relay](https://relay.dev/docs/guides/persisted-queries/) both support build-time operation extraction. -You can combine both models. A common pattern is to use trusted documents for your own frontend applications and cost analysis as a fallback for partner integrations. +These two approaches complement each other. A common setup is trusted documents for your own frontend applications and cost analysis for partner integrations. # Key Terminology From 85f01fb59a77f717dd5fca5f99a74a64471d469a Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 18 Mar 2026 14:02:40 +0100 Subject: [PATCH 2/2] [website] Restructure docs --- website/src/docs/docs.json | 329 +++------- .../v16/api-reference/apollo-federation.md | 6 +- .../v16/api-reference/custom-attributes.md | 2 +- .../v16/api-reference/custom-context-data.md | 93 --- .../v16/api-reference/executable.md | 2 +- .../v16/api-reference/extending-filtering.md | 4 +- .../hotchocolate/v16/api-reference/options.md | 153 ++++- .../v16/building-a-schema/arguments.md | 2 +- .../v16/building-a-schema/directives.md | 2 +- .../v16/building-a-schema/index.md | 2 +- .../v16/building-a-schema/lists.md | 4 +- .../v16/building-a-schema/mutations.md | 2 +- .../v16/building-a-schema/object-types.md | 4 +- .../v16/building-a-schema/queries.md | 2 +- .../v16/building-a-schema/relay.md | 6 +- .../v16/execution-engine/field-middleware.md | 6 +- .../get-started-with-graphql-in-net-core.md | 2 +- .../v16/guides/dynamic-schemas.md | 241 +++++++ .../hotchocolate/v16/guides/error-handling.md | 593 ++++++++++++++++++ .../hotchocolate/v16/guides/mcp-adapter.md | 331 ++++++++++ .../v16/guides/openapi-adapter.md | 307 +++++++++ .../hotchocolate/v16/guides/performance.md | 251 ++++++++ .../hotchocolate/v16/guides/private-api.md | 392 ++++++++++++ .../hotchocolate/v16/guides/public-api.md | 405 ++++++++++++ .../v16/guides/schema-evolution.md | 319 ++++++++++ .../docs/hotchocolate/v16/guides/testing.md | 330 ++++++++++ website/src/docs/hotchocolate/v16/index.md | 10 +- .../v16/integrations/entity-framework.md | 12 +- .../hotchocolate/v16/integrations/marten.md | 14 +- .../hotchocolate/v16/integrations/mongodb.md | 6 +- .../v16/integrations/spatial-data.md | 4 +- .../automatic-persisted-operations.md | 2 +- .../hotchocolate/v16/performance/index.md | 2 +- ...ted-operations.md => trusted-documents.md} | 0 .../dataloader.md | 8 +- .../dependency-injection.md | 0 .../fetching-from-databases.md | 4 +- .../fetching-from-rest.md | 8 +- .../filtering.md | 8 +- .../index.md | 26 +- .../pagination.md | 12 +- .../projections.md | 6 +- .../resolvers.md | 8 +- .../sorting.md | 8 +- .../authentication.md | 4 +- .../authorization.md | 6 +- .../cost-analysis.md | 6 +- .../{security => securing-your-api}/index.md | 22 +- .../introspection.md | 0 .../hotchocolate/v16/security/query-depth.md | 3 - .../docs/hotchocolate/v16/server/batching.md | 2 +- .../docs/hotchocolate/v16/server/endpoints.md | 185 +++++- .../hotchocolate/v16/server/global-state.md | 2 +- .../hotchocolate/v16/server/http-transport.md | 185 +++++- .../src/docs/hotchocolate/v16/server/index.md | 4 +- .../hotchocolate/v16/server/interceptors.md | 168 ++++- 56 files changed, 4013 insertions(+), 502 deletions(-) delete mode 100644 website/src/docs/hotchocolate/v16/api-reference/custom-context-data.md create mode 100644 website/src/docs/hotchocolate/v16/guides/dynamic-schemas.md create mode 100644 website/src/docs/hotchocolate/v16/guides/error-handling.md create mode 100644 website/src/docs/hotchocolate/v16/guides/mcp-adapter.md create mode 100644 website/src/docs/hotchocolate/v16/guides/openapi-adapter.md create mode 100644 website/src/docs/hotchocolate/v16/guides/performance.md create mode 100644 website/src/docs/hotchocolate/v16/guides/private-api.md create mode 100644 website/src/docs/hotchocolate/v16/guides/public-api.md create mode 100644 website/src/docs/hotchocolate/v16/guides/schema-evolution.md create mode 100644 website/src/docs/hotchocolate/v16/guides/testing.md rename website/src/docs/hotchocolate/v16/performance/{persisted-operations.md => trusted-documents.md} (100%) rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers-and-data}/dataloader.md (97%) rename website/src/docs/hotchocolate/v16/{server => resolvers-and-data}/dependency-injection.md (100%) rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers-and-data}/fetching-from-databases.md (97%) rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers-and-data}/fetching-from-rest.md (93%) rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers-and-data}/filtering.md (97%) rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers-and-data}/index.md (79%) rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers-and-data}/pagination.md (95%) rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers-and-data}/projections.md (98%) rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers-and-data}/resolvers.md (97%) rename website/src/docs/hotchocolate/v16/{fetching-data => resolvers-and-data}/sorting.md (97%) rename website/src/docs/hotchocolate/v16/{security => securing-your-api}/authentication.md (98%) rename website/src/docs/hotchocolate/v16/{security => securing-your-api}/authorization.md (98%) rename website/src/docs/hotchocolate/v16/{security => securing-your-api}/cost-analysis.md (98%) rename website/src/docs/hotchocolate/v16/{security => securing-your-api}/index.md (87%) rename website/src/docs/hotchocolate/v16/{server => securing-your-api}/introspection.md (100%) delete mode 100644 website/src/docs/hotchocolate/v16/security/query-depth.md diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index 00c54bc28ab..c1a03dd7026 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -325,10 +325,7 @@ "path": "v16", "title": "v16", "items": [ - { - "path": "index", - "title": "Overview" - }, + { "path": "index", "title": "Overview" }, { "path": "get-started-with-graphql-in-net-core", "title": "Getting Started" @@ -337,292 +334,134 @@ "path": "building-a-schema", "title": "Building a Schema", "items": [ + { "path": "index", "title": "Overview" }, + { "path": "queries", "title": "Queries" }, + { "path": "mutations", "title": "Mutations" }, + { "path": "subscriptions", "title": "Subscriptions" }, + { "path": "object-types", "title": "Object Types" }, + { "path": "scalars", "title": "Scalars" }, + { "path": "arguments", "title": "Arguments" }, + { "path": "input-object-types", "title": "Input Object Types" }, + { "path": "lists", "title": "Lists" }, + { "path": "non-null", "title": "Non-Null" }, + { "path": "enums", "title": "Enums" }, + { "path": "interfaces", "title": "Interfaces" }, + { "path": "unions", "title": "Unions" }, + { "path": "extending-types", "title": "Extending Types" }, + { "path": "directives", "title": "Directives" }, + { "path": "documentation", "title": "Documentation" }, + { "path": "versioning", "title": "Versioning" }, + { "path": "relay", "title": "Relay" }, + { "path": "dynamic-schemas", "title": "Dynamic Schemas" } + ] + }, + { + "path": "resolvers-and-data", + "title": "Resolvers and Data", + "items": [ + { "path": "index", "title": "Overview" }, + { "path": "resolvers", "title": "Resolvers" }, { - "path": "index", - "title": "Overview" - }, - { - "path": "queries", - "title": "Queries" - }, - { - "path": "mutations", - "title": "Mutations" - }, - { - "path": "subscriptions", - "title": "Subscriptions" - }, - { - "path": "object-types", - "title": "Object Types" - }, - { - "path": "scalars", - "title": "Scalars" - }, - { - "path": "arguments", - "title": "Arguments" - }, - { - "path": "input-object-types", - "title": "Input Object Types" - }, - { - "path": "lists", - "title": "Lists" - }, - { - "path": "non-null", - "title": "Non-Null" - }, - { - "path": "enums", - "title": "Enums" - }, - { - "path": "interfaces", - "title": "Interfaces" - }, - { - "path": "unions", - "title": "Unions" - }, - { - "path": "extending-types", - "title": "Extending Types" - }, - { - "path": "directives", - "title": "Directives" - }, - { - "path": "documentation", - "title": "Documentation" - }, - { - "path": "versioning", - "title": "Versioning" + "path": "dependency-injection", + "title": "Dependency Injection" }, + { "path": "dataloader", "title": "DataLoader" }, + { "path": "pagination", "title": "Pagination" }, + { "path": "filtering", "title": "Filtering" }, + { "path": "sorting", "title": "Sorting" }, + { "path": "projections", "title": "Projections" }, { - "path": "relay", - "title": "Relay" + "path": "fetching-from-databases", + "title": "Fetching from Databases" }, - { - "path": "dynamic-schemas", - "title": "Dynamic Schemas" - } + { "path": "fetching-from-rest", "title": "Fetching from REST" } ] }, { - "path": "fetching-data", - "title": "Fetching data", + "path": "guides", + "title": "Guides", "items": [ - { - "path": "index", - "title": "Overview" - }, - { - "path": "resolvers", - "title": "Resolvers" - }, - { - "path": "fetching-from-databases", - "title": "Fetching from Databases" - }, - { - "path": "fetching-from-rest", - "title": "Fetching from REST" - }, - { - "path": "dataloader", - "title": "DataLoader" - }, - { - "path": "pagination", - "title": "Pagination" - }, - { - "path": "filtering", - "title": "Filtering" - }, - { - "path": "sorting", - "title": "Sorting" - }, - { - "path": "projections", - "title": "Projections" - } + { "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": "execution-engine", "title": "Execution Engine", "items": [ - { - "path": "index", - "title": "Overview" - }, - { - "path": "field-middleware", - "title": "Field middleware" - } + { "path": "index", "title": "Overview" }, + { "path": "field-middleware", "title": "Field Middleware" } ] }, { "path": "integrations", "title": "Integrations", "items": [ - { - "path": "index", - "title": "Overview" - }, - { - "path": "entity-framework", - "title": "Entity Framework" - }, - { - "path": "mongodb", - "title": "MongoDB" - }, - { - "path": "spatial-data", - "title": "Spatial Data" - }, - { - "path": "marten", - "title": "Marten" - } + { "path": "index", "title": "Overview" }, + { "path": "entity-framework", "title": "Entity Framework" }, + { "path": "mongodb", "title": "MongoDB" }, + { "path": "spatial-data", "title": "Spatial Data" }, + { "path": "marten", "title": "Marten" } ] }, { "path": "server", "title": "Server", "items": [ - { - "path": "index", - "title": "Overview" - }, - { - "path": "endpoints", - "title": "Endpoints" - }, - { - "path": "http-transport", - "title": "HTTP transport" - }, - { - "path": "interceptors", - "title": "Interceptors" - }, - { - "path": "dependency-injection", - "title": "Dependency injection" - }, - { - "path": "warmup", - "title": "Warmup" - }, - { - "path": "global-state", - "title": "Global State" - }, - { - "path": "introspection", - "title": "Introspection" - }, - { - "path": "files", - "title": "Files" - }, - { - "path": "instrumentation", - "title": "Instrumentation" - }, - { - "path": "batching", - "title": "Batching" - }, - { - "path": "command-line", - "title": "Command Line" - } + { "path": "index", "title": "Overview" }, + { "path": "endpoints", "title": "Endpoints" }, + { "path": "http-transport", "title": "Transports" }, + { "path": "interceptors", "title": "Interceptors" }, + { "path": "warmup", "title": "Warmup" }, + { "path": "global-state", "title": "Global State" }, + { "path": "files", "title": "File Uploads" }, + { "path": "instrumentation", "title": "Instrumentation" }, + { "path": "batching", "title": "Batching" }, + { "path": "command-line", "title": "CLI Commands" } ] }, { "path": "performance", "title": "Performance", "items": [ - { - "path": "index", - "title": "Overview" - }, - { - "path": "persisted-operations", - "title": "Persisted operations" - }, + { "path": "index", "title": "Overview" }, + { "path": "trusted-documents", "title": "Trusted Documents" }, { "path": "automatic-persisted-operations", - "title": "Automatic persisted operations" + "title": "Automatic Persisted Operations" } ] }, { - "path": "security", - "title": "Security", + "path": "securing-your-api", + "title": "Securing Your API", "items": [ - { - "path": "index", - "title": "Overview" - }, - { - "path": "authentication", - "title": "Authentication" - }, - { - "path": "authorization", - "title": "Authorization" - }, - { - "path": "cost-analysis", - "title": "Cost Analysis" - } + { "path": "index", "title": "Overview" }, + { "path": "authentication", "title": "Authentication" }, + { "path": "authorization", "title": "Authorization" }, + { "path": "cost-analysis", "title": "Cost Analysis" }, + { "path": "introspection", "title": "Controlling Introspection" } ] }, { "path": "api-reference", "title": "API Reference", "items": [ - { - "path": "custom-attributes", - "title": "Custom Attributes" - }, - { - "path": "errors", - "title": "Errors" - }, - { - "path": "language", - "title": "Language" - }, - { - "path": "extending-filtering", - "title": "Extending Filtering" - }, - { - "path": "visitors", - "title": "Visitors" - }, - { - "path": "apollo-federation", - "title": "Apollo Federation" - }, - { - "path": "executable", - "title": "Executable" - } + { "path": "custom-attributes", "title": "Attributes" }, + { "path": "options", "title": "Configuration Options" }, + { "path": "errors", "title": "Error Codes" }, + { "path": "extending-filtering", "title": "Extending Filtering" }, + { "path": "apollo-federation", "title": "Apollo Federation" }, + { "path": "language", "title": "Language" }, + { "path": "visitors", "title": "Visitors" }, + { "path": "executable", "title": "Executable" } ] }, { diff --git a/website/src/docs/hotchocolate/v16/api-reference/apollo-federation.md b/website/src/docs/hotchocolate/v16/api-reference/apollo-federation.md index 659160fad60..9fbbce89ed6 100644 --- a/website/src/docs/hotchocolate/v16/api-reference/apollo-federation.md +++ b/website/src/docs/hotchocolate/v16/api-reference/apollo-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/resolvers-and-data/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 @@ -427,6 +427,6 @@ Use `[GraphQLName("...")]` or `descriptor.Name("...")` to set the GraphQL name e # Next Steps -- [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) for resolver patterns -- [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader) for batching in reference resolvers +- [Resolvers](/docs/hotchocolate/v16/resolvers-and-data/resolvers) for resolver patterns +- [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/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/api-reference/custom-attributes.md b/website/src/docs/hotchocolate/v16/api-reference/custom-attributes.md index e560c48df27..9c76008e113 100644 --- a/website/src/docs/hotchocolate/v16/api-reference/custom-attributes.md +++ b/website/src/docs/hotchocolate/v16/api-reference/custom-attributes.md @@ -215,4 +215,4 @@ Confirm that you are inheriting from the correct base class and that the `OnConf - [Building a schema](/docs/hotchocolate/v16/building-a-schema) for type system configuration - [Field middleware](/docs/hotchocolate/v16/execution-engine/field-middleware) for creating custom middleware -- [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) for field resolution patterns +- [Resolvers](/docs/hotchocolate/v16/resolvers-and-data/resolvers) for field resolution patterns diff --git a/website/src/docs/hotchocolate/v16/api-reference/custom-context-data.md b/website/src/docs/hotchocolate/v16/api-reference/custom-context-data.md deleted file mode 100644 index 1df79256209..00000000000 --- a/website/src/docs/hotchocolate/v16/api-reference/custom-context-data.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: Custom Context Data ---- - -When implementing custom middleware, it can be useful to be able to store some custom state on the context. This could be to build up a cache or other state data. Hot Chocolate has two types of context stores that we can use. - -# Global Context Data - -The global context data is a thread-safe dictionary that is available though the `IQueryContext` and the `IResolverContext`. This means we are able to share context data between query middleware components and field middleware components. - -One common use case is to aggregate some state when the GraphQL request is created and use it in field middleware or in the resolver. - -In order to intercept the request creation we can add an `IOperationRequestInterceptor` to our services and there build up our custom state. - -```csharp -services.AddQueryRequestInterceptor((ctx, builder, ct) => -{ - builder.SetProperty("Foo", new Foo()); - return Task.CompletedTask; -}); -``` - -We can access the initial provided data in a query middleware, field middleware or our resolver. - -Query Middleware Example: - -```csharp -builder.Use(next => context => -{ - // access data - var foo = (Foo)context.ContextData["Foo"]; - - // set new data - context.ContextData["Bar"] = new Bar(); - - return next.Invoke(context); -}); -``` - -Field Middleware Example: - -```csharp -SchemaBuilder.New() - .Use(next => context => - { - // access data - var foo = (Foo)context.ContextData["Foo"]; - - // set new data - context.ContextData["Bar"] = new Bar(); - - return next.Invoke(context); - }) - .Create(); -``` - -Resolver Example: - -```csharp -public Task MyResolver([State("Foo")]Foo foo) -{ - ... -} -``` - -# Scoped Context Data - -The scoped context data is an immutable dictionary and is only available through the `IResolverContext`. - -Scoped state allows us to aggregate state for our child field resolvers. - -Let's say we have the following query: - -```graphql -{ - a { - b { - c - } - } - d { - e { - f - } - } -} -``` - -If the `a`-resolver would put something on the scoped context its sub-tree could access that data. This means, `b` and `c` could access the data but `d`, `e` and `f` would _NOT_ be able to access the data, their dictionary is still unchanged. - -```csharp -context.ScopedContextData = context.ScopedContextData.SetItem("foo", "bar"); -``` diff --git a/website/src/docs/hotchocolate/v16/api-reference/executable.md b/website/src/docs/hotchocolate/v16/api-reference/executable.md index f42eee8970f..9a99babdc13 100644 --- a/website/src/docs/hotchocolate/v16/api-reference/executable.md +++ b/website/src/docs/hotchocolate/v16/api-reference/executable.md @@ -125,4 +125,4 @@ This occurs when `SingleOrDefaultAsync` finds multiple results. Verify that your - [Entity Framework integration](/docs/hotchocolate/v16/integrations/entity-framework) for EF Core executables - [MongoDB integration](/docs/hotchocolate/v16/integrations/mongodb) for MongoDB executables -- [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) for applying filters to executables +- [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering) for applying filters to executables diff --git a/website/src/docs/hotchocolate/v16/api-reference/extending-filtering.md b/website/src/docs/hotchocolate/v16/api-reference/extending-filtering.md index 2efa3efa2a2..5993e2cc6a7 100644 --- a/website/src/docs/hotchocolate/v16/api-reference/extending-filtering.md +++ b/website/src/docs/hotchocolate/v16/api-reference/extending-filtering.md @@ -47,7 +47,7 @@ A filter convention is a .NET class that implements `IFilterConvention`. Instead ## Descriptor -Most descriptor capabilities are documented under [Filtering](/docs/hotchocolate/v16/fetching-data/filtering). Read the parts about `FilterConventions` there first. +Most descriptor capabilities are documented under [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering). Read the parts about `FilterConventions` there first. Two features on the descriptor are specific to extensibility: @@ -298,6 +298,6 @@ Ensure that the convention and provider are registered on the schema builder. If # Next Steps -- [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) for using built-in filtering +- [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering) for using built-in filtering - [Visitors](/docs/hotchocolate/v16/api-reference/visitors) for understanding the visitor pattern - [MongoDB integration](/docs/hotchocolate/v16/integrations/mongodb) for MongoDB-specific filtering diff --git a/website/src/docs/hotchocolate/v16/api-reference/options.md b/website/src/docs/hotchocolate/v16/api-reference/options.md index cb9ad587279..9f1a864fb18 100644 --- a/website/src/docs/hotchocolate/v16/api-reference/options.md +++ b/website/src/docs/hotchocolate/v16/api-reference/options.md @@ -1,6 +1,6 @@ --- title: Options Reference -description: Comprehensive reference for all configuration options in Hot Chocolate v16, including schema, request, cost, server, and paging options. +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. --- Hot Chocolate provides several option groups that control different aspects of the GraphQL server. You configure them through methods on the `IRequestExecutorBuilder`. @@ -107,13 +107,19 @@ builder.Services }); ``` -| Property | Type | Default | Description | -| ------------------------- | ----------------- | ------- | ------------------------------------------------------------------------------- | -| `EnableGetRequests` | `bool` | `true` | Allows GraphQL queries over HTTP GET. | -| `EnableMultipartRequests` | `bool` | `true` | Allows multipart HTTP requests (file uploads). | -| `Batching` | `AllowedBatching` | `None` | Controls which batching modes are allowed. Use `AllowedBatching.All` to enable. | -| `MaxBatchSize` | `int` | `1024` | Maximum number of operations in a single batch. Set to `0` for unlimited. | -| `EnableSchemaRequests` | `bool` | `true` | Allows schema introspection requests. | +| Property | Type | Default | Description | +| ----------------------------------------- | ---------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `AllowedGetOperations` | `AllowedGetOperations` | `Query` | Controls which operation types are allowed via HTTP GET. Values: `None`, `Query`, `Mutation`, `Subscription`, `QueryAndMutation`, `All`. | +| `EnableGetRequests` | `bool` | `true` | Allows GraphQL queries over HTTP GET. | +| `EnableMultipartRequests` | `bool` | `true` | Allows multipart HTTP requests (file uploads). | +| `EnableSchemaRequests` | `bool` | `true` | Allows schema SDL downloads. | +| `EnableSchemaFileSupport` | `bool` | `true` | Allows the schema SDL to be served as a file download. | +| `EnforceGetRequestsPreflightHeader` | `bool` | `false` | Requires a preflight header on GET requests for CSRF protection. | +| `EnforceMultipartRequestsPreflightHeader` | `bool` | `true` | Requires a preflight header on multipart requests for CSRF protection. | +| `Batching` | `AllowedBatching` | `None` | Controls which batching modes are allowed. Use `AllowedBatching.All` to enable. | +| `MaxBatchSize` | `int` | `1024` | Maximum number of operations in a single batch. Set to `0` for unlimited. | +| `Sockets` | `GraphQLSocketOptions` | See below | WebSocket transport options. See [WebSocket options](#websocket-options-graphqlsocketoptions) for details. | +| `Tool` | `NitroAppOptions` | Default | Nitro IDE tool options. | Per-endpoint overrides are still supported through `WithOptions` on the endpoint builder: @@ -121,6 +127,25 @@ Per-endpoint overrides are still supported through `WithOptions` on the endpoint app.MapGraphQL().WithOptions(o => o.EnableGetRequests = false); ``` +## WebSocket Options (GraphQLSocketOptions) + +The `Sockets` property on `GraphQLServerOptions` holds WebSocket-specific settings. You configure them through `ModifyServerOptions`: + +```csharp +builder.Services + .AddGraphQLServer() + .ModifyServerOptions(o => + { + o.Sockets.ConnectionInitializationTimeout = TimeSpan.FromSeconds(30); + o.Sockets.KeepAliveInterval = TimeSpan.FromSeconds(10); + }); +``` + +| Property | Type | Default | Description | +| --------------------------------- | ----------- | ---------- | -------------------------------------------------------------------------------------------------------------- | +| `ConnectionInitializationTimeout` | `TimeSpan` | 10 seconds | The time a client has to send a `connection_init` message before the server closes the connection. | +| `KeepAliveInterval` | `TimeSpan?` | 5 seconds | The interval at which the server sends keep-alive ping messages. Set to `null` to disable keep-alive messages. | + # Paging Options (ModifyPagingOptions) Paging options control the default behavior for cursor-based pagination. Configure with `ModifyPagingOptions`: @@ -136,14 +161,106 @@ builder.Services }); ``` -| Property | Type | Default | Description | -| ------------------------------ | ------ | ------- | ----------------------------------------------------------------------------- | -| `DefaultPageSize` | `int` | `10` | The default number of items per page when `first` or `last` is not specified. | -| `MaxPageSize` | `int` | `50` | The maximum number of items a client can request per page. | -| `IncludeTotalCount` | `bool` | `false` | When `true`, includes a `totalCount` field on connection types. | -| `AllowBackwardPagination` | `bool` | `true` | Allows clients to paginate backward using `last` and `before`. | -| `RequirePagingBoundaries` | `bool` | `false` | Requires clients to provide either `first` or `last`. | -| `InferConnectionNameFromField` | `bool` | `true` | Infers the connection type name from the field name. | +| Property | Type | Default | Description | +| ------------------------------ | -------------- | ------------- | ----------------------------------------------------------------------------------------------------------- | +| `DefaultPageSize` | `int?` | `10` | The default number of items per page when `first` or `last` is not specified. | +| `MaxPageSize` | `int?` | `50` | The maximum number of items a client can request per page. | +| `IncludeTotalCount` | `bool?` | `false` | When `true`, includes a `totalCount` field on connection types. | +| `AllowBackwardPagination` | `bool?` | `true` | Allows clients to paginate backward using `last` and `before`. | +| `RequirePagingBoundaries` | `bool?` | `false` | Requires clients to provide either `first` or `last`. | +| `InferConnectionNameFromField` | `bool?` | `true` | Infers the connection type name from the field name. | +| `IncludeNodesField` | `bool?` | `null` | When `true`, exposes a `nodes` field on the Connection type that returns a flattened list without edges. | +| `EnableRelativeCursors` | `bool?` | `null` | When `true`, enables relative cursor support for pagination. | +| `NullOrdering` | `NullOrdering` | `Unspecified` | Defines how your database orders null values. Values: `Unspecified`, `NativeNullsFirst`, `NativeNullsLast`. | +| `ProviderName` | `string?` | `null` | The name of the paging provider to use. When `null`, the default provider is used. | + +# Parser Options (ModifyParserOptions) + +Parser options control limits on the GraphQL document parser. These are important security and performance settings that protect against excessively large or complex queries. Configure them with `ModifyParserOptions`: + +```csharp +builder.Services + .AddGraphQLServer() + .ModifyParserOptions(o => + { + o.MaxAllowedFields = 500; + o.MaxAllowedNodes = 5000; + }); +``` + +| Property | Type | Default | Description | +| ------------------ | ------ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `MaxAllowedNodes` | `int` | `int.MaxValue` | Maximum number of syntax nodes allowed in a document. Prevents excessive memory and CPU usage during parsing. | +| `MaxAllowedTokens` | `int` | `int.MaxValue` | Maximum number of tokens allowed in a document. Prevents excessive memory and CPU usage during lexing. | +| `MaxAllowedFields` | `int` | `2048` | Maximum number of fields allowed in a document. Provides a convenient way to limit query size since fields are an intuitive measure of scope. | +| `IncludeLocations` | `bool` | `true` | Preserves location information in syntax nodes so that errors can reference positions in the original source. Disabling reduces memory usage. | + +Parsing happens before validation, so even invalid queries consume resources. Setting `MaxAllowedNodes`, `MaxAllowedTokens`, and `MaxAllowedFields` to reasonable values for your schema protects against denial-of-service attacks. + +# Subscription Options + +Subscription options control topic buffer behavior for subscription providers. You pass them when registering a subscription provider: + +```csharp +builder.Services + .AddGraphQLServer() + .AddInMemorySubscriptions(new SubscriptionOptions + { + TopicBufferCapacity = 128, + TopicBufferFullMode = TopicBufferFullMode.DropOldest, + }); +``` + +| Property | Type | Default | Description | +| --------------------- | --------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TopicPrefix` | `string?` | `null` | A prefix prepended to all topic names. Useful when multiple services share the same message broker. | +| `TopicBufferCapacity` | `int` | `64` | The in-memory buffer size for messages per topic. When the buffer fills, the `TopicBufferFullMode` policy applies. | +| `TopicBufferFullMode` | `TopicBufferFullMode` | `DropOldest` | The behavior when writing to a full topic buffer. Values: `DropOldest` (remove oldest message), `DropNewest` (remove newest message), `DropWrite` (discard the incoming message). | + +All subscription providers (in-memory, Redis, NATS, RabbitMQ, Postgres) accept these options. + +# Global Object Identification Options + +Global object identification options configure the Relay-style `node` and `nodes` fields. You configure them through `AddGlobalObjectIdentification`: + +```csharp +builder.Services + .AddGraphQLServer() + .AddGlobalObjectIdentification(o => + { + o.MaxAllowedNodeBatchSize = 25; + }); +``` + +| Property | Type | Default | Description | +| ----------------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------ | +| `RegisterNodeInterface` | `bool` | `true` | Registers the `Node` interface and adds the `node(id: ID!): Node` field to the Query type. | +| `AddNodesField` | `bool` | `true` | Adds a `nodes(ids: [ID!]!): [Node]!` field to the Query type for batch node fetching. | +| `EnsureAllNodesCanBeResolved` | `bool` | `true` | Validates during schema building that every type implementing `Node` has a corresponding node resolver configured. | +| `MaxAllowedNodeBatchSize` | `int` | `50` | The maximum number of IDs a client can pass to the `nodes` field in a single request. Prevents excessive batch fetching. | + +# Cache Control Options (ModifyCacheControlOptions) + +Cache control options configure HTTP response caching hints based on the `@cacheControl` directive. Install the `HotChocolate.Caching` package and configure with `ModifyCacheControlOptions`: + +```csharp +builder.Services + .AddGraphQLServer() + .UseQueryCachePipeline() + .AddCacheControl() + .ModifyCacheControlOptions(o => + { + o.DefaultMaxAge = 60; + o.ApplyDefaults = true; + }); +``` + +| Property | Type | Default | Description | +| --------------- | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Enable` | `bool` | `true` | Enables or disables query result caching. | +| `DefaultMaxAge` | `int` | `0` | The default `max-age` value (in seconds) applied to fields when `ApplyDefaults` is `true`. | +| `DefaultScope` | `CacheControlScope` | `Public` | The default cache scope applied to fields when `ApplyDefaults` is `true`. Values: `Public`, `Private`. | +| `ApplyDefaults` | `bool` | `true` | When `true`, applies `DefaultMaxAge` and `DefaultScope` to all fields that do not already have a `@cacheControl` directive, are on the Query root type, or are responsible for fetching data. | # Troubleshooting @@ -159,6 +276,6 @@ Batching is disabled by default in v16. Enable it with `ModifyServerOptions(o => # Next Steps - [Execution engine](/docs/hotchocolate/v16/execution-engine) for pipeline configuration -- [Pagination](/docs/hotchocolate/v16/fetching-data/pagination) for paging setup -- [Persisted operations](/docs/hotchocolate/v16/performance/persisted-operations) for operation caching +- [Pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination) for paging setup +- [Persisted operations](/docs/hotchocolate/v16/performance/trusted-documents) for operation caching - [Migration guide](/docs/hotchocolate/v16/migrating/migrate-from-15-to-16) for breaking option changes diff --git a/website/src/docs/hotchocolate/v16/building-a-schema/arguments.md b/website/src/docs/hotchocolate/v16/building-a-schema/arguments.md index d1611d4a0b6..7cdeec7d92c 100644 --- a/website/src/docs/hotchocolate/v16/building-a-schema/arguments.md +++ b/website/src/docs/hotchocolate/v16/building-a-schema/arguments.md @@ -225,4 +225,4 @@ If a global ID cannot be deserialized, the error message indicates a type mismat - **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-and-data/resolvers). diff --git a/website/src/docs/hotchocolate/v16/building-a-schema/directives.md b/website/src/docs/hotchocolate/v16/building-a-schema/directives.md index 426f8ecd2b6..bf58bfc895e 100644 --- a/website/src/docs/hotchocolate/v16/building-a-schema/directives.md +++ b/website/src/docs/hotchocolate/v16/building-a-schema/directives.md @@ -224,5 +224,5 @@ If a non-repeatable directive is applied twice at the same location, validation # Next Steps - **Need to deprecate fields?** See [Versioning](/docs/hotchocolate/v16/defining-a-schema/versioning). -- **Need to authorize fields?** See [Authorization](/docs/hotchocolate/v16/security/authorization). +- **Need to authorize fields?** See [Authorization](/docs/hotchocolate/v16/securing-your-api/authorization). - **Need to extend types?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/extending-types). diff --git a/website/src/docs/hotchocolate/v16/building-a-schema/index.md b/website/src/docs/hotchocolate/v16/building-a-schema/index.md index f94bda7e512..cf6278a6f0c 100644 --- a/website/src/docs/hotchocolate/v16/building-a-schema/index.md +++ b/website/src/docs/hotchocolate/v16/building-a-schema/index.md @@ -147,6 +147,6 @@ Modifiers wrap other types to change their nullability or turn them into lists. - **"I want to define my first query."** Start with [Queries](/docs/hotchocolate/v16/defining-a-schema/queries). It covers the `[QueryType]` attribute, naming conventions, and how to register multiple query classes. -- **"I want to fetch data from a database."** See [Resolvers](/docs/hotchocolate/v16/fetching-data/resolvers) for how fields load data, and [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader) for batching. +- **"I want to fetch data from a database."** See [Resolvers](/docs/hotchocolate/v16/resolvers-and-data/resolvers) for how fields load data, and [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/dataloader) for batching. - **"I want to understand how my C# types become GraphQL types."** Read [Object Types](/docs/hotchocolate/v16/defining-a-schema/object-types) for a detailed walkthrough of the mapping rules. diff --git a/website/src/docs/hotchocolate/v16/building-a-schema/lists.md b/website/src/docs/hotchocolate/v16/building-a-schema/lists.md index e71418f2088..936361574b1 100644 --- a/website/src/docs/hotchocolate/v16/building-a-schema/lists.md +++ b/website/src/docs/hotchocolate/v16/building-a-schema/lists.md @@ -137,5 +137,5 @@ When returning `IQueryable`, Hot Chocolate materializes the query. If you use # Next Steps - **Need to control nullability?** See [Non-Null](/docs/hotchocolate/v16/defining-a-schema/non-null). -- **Need pagination instead of full lists?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination). -- **Need to filter or sort lists?** See [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) and [Sorting](/docs/hotchocolate/v16/fetching-data/sorting). +- **Need pagination instead of full lists?** See [Pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination). +- **Need to filter or sort lists?** See [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering) and [Sorting](/docs/hotchocolate/v16/resolvers-and-data/sorting). diff --git a/website/src/docs/hotchocolate/v16/building-a-schema/mutations.md b/website/src/docs/hotchocolate/v16/building-a-schema/mutations.md index 52119b22785..ba8d476e732 100644 --- a/website/src/docs/hotchocolate/v16/building-a-schema/mutations.md +++ b/website/src/docs/hotchocolate/v16/building-a-schema/mutations.md @@ -425,4 +425,4 @@ GraphQL guarantees serial execution of top-level mutation fields. If mutations a - **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/resolvers-and-data/dataloader). diff --git a/website/src/docs/hotchocolate/v16/building-a-schema/object-types.md b/website/src/docs/hotchocolate/v16/building-a-schema/object-types.md index 9230f517121..c06b0b9655c 100644 --- a/website/src/docs/hotchocolate/v16/building-a-schema/object-types.md +++ b/website/src/docs/hotchocolate/v16/building-a-schema/object-types.md @@ -559,7 +559,7 @@ Dictionary fields produce auto-generated type names like `KeyValuePairOfStringAn # 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-and-data/resolvers). - **Need to compose types from multiple classes?** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/extending-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/resolvers-and-data/dataloader). diff --git a/website/src/docs/hotchocolate/v16/building-a-schema/queries.md b/website/src/docs/hotchocolate/v16/building-a-schema/queries.md index 4d22eb096ce..d40bbeabb8d 100644 --- a/website/src/docs/hotchocolate/v16/building-a-schema/queries.md +++ b/website/src/docs/hotchocolate/v16/building-a-schema/queries.md @@ -187,4 +187,4 @@ If two `[QueryType]` classes define methods that produce the same GraphQL field - **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/resolvers-and-data/resolvers) and [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/dataloader). diff --git a/website/src/docs/hotchocolate/v16/building-a-schema/relay.md b/website/src/docs/hotchocolate/v16/building-a-schema/relay.md index 3c8239df531..762d6b4618e 100644 --- a/website/src/docs/hotchocolate/v16/building-a-schema/relay.md +++ b/website/src/docs/hotchocolate/v16/building-a-schema/relay.md @@ -297,7 +297,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/resolvers-and-data/dataloader) for efficient batched fetching. ## Node with Type Extensions @@ -397,7 +397,7 @@ In v16, `MaxAllowedNodeBatchSize` has moved from the `Node` type configuration t # Next Steps -- **Need to fetch data efficiently?** See [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader). -- **Need pagination?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination). +- **Need to fetch data efficiently?** See [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/dataloader). +- **Need pagination?** See [Pagination](/docs/hotchocolate/v16/resolvers-and-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). diff --git a/website/src/docs/hotchocolate/v16/execution-engine/field-middleware.md b/website/src/docs/hotchocolate/v16/execution-engine/field-middleware.md index 0ce5d57132c..d2cadf13ccd 100644 --- a/website/src/docs/hotchocolate/v16/execution-engine/field-middleware.md +++ b/website/src/docs/hotchocolate/v16/execution-engine/field-middleware.md @@ -296,6 +296,6 @@ Hot Chocolate validates the order of built-in data middleware (paging, filtering # 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 +- [Resolvers](/docs/hotchocolate/v16/resolvers-and-data/resolvers) for field resolution +- [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering) and [sorting](/docs/hotchocolate/v16/resolvers-and-data/sorting) middleware +- [Pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination) middleware 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 66b5d1c89f3..4133d8e2a23 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 @@ -193,7 +193,7 @@ If `dotnet new graphql` fails with "No templates matched the input template name - **"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/integrations/entity-framework) for EF Core integration. +- **"I want to fetch data from a database."** See [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/dataloader) for batched data fetching, or [Entity Framework](/docs/hotchocolate/v16/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 new file mode 100644 index 00000000000..8579b81ea53 --- /dev/null +++ b/website/src/docs/hotchocolate/v16/guides/dynamic-schemas.md @@ -0,0 +1,241 @@ +--- +title: "Dynamic Schemas" +--- + +In multi-tenant or CMS-like applications, the GraphQL schema may need to change at runtime based on configuration, database structure, or per-tenant requirements. Hot Chocolate supports dynamic schemas through the `ITypeModule` interface, which lets you provide types programmatically and trigger schema reloads when the underlying data changes. + +# The ITypeModule Interface + +`ITypeModule` is the entry point for dynamically providing types to the schema building process. It has two members: + +- `TypesChanged`: An event that signals when types have changed and the current schema should be replaced. +- `CreateTypesAsync`: A method called during schema construction to create types for the new schema instance. + +When you fire the `TypesChanged` event, Hot Chocolate phases out the old schema and builds a new one using the updated types from your module. This gives you hot-reload behavior without restarting the application. + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddTypeModule(); +``` + +# Creating Types from a JSON File + +This example reads type definitions from a JSON file. In a real application, the JSON might come from a database, an admin UI, or an external configuration service. + +```csharp +// Infrastructure/JsonTypeModule.cs +public class JsonTypeModule : ITypeModule +{ + private readonly string _file; + + public JsonTypeModule(string file) + { + _file = file; + } + + public event EventHandler? TypesChanged; + + public async ValueTask> CreateTypesAsync( + IDescriptorContext context, + CancellationToken cancellationToken) + { + var types = new List(); + + await using var file = File.OpenRead(_file); + using var json = await JsonDocument.ParseAsync( + file, cancellationToken: cancellationToken); + + foreach (var type in json.RootElement.EnumerateArray()) + { + var typeDefinition = new ObjectTypeDefinition( + type.GetProperty("name").GetString()!); + + foreach (var field in type.GetProperty("fields").EnumerateArray()) + { + typeDefinition.Fields.Add( + new ObjectFieldDefinition( + field.GetString()!, + type: TypeReference.Parse("String!"), + pureResolver: ctx => "foo")); + } + + types.Add( + type.GetProperty("extension").GetBoolean() + ? ObjectTypeExtension.CreateUnsafe(typeDefinition) + : ObjectType.CreateUnsafe(typeDefinition)); + } + + return types; + } +} +``` + +When the JSON file changes, call `TypesChanged` to trigger a schema rebuild. You could use a file watcher or a polling mechanism to detect changes. + +# Unsafe Type Creation + +The `CreateUnsafe` method creates types directly from definition objects, bypassing the standard descriptor API. This gives you full control over the type structure but requires understanding the Hot Chocolate type system internals. + +## Creating an Object Type + +```csharp +var objectTypeDef = new ObjectTypeDefinition("Product") +{ + Description = "Represents a product in the catalog.", + RuntimeType = typeof(Dictionary) +}; + +var idField = new ObjectFieldDefinition( + "id", + "Unique identifier for the product.", + TypeReference.Parse("ID!"), + pureResolver: ctx => ctx.Parent>()["id"]); + +var nameField = new ObjectFieldDefinition( + "name", + "Name of the product.", + TypeReference.Parse("String!"), + pureResolver: ctx => ctx.Parent>()["name"]); + +objectTypeDef.Fields.Add(idField); +objectTypeDef.Fields.Add(nameField); + +var productType = ObjectType.CreateUnsafe(objectTypeDef); +``` + +## Adding Fields with Arguments + +```csharp +var discountArg = new ArgumentDefinition( + "discount", + "Discount percentage to apply.", + TypeReference.Parse("Float!")); + +var discountPriceField = new ObjectFieldDefinition( + "discountPrice", + "Price after discount.", + TypeReference.Parse("Float!"), + pureResolver: ctx => + { + var product = ctx.Parent>(); + var discountPct = ctx.ArgumentValue("discount"); + var price = (float)product["price"]; + return price * (1 - discountPct / 100); + }) +{ + Arguments = { discountArg } +}; + +objectTypeDef.Fields.Add(discountPriceField); +``` + +## Creating an Input Object Type + +```csharp +var inputTypeDef = new InputObjectTypeDefinition("ProductInput") +{ + Description = "Input for creating or updating a product.", + RuntimeType = typeof(Dictionary) +}; + +inputTypeDef.Fields.Add(new InputFieldDefinition( + "name", "Name of the product.", TypeReference.Parse("String!"))); + +inputTypeDef.Fields.Add(new InputFieldDefinition( + "price", "Price of the product.", TypeReference.Parse("Float!"))); + +var productInputType = InputObjectType.CreateUnsafe(inputTypeDef); +``` + +## Resolver Types + +Hot Chocolate supports two resolver delegate types for dynamically created fields: + +**Async resolvers** handle asynchronous operations like database queries or service calls: + +```csharp +var reviewsField = new ObjectFieldDefinition( + "reviews", + "Reviews for the product.", + TypeReference.Parse("[Review!]"), + resolver: async ctx => + { + var productId = ctx.Parent>()["id"]; + var service = ctx.Service(); + return await service.GetReviewsAsync(productId); + }); +``` + +**Pure resolvers** handle synchronous, side-effect-free operations. The execution engine optimizes these for better performance: + +```csharp +var nameField = new ObjectFieldDefinition( + "name", + "Name of the product.", + TypeReference.Parse("String!"), + pureResolver: ctx => ctx.Parent>()["name"]); +``` + +Use pure resolvers when you do not need async operations or service access. Use async resolvers when you need to call services, databases, or perform any I/O. + +## Combining Types in a Mutation + +```csharp +var createProductField = new ObjectFieldDefinition( + "createProduct", + "Creates a new product.", + TypeReference.Parse("Product!"), + resolver: async ctx => + { + var input = ctx.ArgumentValue>("input"); + var service = ctx.Service(); + return await service.CreateProductAsync(input); + }) +{ + Arguments = + { + new ArgumentDefinition( + "input", + "Input for creating the product.", + TypeReference.Parse("ProductInput!")) + } +}; + +var mutationDef = new ObjectTypeDefinition("Mutation") +{ + RuntimeType = typeof(object) +}; +mutationDef.Fields.Add(createProductField); + +var mutationType = ObjectType.CreateUnsafe(mutationDef); + +builder.Services + .AddGraphQLServer() + .AddQueryType() + .AddMutationType(mutationType) + .AddType(productInputType) + .AddType(productType); +``` + +# Troubleshooting + +## Schema not updating after TypesChanged + +Verify that the `TypesChanged` event is wired up and fired. The schema rebuild is asynchronous, so there may be a brief delay before the new schema is active. Check the logs for schema build errors. + +## CreateUnsafe produces validation errors + +`CreateUnsafe` bypasses the descriptor-level validation. If a type definition is incomplete (missing required fields, invalid type references), the error surfaces during schema construction. Verify all field definitions have valid type references and resolvers. + +## Dynamic types not visible in introspection + +Types created through `ITypeModule` must be registered as part of the schema. Verify that your module returns them from `CreateTypesAsync`. If a type is an extension, ensure the base type it extends exists in the schema. + +# Next Steps + +- **Extending existing types:** See [Extending Types](/docs/hotchocolate/v16/defining-a-schema/extending-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 new file mode 100644 index 00000000000..0be9b87d6b8 --- /dev/null +++ b/website/src/docs/hotchocolate/v16/guides/error-handling.md @@ -0,0 +1,593 @@ +--- +title: "Error Handling" +--- + +GraphQL APIs produce two kinds of errors. **Request errors** occur when something goes wrong during execution, such as an unhandled exception in a resolver. **Domain errors** represent business logic rejections, such as a username already being taken or an invalid input value. Hot Chocolate handles both, with different mechanisms for each. + +Request errors appear in the top-level `errors` array of the GraphQL response. Domain errors, when using mutation conventions, appear as typed error objects on the mutation payload. This guide covers both patterns in depth. + +# Request Errors + +When a resolver throws an unhandled exception, Hot Chocolate catches it and does two things: the field returns `null`, and an error entry appears in the `errors` array of the response. + +By default, exception details are hidden in production. Instead of exposing the original exception message, the response contains a generic `"Unexpected Execution Error"` message. This prevents leaking internal implementation details to clients. + +```json +{ + "data": { + "userById": null + }, + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [{ "line": 2, "column": 3 }], + "path": ["userById"] + } + ] +} +``` + +During development, if a debugger is attached, Hot Chocolate includes the original exception message and stack trace. You can also enable this behavior explicitly: + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .ModifyRequestOptions(opt => opt.IncludeExceptionDetails = true); +``` + +> **Warning:** Do not enable `IncludeExceptionDetails` in production. Exception messages and stack traces can expose sensitive information about your application internals. + +# Error Filters + +An error filter lets you intercept every error before it reaches the client. Use error filters to log the original exception, sanitize the error message, or add error codes. + +Register an error filter with `AddErrorFilter`. The filter receives an `IError` and must return an `IError`. You can modify the error using its `With*` methods, which return a new `IError` instance with the changed property. + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddErrorFilter(error => + { + if (error.Exception is not null) + { + // Log the original exception for debugging + Console.Error.WriteLine(error.Exception); + + return error + .WithMessage("An internal error occurred. Please try again later.") + .WithCode("INTERNAL_ERROR"); + } + + return error; + }); +``` + +For more complex scenarios, implement the `IErrorFilter` interface as a class. This lets you inject services such as a logger. + +```csharp +// Infrastructure/LoggingErrorFilter.cs +public class LoggingErrorFilter : IErrorFilter +{ + private readonly ILogger _logger; + + public LoggingErrorFilter(ILogger logger) + { + _logger = logger; + } + + public IError OnError(IError error) + { + if (error.Exception is not null) + { + _logger.LogError(error.Exception, "Unhandled exception in resolver."); + + return error + .WithMessage("An internal error occurred.") + .WithCode("INTERNAL_ERROR") + .WithException(null); // strip the exception from the error + } + + return error; + } +} +``` + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddErrorFilter(); +``` + +Multiple error filters can be registered. They run in the order they are added, and each filter receives the output of the previous one. + +# Error Codes + +The `IError` interface supports a `Code` property, which appears under `extensions.code` in the GraphQL response. Error codes let clients handle specific error conditions programmatically without parsing messages. + +```json +{ + "errors": [ + { + "message": "An internal error occurred.", + "locations": [{ "line": 2, "column": 3 }], + "path": ["userById"], + "extensions": { + "code": "INTERNAL_ERROR" + } + } + ] +} +``` + +Set error codes in an error filter using `WithCode`, or build errors with codes from scratch using `ErrorBuilder`: + +```csharp +var error = ErrorBuilder.New() + .SetMessage("Rate limit exceeded.") + .SetCode("RATE_LIMITED") + .Build(); +``` + +# Domain Errors with Mutation Conventions + +Domain errors are the primary mechanism for communicating business logic failures to clients. When mutation conventions are enabled, you annotate mutations with `[Error]` attributes. Hot Chocolate catches the declared exception types and maps them to typed error objects on the mutation payload. + +This keeps domain errors separate from request errors: they appear on the payload, not in the top-level `errors` array, and clients can query them with specific fields and types. + +For mutation conventions setup, see [Mutations](/docs/hotchocolate/v16/building-a-schema/mutations#mutation-conventions). + +## Map Exceptions Directly + +The most straightforward approach is to annotate the mutation with the exception type. The exception's `Message` property becomes the error message. + + + + +```csharp +// Exceptions/UserNameTakenException.cs +public class UserNameTakenException : Exception +{ + public UserNameTakenException(string username) + : base($"The username '{username}' is already taken.") + { + Username = username; + } + + public string Username { get; } +} +``` + +```csharp +// Types/UserMutations.cs +[MutationType] +public static partial class UserMutations +{ + [Error(typeof(UserNameTakenException))] + [Error(typeof(InvalidUserNameException))] + public static async Task UpdateUserNameAsync( + [ID] Guid userId, + string username, + UserService users, + CancellationToken ct) + => await users.UpdateNameAsync(userId, username, ct); +} +``` + + + + +```csharp +// Types/UserMutationsType.cs +public class UserMutationsType : ObjectType +{ + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor + .Field(f => f.UpdateUserNameAsync( + default, default!, default!, default)) + .Argument("userId", a => a.ID()) + .Error() + .Error(); + } +} +``` + + + + +Hot Chocolate rewrites the exception class name for the schema: `UserNameTakenException` becomes `UserNameTakenError`. The generated schema looks like this: + +```graphql +type UpdateUserNamePayload { + user: User + errors: [UpdateUserNameError!] +} + +union UpdateUserNameError = UserNameTakenError | InvalidUserNameError + +type UserNameTakenError implements Error { + message: String! +} + +interface Error { + message: String! +} +``` + +## Map with a Factory Method + +When you need control over the error shape, or want to hide internal details from the exception, create a dedicated error class with a static `CreateErrorFrom` method. Hot Chocolate discovers this method by convention. + +```csharp +// Errors/UserNameTakenError.cs +public class UserNameTakenError +{ + private UserNameTakenError(string message, string username) + { + Message = message; + Username = username; + } + + public string Message { get; } + + public string Username { get; } + + public static UserNameTakenError CreateErrorFrom(UserNameTakenException ex) + => new($"The username '{ex.Username}' is already taken.", ex.Username); +} +``` + +Then reference the error class instead of the exception: + +```csharp +// Types/UserMutations.cs +[MutationType] +public static partial class UserMutations +{ + [Error(typeof(UserNameTakenError))] + public static async Task UpdateUserNameAsync( + [ID] Guid userId, + string username, + UserService users, + CancellationToken ct) + => await users.UpdateNameAsync(userId, username, ct); +} +``` + +A single error class can handle multiple exception types by defining multiple `CreateErrorFrom` overloads: + +```csharp +// Errors/UserValidationError.cs +public class UserValidationError +{ + private UserValidationError(string message) => Message = message; + + public string Message { get; } + + public static UserValidationError CreateErrorFrom(UserNameTakenException ex) + => new($"The username '{ex.Username}' is already taken."); + + public static UserValidationError CreateErrorFrom(InvalidUserNameException ex) + => new($"The username is invalid: {ex.Reason}"); +} +``` + +## Map with a Constructor + +Alternatively, give the error class a constructor that accepts the exception. + +```csharp +// Errors/UserNameTakenError.cs +public class UserNameTakenError +{ + public UserNameTakenError(UserNameTakenException ex) + { + Message = $"The username '{ex.Username}' is already taken."; + Username = ex.Username; + } + + public string Message { get; } + + public string Username { get; } +} +``` + +## Factory with Dependency Injection + +For error factories that need access to services (such as a localizer or a logger), implement the `IPayloadErrorFactory` interface. Hot Chocolate resolves the factory from the DI container. + +```csharp +// Errors/UserNameTakenErrorFactory.cs +public class UserNameTakenErrorFactory + : IPayloadErrorFactory +{ + private readonly IStringLocalizer _localizer; + + public UserNameTakenErrorFactory(IStringLocalizer localizer) + { + _localizer = localizer; + } + + public UserNameTakenError CreateErrorFrom(UserNameTakenException exception) + => new(_localizer["UserNameTaken", exception.Username]); +} +``` + +Register the factory in the DI container: + +```csharp +// Program.cs +builder.Services + .AddSingleton, + UserNameTakenErrorFactory>(); +``` + +## Returning Multiple Errors + +A mutation can return multiple domain errors at once by throwing an `AggregateException`. Hot Chocolate unwraps it and maps each inner exception to its corresponding error type. + +```csharp +// Services/UserService.cs +public async Task UpdateNameAsync( + Guid userId, string username, CancellationToken ct) +{ + var errors = new List(); + + if (username.Length < 3) + errors.Add(new InvalidUserNameException("Must be at least 3 characters.")); + + if (await IsUserNameTakenAsync(username, ct)) + errors.Add(new UserNameTakenException(username)); + + if (errors.Count > 0) + throw new AggregateException(errors); + + // ... proceed with update +} +``` + +## Sharing Errors Across Mutations + +Error classes and error factories are not tied to a specific mutation. You can reuse the same `[Error(typeof(...))]` annotation across multiple mutation methods. This keeps your error types consistent and avoids duplication. + +```csharp +[MutationType] +public static partial class UserMutations +{ + [Error(typeof(UserNameTakenError))] + public static async Task UpdateUserNameAsync(/* ... */) { /* ... */ } + + [Error(typeof(UserNameTakenError))] + public static async Task CreateUserAsync(/* ... */) { /* ... */ } +} +``` + +# Custom Error Interface + +By default, mutation convention errors implement an `Error` interface with a single `message` field. You can replace this interface to require additional fields such as `code`. + + + + +```csharp +// Types/IUserError.cs +[GraphQLName("UserError")] +public interface IUserError +{ + string Message { get; } + string Code { get; } +} +``` + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddMutationConventions(applyToAllMutations: true) + .AddErrorInterfaceType(); +``` + + + + +```csharp +// Types/CustomErrorInterfaceType.cs +public class CustomErrorInterfaceType : InterfaceType +{ + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("UserError"); + descriptor.Field("message").Type>(); + descriptor.Field("code").Type>(); + } +} +``` + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddMutationConventions(applyToAllMutations: true) + .AddErrorInterfaceType(); +``` + + + + +All error types must declare every field required by the interface. They do not need to implement the C# interface, but they must have matching properties. + +```csharp +// Errors/UserNameTakenError.cs +public class UserNameTakenError +{ + public UserNameTakenError(UserNameTakenException ex) + { + Message = $"The username '{ex.Username}' is already taken."; + Code = "USERNAME_TAKEN"; + } + + public string Message { get; } + public string Code { get; } +} +``` + +The generated schema now requires both fields on every error type: + +```graphql +interface UserError { + message: String! + code: String! +} + +type UserNameTakenError implements UserError { + message: String! + code: String! +} +``` + +# Errors Outside Mutations + +Query and subscription resolvers do not use mutation conventions, so domain errors work differently. You have several options. + +## Report an Error and Return Null + +Use `ReportError` on `IResolverContext` to add an error to the response while still returning data (or `null`) from the resolver. The error appears in the top-level `errors` array. + +```csharp +// Types/UserQueries.cs +[QueryType] +public static partial class UserQueries +{ + public static User? GetUserByEmail( + string email, + UserService users, + IResolverContext context) + { + var user = users.FindByEmail(email); + + if (user is null) + { + context.ReportError( + ErrorBuilder.New() + .SetMessage($"No user found with email '{email}'.") + .SetCode("USER_NOT_FOUND") + .Build()); + return null; + } + + return user; + } +} +``` + +`ReportError` has three overloads: + +- `ReportError(string errorMessage)` for quick error messages. +- `ReportError(IError error)` for fully constructed error objects. +- `ReportError(Exception exception, Action? configure)` for reporting caught exceptions with optional customization. + +## Use a Result Union + +For queries where you need typed error handling similar to mutation conventions, return a union type. The client can then use inline fragments to handle each case. + + + + +```csharp +// Types/UserQueries.cs +[QueryType] +public static partial class UserQueries +{ + public static IUserByEmailResult GetUserByEmail( + string email, + UserService users) + { + var user = users.FindByEmail(email); + + if (user is null) + return new UserNotFoundError($"No user found with email '{email}'."); + + return user; + } +} +``` + +```csharp +// Types/UserNotFoundError.cs +public record UserNotFoundError(string Message); +``` + +```csharp +// Types/IUserByEmailResult.cs +[UnionType("UserByEmailResult")] +public interface IUserByEmailResult; + +// Make User and UserNotFoundError implement the interface +public partial class User : IUserByEmailResult { } +public partial record UserNotFoundError : IUserByEmailResult; +``` + + + + +```csharp +// Types/UserByEmailResultType.cs +public class UserByEmailResultType : UnionType +{ + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("UserByEmailResult"); + descriptor.Type(); + descriptor.Type(); + } +} +``` + + + + +# Troubleshooting + +## Exception messages not showing in responses + +By default, Hot Chocolate replaces exception messages with `"Unexpected Execution Error"` when no debugger is attached. This is a security measure. To see the original messages during development, enable `IncludeExceptionDetails`: + +```csharp +builder.Services + .AddGraphQLServer() + .ModifyRequestOptions(opt => opt.IncludeExceptionDetails = true); +``` + +In production, use an [error filter](#error-filters) to log the original exception and return a safe message to the client. + +## Error types not appearing in the schema + +Verify that mutation conventions are enabled. Domain errors on payloads require mutation conventions to rewrite the schema. + +```csharp +builder.Services + .AddGraphQLServer() + .AddMutationConventions(applyToAllMutations: true); +``` + +Also check that the `[Error(typeof(...))]` attribute is on the mutation method, not on the class. + +## AggregateException not unwrapping into multiple errors + +Hot Chocolate unwraps `AggregateException` automatically, but only for exception types declared with `[Error]` on the mutation. If an inner exception type is not declared, it is treated as a request error and appears in the top-level `errors` array instead. + +Make sure every exception type inside the `AggregateException` has a corresponding `[Error]` attribute on the mutation. + +## Error class missing required interface fields + +If you have a custom error interface (e.g., one that requires both `message` and `code`), every error class must expose matching properties. If a property is missing, schema generation fails. Check that each error class has all the properties defined by the interface. + +# Next Steps + +- **Need mutation conventions?** See [Mutations](/docs/hotchocolate/v16/building-a-schema/mutations) for the full pattern including inputs, payloads, and naming customization. +- **Need to build a schema?** See [Schema Basics](/docs/hotchocolate/v16/building-a-schema/schema-basics) for an overview of how types, queries, and mutations fit together. +- **Need to fetch data?** See [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/dataloader) for efficient data fetching patterns. diff --git a/website/src/docs/hotchocolate/v16/guides/mcp-adapter.md b/website/src/docs/hotchocolate/v16/guides/mcp-adapter.md new file mode 100644 index 00000000000..32b68df8316 --- /dev/null +++ b/website/src/docs/hotchocolate/v16/guides/mcp-adapter.md @@ -0,0 +1,331 @@ +--- +title: "MCP Adapter" +--- + +The MCP (Model Context Protocol) adapter exposes your Hot Chocolate GraphQL schema as MCP tools, prompts, and resources. AI agents and LLMs that support MCP can discover and call your GraphQL operations without any manual tool definition. The adapter reads your schema, converts GraphQL operations into MCP tool definitions with full input and output JSON schemas, and serves them over the MCP protocol. + +This is useful when you want AI assistants to interact with your existing GraphQL API. Instead of writing custom integrations, you register the adapter and it handles the translation between MCP and GraphQL automatically. + +# Setup + +Install the `HotChocolate.Adapters.Mcp` package: + +```bash +dotnet add package HotChocolate.Adapters.Mcp +``` + +Register the MCP adapter on your GraphQL server and map the MCP endpoint: + +```csharp +// Program.cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddGraphQL() + .AddQueryType() + .AddMutationType() + .AddMcp() + .AddMcpStorage(myStorage); + +var app = builder.Build(); + +app.UseRouting(); +app.UseEndpoints(endpoints => endpoints.MapGraphQLMcp()); + +app.Run(); +``` + +`AddMcp()` registers the MCP protocol handlers and directive types. `AddMcpStorage()` provides the tool and prompt definitions. `MapGraphQLMcp()` maps the MCP endpoint at `/graphql/mcp` by default. + +# Tool Definitions + +Each MCP tool is defined by a GraphQL operation document. You create an `OperationToolDefinition` with a parsed GraphQL document that contains exactly one operation: + +```csharp +// Services/MyMcpStorage.cs +using HotChocolate.Adapters.Mcp.Storage; +using HotChocolate.Language; + +var tool = new OperationToolDefinition( + Utf8GraphQLParser.Parse( + """ + query GetBooks { + books { + title + } + } + """)); +``` + +The adapter derives the tool name from the operation name, converting it to `snake_case`. The operation `GetBooks` becomes the MCP tool `get_books`. The operation's variable definitions become the tool's input parameters, and the selected fields define the output schema. + +# How It Works + +The adapter translates between GraphQL and MCP concepts: + +| GraphQL Concept | MCP Concept | +| --------------------- | ------------------------------------- | +| Query operation | Read-only tool (`readOnlyHint: true`) | +| Mutation operation | Tool (`readOnlyHint: false`) | +| Operation variables | Tool input parameters (JSON Schema) | +| Selected fields | Tool output schema (JSON Schema) | +| Operation name | Tool name (converted to `snake_case`) | +| Operation description | Tool description | + +When an AI agent calls an MCP tool, the adapter takes the JSON arguments, maps them to GraphQL variables, executes the operation against your schema, and returns the result as structured JSON content. The response includes both `data` and `errors` fields, following the standard GraphQL response format. + +# Storage + +The `IMcpStorage` interface provides tool and prompt definitions to the adapter. You implement this interface to load definitions from any source: a file system, database, or in-memory collection. + +```csharp +// Services/FileMcpStorage.cs +using HotChocolate.Adapters.Mcp.Storage; + +public class FileMcpStorage : IMcpStorage +{ + public async ValueTask> + GetOperationToolDefinitionsAsync( + CancellationToken cancellationToken = default) + { + // Load tool definitions from your preferred source. + var graphql = await File.ReadAllTextAsync( + "tools/get-books.graphql", cancellationToken); + var document = Utf8GraphQLParser.Parse(graphql); + return [new OperationToolDefinition(document)]; + } + + public ValueTask> + GetPromptDefinitionsAsync( + CancellationToken cancellationToken = default) + { + return ValueTask.FromResult( + Enumerable.Empty()); + } + + // IMcpStorage also extends IObservable for both tool and + // prompt events, enabling hot-reload when definitions change. + public IDisposable Subscribe( + IObserver observer) + => /* your subscription logic */; + + public IDisposable Subscribe( + IObserver observer) + => /* your subscription logic */; +} +``` + +The storage implements `IObservable` and `IObservable`. When you push change events through these observables, connected MCP clients receive a `tools/list_changed` or `prompts/list_changed` notification. This enables hot-reload: you can add, update, or remove tools at runtime without restarting the server. + +# Tool Annotations + +MCP tool annotations provide hints to AI agents about a tool's behavior. The adapter infers default annotations from the operation type, but you can override them. + +**Default behavior:** + +- Mutation operations default to `destructiveHint: true` and `idempotentHint: false`. +- Query operations default to `readOnlyHint: true`. + +**Override on the tool definition:** + +```csharp +var tool = new OperationToolDefinition( + Utf8GraphQLParser.Parse( + """ + mutation AddBook($title: String!) { + addBook(title: $title) { title } + } + """)) +{ + DestructiveHint = false, + IdempotentHint = true, + OpenWorldHint = false +}; +``` + +**Override in the GraphQL schema:** + +You can annotate resolver methods with `[McpToolAnnotations]` to set hints at the schema level: + +```csharp +// Types/Mutation.cs +using HotChocolate.Adapters.Mcp.Directives; + +public class Mutation +{ + [McpToolAnnotations(DestructiveHint = false, IdempotentHint = true)] + public Book AddBook(string title) => new(title); +} +``` + +You can also apply annotations using the fluent descriptor API: + +```csharp +// Types/MutationType.cs +using HotChocolate.Adapters.Mcp.Extensions; + +public class MutationType : ObjectType +{ + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor + .Field(m => m.AddBook(default!)) + .McpToolAnnotations( + destructiveHint: false, + idempotentHint: true); + } +} +``` + +Annotations set on the `OperationToolDefinition` take priority over annotations set in the schema. + +# Tool Customization + +You can customize the display title and icons of a tool: + +```csharp +var tool = new OperationToolDefinition( + Utf8GraphQLParser.Parse("query GetBooks { books { title } }")) +{ + Title = "Search Books", + Icons = + [ + new IconDefinition(new Uri("https://example.com/books.png")) + { + MimeType = "image/png", + Sizes = ["48x48"], + Theme = "light" + } + ] +}; +``` + +# Prompts + +The adapter supports MCP prompts, which are reusable prompt templates that AI agents can discover and use. Define prompts through your `IMcpStorage` implementation: + +````csharp +var prompt = new PromptDefinition("code_review") +{ + Title = "Code Review", + Description = + "Asks the LLM to analyze code quality and suggest improvements.", + Arguments = + [ + new PromptArgumentDefinition("code") + { + Title = "Code to Review", + Description = "The code to review", + Required = true + } + ], + Messages = + [ + new PromptMessageDefinition( + RoleDefinition.User, + new TextContentBlockDefinition( + """ + Please review this code: + + ``` + {code} + ``` + """)) + ] +}; +```` + +# Custom MCP Server Options + +You can configure the underlying MCP server options and add custom (non-GraphQL) tools through the `AddMcp` overload: + +```csharp +// Program.cs +builder.Services + .AddGraphQL() + .AddQueryType() + .AddMcp( + configureServerOptions: options => + { + options.InitializationTimeout = TimeSpan.FromSeconds(10); + }, + configureServer: server => + { + server.WithTools([typeof(MyCustomTool)]); + }) + .AddMcpStorage(myStorage); +``` + +Custom tools registered through `configureServer` appear alongside the GraphQL-derived tools. This lets you mix GraphQL operations and native MCP tools in the same server. + +# Endpoint Configuration + +The MCP endpoint defaults to `/graphql/mcp`. You can change the path: + +```csharp +app.UseEndpoints(endpoints => +{ + endpoints.MapGraphQLMcp("/api/mcp"); +}); +``` + +The adapter supports two MCP transport modes: + +- **Streamable HTTP** (default): POST, GET, and DELETE on the base path. +- **HTTP with SSE** (legacy): GET on `/sse` and POST on `/message` sub-paths. + +In stateless mode, only the Streamable HTTP POST endpoint is available. + +# Fusion Integration + +The MCP adapter works with Fusion gateway servers. Instead of `AddGraphQL()`, use `AddGraphQLGatewayServer()` and the rest of the configuration remains the same: + +```csharp +// Program.cs +builder.Services + .AddGraphQLGatewayServer() + .AddInMemoryConfiguration(compositeSchema) + .AddHttpClientConfiguration("Subgraph", subgraphUri) + .AddMcp() + .AddMcpStorage(myStorage); +``` + +When the Fusion gateway schema changes (for example, a new subgraph is composed), connected MCP clients receive tool list changed notifications automatically. + +# Troubleshooting + +**"You must call AddMcp()" error when starting the application** + +You called `MapGraphQLMcp()` without registering the MCP services. Add `.AddMcp()` and `.AddMcpStorage()` to your GraphQL builder chain before calling `MapGraphQLMcp()`. + +**Tools do not appear in the MCP client** + +Verify that your `IMcpStorage` implementation returns the tool definitions from `GetOperationToolDefinitionsAsync`. If a tool's GraphQL operation references fields that do not exist on the schema, the adapter silently skips it and logs validation errors. Attach an `McpDiagnosticEventListener` to inspect validation errors: + +```csharp +builder.Services + .AddGraphQL() + .AddMcp() + .AddMcpStorage(myStorage) + .AddDiagnosticEventListener(_ => new MyMcpListener()); + +public class MyMcpListener : McpDiagnosticEventListener +{ + public override void ValidationErrors(IReadOnlyList errors) + { + foreach (var error in errors) + { + Console.WriteLine(error.Message); + } + } +} +``` + +**Tool list does not update after changing storage** + +Ensure your `IMcpStorage` implementation pushes events through the `IObservable` interface when definitions change. Without these events, connected MCP clients are not notified. + +# Next Steps + +- [Error Handling](/docs/hotchocolate/v16/guides/error-handling) to customize how GraphQL errors appear in MCP tool results. diff --git a/website/src/docs/hotchocolate/v16/guides/openapi-adapter.md b/website/src/docs/hotchocolate/v16/guides/openapi-adapter.md new file mode 100644 index 00000000000..68ce5f8b843 --- /dev/null +++ b/website/src/docs/hotchocolate/v16/guides/openapi-adapter.md @@ -0,0 +1,307 @@ +--- +title: "OpenAPI Adapter" +--- + +The OpenAPI adapter exposes your Hot Chocolate GraphQL schema as REST endpoints with automatic OpenAPI documentation. You define GraphQL operations annotated with `@http` directives, and the adapter generates HTTP endpoints that accept REST-style requests, execute the underlying GraphQL operation, and return the result as JSON. The generated endpoints appear in your OpenAPI specification alongside any other ASP.NET Core endpoints. + +This is useful when you have a GraphQL API and need to provide a REST interface for clients that do not support GraphQL, or when you want to offer both GraphQL and REST access to the same backend. + +# Setup + +Install the `HotChocolate.Adapters.OpenApi` package: + +```bash +dotnet add package HotChocolate.Adapters.OpenApi +``` + +Register the adapter on your GraphQL server and map the endpoints: + +```csharp +// Program.cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddRouting() + .AddOpenApi(options => options.AddGraphQLTransformer()); + +builder.Services + .AddGraphQLServer() + .AddQueryType() + .AddMutationType() + .AddOpenApiDefinitionStorage(myStorage); + +var app = builder.Build(); + +app.UseRouting(); +app.UseEndpoints(endpoints => +{ + endpoints.MapOpenApi(); + endpoints.MapOpenApiEndpoints(); + endpoints.MapGraphQL(); +}); + +app.Run(); +``` + +`AddOpenApiDefinitionStorage()` registers the adapter services and provides the endpoint definitions. `AddGraphQLTransformer()` adds a document transformer that injects the generated endpoints into your OpenAPI specification. `MapOpenApiEndpoints()` registers the dynamic HTTP endpoints at runtime. + +# Endpoint Definitions + +Each REST endpoint is defined by a GraphQL operation annotated with an `@http` directive. You provide these operations through an `IOpenApiDefinitionStorage` implementation. + +A GET endpoint that fetches a user by ID: + +```graphql +"Fetches a user by their id" +query GetUserById($userId: ID!) + @http(method: GET, route: "/users/{userId}") { + userById(id: $userId) { + id + name + email + } +} +``` + +A POST endpoint that creates a user: + +```graphql +"Creates a user" +mutation CreateUser($user: UserInput! @body) + @http(method: POST, route: "/users") { + createUser(user: $user) { + id + name + email + } +} +``` + +The `@http` directive specifies the HTTP method and route. Route parameters like `{userId}` map to GraphQL variables. The `@body` directive on a variable indicates that the HTTP request body maps to that variable. + +# How It Works + +The adapter translates between REST and GraphQL concepts: + +| REST Concept | GraphQL Concept | +| ---------------------------- | ------------------------------------- | +| HTTP method (GET, POST, PUT) | Specified by `@http(method: ...)` | +| Route path | `@http(route: "/path/{param}")` | +| Route parameters | GraphQL variables matched by name | +| Query parameters | Variables listed in `queryParameters` | +| Request body | Variable annotated with `@body` | +| Response body | Selected fields from the operation | + +When a client sends an HTTP request to a generated endpoint, the adapter extracts route parameters, query parameters, and the request body, maps them to GraphQL variables, executes the operation, and returns the root field's data as the response body. + +# Route Parameters + +Route parameters in curly braces map to GraphQL variables by name: + +```graphql +query GetUser($userId: ID!) @http(method: GET, route: "/users/{userId}") { + userById(id: $userId) { + id + name + } +} +``` + +A request to `GET /users/42` sets `$userId` to `"42"`. + +You can also map route parameters to nested fields of a variable using the `key:$variable.path` syntax: + +```graphql +mutation UpdateUser($user: UserInput! @body) +@http(method: PUT, route: "/users/{userId:$user.id}") { + updateUser(user: $user) { + id + name + } +} +``` + +A PUT request to `/users/42` with a JSON body sets the `id` field of the `$user` variable to `"42"`, and the rest of the body fills in the remaining fields. + +# Query Parameters + +Use the `queryParameters` argument on the `@http` directive to expose GraphQL variables as URL query parameters: + +```graphql +query GetUserDetails($userId: ID!, $includeAddress: Boolean!) +@http( + method: GET + route: "/users/{userId}/details" + queryParameters: ["includeAddress"] +) { + userById(id: $userId) { + id + name + address @include(if: $includeAddress) { + street + } + } +} +``` + +A request to `GET /users/1/details?includeAddress=true` sets `$includeAddress` to `true`. + +Query parameters support the same `key:$variable.path` mapping syntax as route parameters. + +# Request Body + +The `@body` directive on a variable maps the entire HTTP request body to that variable: + +```graphql +mutation CreateUser($user: UserInput! @body) +@http(method: POST, route: "/users") { + createUser(user: $user) { + id + name + email + } +} +``` + +A POST request with a JSON body `{"id": "6", "name": "Alice", "email": "alice@example.com"}` sets `$user` to that object. The request must have a `Content-Type: application/json` header. + +# Shared Fragments + +You can define reusable GraphQL fragments as separate documents. The adapter resolves fragment references across documents: + +```graphql +-- Document 1: endpoint definition +query GetUser($userId: ID!) + @http(method: GET, route: "/users/{userId}") { + userById(id: $userId) { + ...UserFields + } +} + +-- Document 2: shared fragment +fragment UserFields on User { + id + name + email + address { + ...AddressFields + } +} + +-- Document 3: another shared fragment +fragment AddressFields on Address { + street +} +``` + +Each document is a separate entry in your `IOpenApiDefinitionStorage`. Fragment-only documents are treated as shared models. + +# Storage + +The `IOpenApiDefinitionStorage` interface provides endpoint and fragment definitions to the adapter: + +```csharp +// Services/MyOpenApiStorage.cs +using HotChocolate.Adapters.OpenApi; +using HotChocolate.Language; + +public class MyOpenApiStorage : IOpenApiDefinitionStorage +{ + public event EventHandler? Changed; + + public ValueTask> + GetDefinitionsAsync( + CancellationToken cancellationToken = default) + { + var documents = new List(); + + var getUserDoc = Utf8GraphQLParser.Parse( + """ + query GetUser($userId: ID!) + @http(method: GET, route: "/users/{userId}") { + userById(id: $userId) { + id + name + } + } + """); + documents.Add(OpenApiDefinitionParser.Parse(getUserDoc)); + + return ValueTask.FromResult>( + documents); + } +} +``` + +Register it with your GraphQL server: + +```csharp +// Program.cs +var storage = new MyOpenApiStorage(); + +builder.Services + .AddGraphQLServer() + .AddQueryType() + .AddOpenApiDefinitionStorage(storage); +``` + +The storage raises its `Changed` event when definitions are modified. The adapter picks up changes at runtime, adding, updating, or removing HTTP endpoints without a restart. This hot-reload behavior extends to the OpenAPI specification. + +# OpenAPI Specification + +The adapter integrates with ASP.NET Core's built-in OpenAPI support. After you register `AddOpenApi(options => options.AddGraphQLTransformer())`, the generated endpoints appear in the OpenAPI document at `/openapi/v1.json`. + +Each endpoint definition's description becomes the OpenAPI operation summary. Route and query parameters become OpenAPI parameters with types inferred from the GraphQL schema. Request body schemas are generated from the GraphQL input types. + +# Fusion Integration + +The OpenAPI adapter works with Fusion gateway servers. Replace `AddGraphQLServer()` with `AddGraphQLGatewayServer()` and the rest of the configuration remains the same: + +```csharp +// Program.cs +builder.Services + .AddGraphQLGatewayServer() + .AddInMemoryConfiguration(compositeSchema) + .AddHttpClientConfiguration("Subgraph", subgraphUri) + .AddOpenApiDefinitionStorage(myStorage); +``` + +The Fusion gateway composes schemas from multiple subgraphs. The OpenAPI adapter generates REST endpoints that execute operations against the composed schema, so a single REST endpoint can fetch data from multiple subgraphs transparently. + +# Troubleshooting + +**Endpoint returns 404** + +Verify that your `IOpenApiDefinitionStorage` returns the definitions and that the route in the `@http` directive matches the URL you are requesting. Check that you called both `MapOpenApiEndpoints()` and `MapGraphQL()` in your endpoint configuration. If you added a definition at runtime, wait for the hot-reload cycle to complete. + +**Request body is not parsed** + +The adapter requires a `Content-Type: application/json` header on POST and PUT requests. Other content types are rejected. Ensure the `@body` directive is present on the variable that should receive the request body. + +**Endpoint returns 500 with validation errors** + +The operation references a field or type that does not exist on the GraphQL schema. The adapter validates definitions on startup and logs errors. Attach an `OpenApiDiagnosticEventListener` to inspect validation details: + +```csharp +builder.Services + .AddGraphQLServer() + .AddOpenApiDefinitionStorage(myStorage) + .AddDiagnosticEventListener(_ => new MyOpenApiListener()); + +public class MyOpenApiListener : OpenApiDiagnosticEventListener +{ + public override void ValidationErrors( + IReadOnlyList errors) + { + foreach (var error in errors) + { + Console.WriteLine(error.Message); + } + } +} +``` + +# Next Steps + +- [MCP Adapter](/docs/hotchocolate/v16/guides/mcp-adapter) to expose your GraphQL schema as MCP tools for AI agents. +- [Error Handling](/docs/hotchocolate/v16/guides/error-handling) to customize error responses in generated endpoints. diff --git a/website/src/docs/hotchocolate/v16/guides/performance.md b/website/src/docs/hotchocolate/v16/guides/performance.md new file mode 100644 index 00000000000..4fc61a298d8 --- /dev/null +++ b/website/src/docs/hotchocolate/v16/guides/performance.md @@ -0,0 +1,251 @@ +--- +title: "Performance Tuning" +--- + +Hot Chocolate is designed for high throughput out of the box. The schema is built eagerly at startup, operations are cached after their first execution, and DataLoaders batch database calls automatically. This guide covers the tuning options available when you need to go further. + +# Warmup + +The schema is constructed eagerly at startup by default. You can go a step further and register warmup tasks that pre-populate in-memory caches before the server starts accepting traffic. This eliminates cold-start latency for the first requests after deployment. + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddWarmupTask(async (executor, cancellationToken) => + { + var request = OperationRequestBuilder.New() + .SetDocument("query GetProducts { products(first: 10) { nodes { id name } } }") + .SetOperationName("GetProducts") + .MarkAsWarmupRequest() + .Build(); + + await executor.ExecuteAsync(request, cancellationToken); + }); +``` + +`MarkAsWarmupRequest()` populates the document and operation caches without executing the operation, which avoids side effects during startup. Include the operation name in the warmup request because it is part of the cache key. + +[Learn more about server warmup](/docs/hotchocolate/v16/server/warmup) + +# Operation Caching + +Hot Chocolate caches parsed and compiled operations so that repeated requests skip parsing and validation. Two cache sizes control this behavior: + +- `PreparedOperationCacheSize` controls the compiled operation cache (default `256`, minimum `16`). +- `OperationDocumentCacheSize` controls the parsed document cache (default `256`, minimum `16`). + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .ModifyOptions(options => + { + options.PreparedOperationCacheSize = 1024; + options.OperationDocumentCacheSize = 1024; + }); +``` + +In v16, 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. + +# DataLoader Batching + +DataLoaders collect individual fetch requests during resolver execution and dispatch them as a single batch. This turns N+1 database queries into one query per batch. Hot Chocolate manages DataLoader lifecycle automatically within each request scope. + +The default `MaxBatchSize` for DataLoaders is `1024`. If your data source has a lower limit on batch sizes (for example, a SQL `IN` clause limit), you can adjust this through `DataLoaderOptions` when creating a manual DataLoader class: + +```csharp +// DataLoaders/ProductByIdDataLoader.cs +public class ProductByIdDataLoader : BatchDataLoader +{ + private readonly IDbContextFactory _dbContextFactory; + + public ProductByIdDataLoader( + IDbContextFactory dbContextFactory, + IBatchScheduler batchScheduler, + DataLoaderOptions options) + : base(batchScheduler, options) + { + _dbContextFactory = dbContextFactory; + } + + protected override async Task> LoadBatchAsync( + IReadOnlyList keys, + CancellationToken cancellationToken) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + return await db.Products + .Where(p => keys.Contains(p.Id)) + .ToDictionaryAsync(p => p.Id, cancellationToken); + } +} +``` + +For most applications, the source-generated DataLoader approach (using the `[DataLoader]` attribute) is the recommended starting point. + +[Learn more about DataLoaders](/docs/hotchocolate/v16/resolvers-and-data/dataloader) + +# Projections and Database Efficiency + +Use `[UseProjection]` to translate GraphQL field selections into database-level `SELECT` clauses. When a client requests only `name` and `email`, Hot Chocolate queries only those columns from the database rather than loading entire entities. + +```csharp +// Types/UserQueries.cs +[QueryType] +public static partial class UserQueries +{ + [UseProjection] + [UseFiltering] + [UseSorting] + public static IQueryable GetUsers(CatalogContext db) + => db.Users; +} +``` + +Combine `[UseProjection]` with `[UseFiltering]` and `[UseSorting]` to push filtering and ordering down to the database as well. Apply them in this order: `UsePaging` > `UseProjection` > `UseFiltering` > `UseSorting`. + +As an alternative to middleware stacking, `QueryContext` integrates projection, filtering, and sorting into a single return type: + +```csharp +// Types/UserQueries.cs +[QueryType] +public static partial class UserQueries +{ + public static QueryContext GetUsers(CatalogContext db) + => db.Users.AsQueryContext(); +} +``` + +Do not combine `QueryContext` with `[UseProjection]` on the same field. The HC0099 analyzer warns when both are present. + +[Learn more about projections](/docs/hotchocolate/v16/resolvers-and-data/projections) + +# Cost Analysis for Resource Protection + +Cost analysis calculates the cost of every operation before execution and rejects operations that exceed your budget. Even on private APIs, cost analysis protects against accidentally expensive operations during development. It catches runaway queries before they reach production. + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .ModifyCostOptions(options => + { + options.MaxFieldCost = 5_000; + options.MaxTypeCost = 5_000; + options.EnforceCostLimits = true; + }); +``` + +Use the `GraphQL-Cost: report` HTTP header to inspect the cost of any operation without changing enforcement. Send your most complex expected operations and verify they fall within your limits. + +[Learn more about cost analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis) + +# Reduce Response Size + +Large responses increase serialization time, network transfer time, and client parsing time. Two features help you deliver data incrementally. + +## Incremental Delivery with `@defer` and `@stream` + +`@defer` lets clients mark fragments that can arrive after the initial response. `@stream` lets clients receive list items incrementally. Both reduce time-to-first-byte for operations that include expensive or low-priority fields. + +Enable these directives in schema options: + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .ModifyOptions(options => + { + options.EnableDefer = true; + options.EnableStream = true; + }); +``` + +In v16, 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) + +## Persisted Operations + +Persisted operations reduce request size by replacing the full operation document with a hash. This saves bandwidth on every request and lets the server skip parsing for known operations. + +[Learn more about persisted operations](/docs/hotchocolate/v16/performance/trusted-documents) + +# Instrumentation for Bottleneck Detection + +Use OpenTelemetry to find slow resolvers and DataLoaders. Hot Chocolate ships with a built-in OpenTelemetry integration aligned with the proposed GraphQL semantic conventions. + +For custom diagnostics, implement a diagnostic event listener: + +```csharp +// Diagnostics/PerformanceEventListener.cs +public class PerformanceEventListener : ExecutionDiagnosticEventListener +{ + private readonly ILogger _logger; + + public PerformanceEventListener(ILogger logger) + => _logger = logger; + + public override IDisposable ExecuteRequest(RequestContext context) + { + var start = DateTime.UtcNow; + + return new RequestScope(_logger, context, start); + } + + private sealed class RequestScope( + ILogger logger, + RequestContext context, + DateTime start) : IDisposable + { + public void Dispose() + { + var elapsed = DateTime.UtcNow - start; + if (elapsed > TimeSpan.FromMilliseconds(500)) + { + logger.LogWarning( + "Slow request detected: {Document} took {Elapsed}ms", + context.Request.Document, + elapsed.TotalMilliseconds); + } + } + } +} +``` + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddDiagnosticEventListener(); +``` + +Diagnostic event handlers execute synchronously as part of the GraphQL request. Enqueue expensive work (such as writing to an external monitoring service) to a background service to avoid adding latency. + +[Learn more about instrumentation](/docs/hotchocolate/v16/server/instrumentation) + +# Troubleshooting + +## Cache hit rate is low despite stable operations + +The operation name is part of the cache key. If clients send the same document with different operation names, each variant occupies a separate cache slot. Verify that your clients use consistent operation names. If the number of distinct operations exceeds your cache size, increase `PreparedOperationCacheSize`. + +## Projections do not reduce the SQL query + +Verify that the resolver returns `IQueryable` and that `[UseProjection]` is applied. If you are using `QueryContext`, do not also apply `[UseProjection]` on the same field. Check that projected properties have public setters. + +## DataLoader dispatches too many batches + +By default, the batch dispatcher waits up to 50ms before dispatching. If you see many small batches, verify that your resolvers are structured so that sibling fields resolve in the same execution step. Deeply nested resolver chains can cause sequential dispatches rather than batched ones. + +# Next Steps + +- **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/resolvers-and-data/dataloader) covers source-generated DataLoaders, manual DataLoader classes, and batch resolvers. +- **Projections:** [Projections](/docs/hotchocolate/v16/resolvers-and-data/projections) covers the `[UseProjection]` middleware and `QueryContext`. +- **Cost analysis:** [Cost Analysis](/docs/hotchocolate/v16/securing-your-api/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. +- **Configuration reference:** [Options](/docs/hotchocolate/v16/api-reference/options) lists all schema, request, and server options with their defaults. diff --git a/website/src/docs/hotchocolate/v16/guides/private-api.md b/website/src/docs/hotchocolate/v16/guides/private-api.md new file mode 100644 index 00000000000..e3aa2ff82ec --- /dev/null +++ b/website/src/docs/hotchocolate/v16/guides/private-api.md @@ -0,0 +1,392 @@ +--- +title: "Building a private GraphQL API" +description: "An end-to-end guide for building a private, first-party GraphQL API using trusted documents in Hot Chocolate." +--- + +If you control every client that talks to your GraphQL API, you have a significant advantage: you know every operation at build time. You can extract those operations, register them with the server, and configure the server to reject anything it has not seen before. This is how Meta built and operates GraphQL internally, and it is the recommended path for teams building first-party APIs. + +This guide walks you through the full workflow for building a private GraphQL API with Hot Chocolate. A private API serves your own applications: your website, your mobile app, your internal tools. You are not exposing a public endpoint for third-party developers. The key insight is that if you control the clients, you can lock down the server to only accept operations you have reviewed and approved. + +# The trusted documents workflow + +Trusted documents (also called persisted operations) turn your GraphQL API from an open query endpoint into a closed, auditable contract. The workflow has three steps: + +1. **Extract** operations from your client applications at build time. Each operation gets a hash that serves as its unique identifier. +2. **Register** those operations with the server. You place the operation files in a storage location the server can read from, or you use the Nitro client registry. +3. **Configure** the server to only accept registered operations. Any request that does not reference a known operation ID is rejected. + +Once this workflow is in place, the server never parses or executes an operation it has not seen before. Malicious queries, accidental expensive queries, and schema exploration by unauthorized clients are all blocked at the door. + +# Extract operations from your client + +The first step is to get the operations out of your client code and into files the server can consume. Both Relay and Strawberry Shake support this out of the box. + +## Relay + +Relay extracts operations during its compiler step. Configure `relay.config.js` to output persisted operations: + +```js +// relay.config.js +module.exports = { + src: "./src", + schema: "./schema.graphql", + language: "typescript", + persistConfig: { + file: "./persisted-operations/operations.json", + algorithm: "MD5", + }, +}; +``` + +After running the Relay compiler, the `operations.json` file contains a mapping of hashes to operation documents: + +```json +{ + "913abc361487c481cf6015841c0eca22": "query GetUser { me { id name } }", + "0e7cf2125e8eb711b470cc72c73ca77e": "query GetProducts { products { nodes { name price } } }" +} +``` + +[Learn more about Relay persisted queries](https://relay.dev/docs/guides/persisted-queries/) + +## Strawberry Shake + +Strawberry Shake extracts operations as part of the normal .NET build. Add the `GraphQLPersistedOperationOutput` property to your client project: + +```xml + + + ./persisted-operations + +``` + +When you build the project, Strawberry Shake writes one `.graphql` file per operation to the output directory, using the operation hash as the filename: + +```text +persisted-operations/ + 913abc361487c481cf6015841c0eca22.graphql + 0e7cf2125e8eb711b470cc72c73ca77e.graphql +``` + +If your server expects the Relay JSON format instead, set the output format: + +```xml + + + ./persisted-operations + relay + +``` + +[Learn more about Strawberry Shake persisted operations](/docs/strawberryshake/v16/performance/persisted-operations) + +# Register operations with the server + +Once you have the extracted operation files, the server needs access to them. You have two options. + +## Filesystem storage + +For straightforward setups, place the operation files on disk where the server can read them. Install the filesystem storage package: + + + +Point the server at the directory containing your operation files: + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddQueryType() + .UsePersistedOperationPipeline() + .AddFileSystemOperationDocumentStorage("./persisted-operations"); +``` + +Hot Chocolate looks for files named `{hash}.graphql` in the specified directory. Make sure the hashing algorithm matches between client and server. Hot Chocolate defaults to MD5, which matches Relay's default. + +## Client registry (Nitro) + +For production deployments with multiple client versions, CI/CD validation, and schema compatibility checks, use the [Nitro client registry](/docs/nitro/apis/client-registry). The client registry validates operations against your schema on publish and distributes them to your server at runtime. + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddQueryType() + .AddNitro(x => + { + x.ApiKey = "<>"; + x.ApiId = "<>"; + x.Stage = "production"; + }) + .UsePersistedOperationPipeline(); +``` + +The client registry is the recommended approach for teams running multiple services or deploying frequently. It catches operation-schema mismatches in your CI pipeline before they reach production. + +[Learn more about the client registry](/docs/nitro/apis/client-registry) + +# Configure the server + +The `UsePersistedOperationPipeline()` method replaces the default request pipeline with one that resolves operations by ID. Instead of parsing a raw GraphQL document from the request body, the server looks up the operation in its document storage using the hash provided by the client. + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddQueryType() + .UsePersistedOperationPipeline() + .AddFileSystemOperationDocumentStorage("./persisted-operations"); +``` + +With this pipeline active, clients send requests with an `id` field instead of a `query` field: + +```json +{ + "id": "913abc361487c481cf6015841c0eca22", + "variables": { + "userId": "abc123" + } +} +``` + +> Note: Relay uses `doc_id` instead of `id` by default. Update your Relay network layer to send the hash as `id`. + +# Block ad-hoc operations + +The persisted operation pipeline still accepts regular operations alongside persisted ones unless you explicitly block them. To close this gap, enable `OnlyAllowPersistedDocuments`: + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddQueryType() + .UsePersistedOperationPipeline() + .AddFileSystemOperationDocumentStorage("./persisted-operations") + .ModifyRequestOptions(options => + options.PersistedOperations.OnlyAllowPersistedDocuments = true); +``` + +This is the key security boundary of a private API. With this option enabled, any request that does not reference a registered operation ID is rejected with an error. No ad-hoc queries, no introspection queries, no malicious payloads. + +You can customize the error message returned to clients: + +```csharp +// Program.cs +.ModifyRequestOptions(options => +{ + options.PersistedOperations.OnlyAllowPersistedDocuments = true; + options.PersistedOperations.OperationNotAllowedError = ErrorBuilder.New() + .SetMessage("This API only accepts pre-registered operations.") + .Build(); +}) +``` + +# Development workflow + +During development, you need to iterate on operations without going through the full extract-register-deploy cycle every time you change a query. There are two approaches. + +## Automatic persisted operations for development + +Use `UseAutomaticPersistedOperationPipeline()` in your development environment. This lets clients persist operations at runtime: the first request with a new operation stores it, and subsequent requests use the hash. + +```csharp +// Program.cs +if (app.Environment.IsDevelopment()) +{ + builder.Services + .AddMemoryCache() + .AddGraphQLServer() + .AddQueryType() + .UseAutomaticPersistedOperationPipeline() + .AddInMemoryOperationDocumentStorage(); +} +else +{ + builder.Services + .AddGraphQLServer() + .AddQueryType() + .UsePersistedOperationPipeline() + .AddFileSystemOperationDocumentStorage("./persisted-operations") + .ModifyRequestOptions(options => + options.PersistedOperations.OnlyAllowPersistedDocuments = true); +} +``` + +## Developer bypass with an HTTP interceptor + +If you prefer to keep the persisted operation pipeline active during development but allow specific developers to send ad-hoc operations, use a custom HTTP request interceptor: + +```csharp +// Interceptors/DevelopmentRequestInterceptor.cs +public class DevelopmentRequestInterceptor : DefaultHttpRequestInterceptor +{ + public override ValueTask OnCreateAsync( + HttpContext context, + IRequestExecutor requestExecutor, + OperationRequestBuilder requestBuilder, + CancellationToken cancellationToken) + { + if (context.Request.Headers.ContainsKey("X-Developer")) + { + requestBuilder.AllowNonPersistedOperation(); + } + + return base.OnCreateAsync( + context, requestExecutor, requestBuilder, cancellationToken); + } +} +``` + +Register the interceptor: + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddQueryType() + .AddHttpRequestInterceptor() + .UsePersistedOperationPipeline() + .AddFileSystemOperationDocumentStorage("./persisted-operations") + .ModifyRequestOptions(options => + options.PersistedOperations.OnlyAllowPersistedDocuments = true); +``` + +In production, remove the interceptor or gate the bypass behind a proper authorization check. + +# What you can skip + +A private API with trusted documents eliminates entire categories of attack vectors. Because you control every operation that reaches the server, several security measures that public APIs require become unnecessary: + +**Cost analysis.** Cost analysis protects against expensive queries from unknown clients. With trusted documents, every operation has already been reviewed by your team. You know the cost of each operation before it reaches production. + +**Introspection restrictions.** Clients in a trusted documents workflow do not use introspection at runtime. Operations are compiled against the schema at build time using schema files or code generation. Disabling introspection in production is still reasonable as a defense-in-depth measure, but it is no longer a primary security control. + +**Depth limits.** Query depth limits prevent deeply nested queries from consuming excessive resources. With trusted documents, you have reviewed every operation and know its structure. Depth limits add no value when the operation set is fixed. + +**Operation complexity limits.** Similar to cost analysis, complexity limits guard against operations you have not seen before. With trusted documents, there are no surprise operations. + +This is a significant operational win. Your server configuration becomes lighter, your security model becomes simpler, and your team spends less time tuning limits and thresholds. + +# Putting it all together + +Here is a complete `Program.cs` for a private API with trusted documents: + +```csharp +// Program.cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddGraphQLServer() + .AddQueryType() + .AddMutationType() + .AddAuthorization() + .UsePersistedOperationPipeline() + .AddFileSystemOperationDocumentStorage("./persisted-operations") + .ModifyRequestOptions(options => + options.PersistedOperations.OnlyAllowPersistedDocuments = true); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapGraphQL(); + +app.Run(); +``` + +Compare this with a public API that needs cost analysis, depth limits, and introspection controls: + +```csharp +// Program.cs (public API for comparison) +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddGraphQLServer() + .AddQueryType() + .AddMutationType() + .AddAuthorization() + .AddMaxExecutionDepthRule(15) + .ModifyRequestOptions(o => + { + o.ExecutionTimeout = TimeSpan.FromSeconds(30); + }) + .ModifyCostOptions(o => + { + o.MaxFieldCost = 2000; + o.MaxTypeCost = 2000; + o.EnforceCostLimits = true; + }); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapGraphQL(); + +app.Run(); +``` + +The private API setup is shorter, has fewer knobs to tune, and provides a stronger security guarantee. The tradeoff is the build-time extraction step, which your client tooling handles automatically. + +# Troubleshooting + +## "PersistedQueryNotFound" error + +The server cannot find an operation matching the hash sent by the client. Verify that: + +- The operation files are present in the configured storage directory. +- The file names match the format `{hash}.graphql`. +- The server process has read access to the directory. + +If you are using the Nitro client registry, confirm that the client version has been published and that the API ID and stage match your server configuration. + +## Hash mismatch between client and server + +The client and server must use the same hashing algorithm and encoding format. Hot Chocolate defaults to MD5 with hex encoding. If your client uses SHA256 or a different encoding, configure the server to match: + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddSha256DocumentHashProvider(HashFormat.Hex) + .UsePersistedOperationPipeline() + .AddFileSystemOperationDocumentStorage("./persisted-operations"); +``` + +Check your client's build output to see which algorithm and format it uses. + +## Operations blocked during development + +If `OnlyAllowPersistedDocuments` is enabled and you are sending ad-hoc queries during development, the server rejects them. Either switch to `UseAutomaticPersistedOperationPipeline()` for your development environment or add an HTTP request interceptor that calls `AllowNonPersistedOperation()` for developer requests. See [Development workflow](#development-workflow) above. + +## Relay sends "doc_id" instead of "id" + +Relay's default network layer uses `doc_id` as the field name for the operation hash. Hot Chocolate expects `id`. Update your Relay network layer to send `id`: + +```js +// relay-network.js +function fetchQuery(operation, variables) { + return fetch("/graphql", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: operation.id, // not doc_id + variables, + }), + }).then((response) => response.json()); +} +``` + +## Stale operations after schema changes + +When you change your GraphQL schema, previously extracted operations may no longer be valid. Re-run the client build to extract updated operations and redeploy them to the server. The [Nitro client registry](/docs/nitro/apis/client-registry) catches these mismatches automatically during the validation step. + +# Next Steps + +- **Need to set up persisted operations storage?** See [Persisted Operations](/docs/hotchocolate/v16/performance/trusted-documents) for filesystem, Redis, and Azure Blob Storage options. +- **Want runtime operation persistence for development?** See [Automatic Persisted Operations](/docs/hotchocolate/v16/performance/automatic-persisted-operations). +- **Need authentication and authorization?** See [Security Overview](/docs/hotchocolate/v16/security). +- **Managing multiple clients in production?** See [Client Registry](/docs/nitro/apis/client-registry) for versioning, validation, and distribution. +- **Using Strawberry Shake?** See [Strawberry Shake Persisted Operations](/docs/strawberryshake/v16/performance/persisted-operations) for client-side configuration. diff --git a/website/src/docs/hotchocolate/v16/guides/public-api.md b/website/src/docs/hotchocolate/v16/guides/public-api.md new file mode 100644 index 00000000000..f1387401274 --- /dev/null +++ b/website/src/docs/hotchocolate/v16/guides/public-api.md @@ -0,0 +1,405 @@ +--- +title: "Building a Public GraphQL API" +--- + +If you are building a GraphQL API that external developers will consume, this guide walks through the configuration and design decisions that matter most. A public API is one where you publish a schema and cannot control what operations clients send. Think of APIs like GitHub's GraphQL API, where thousands of third-party applications issue queries you never anticipated. + +This guide is opinionated. It covers schema design, pagination, cost analysis, introspection, authorization, and request limits, then ties everything together in a complete `Program.cs` you can use as a starting point. Each section links to the relevant reference page for full details. + +# Start with a Solid Schema Design + +Your schema is a contract. Once external developers build against it, changing or removing fields is a breaking change. Invest time in naming and documentation before you ship. + +**Name fields and types clearly.** Use domain language that makes sense without reading your source code. Avoid abbreviations and internal jargon. A field called `orgMemberships` is harder to discover than `organizationMemberships`. + +**Add descriptions to every type, field, and argument.** Public API consumers rely on introspection and tooling like [Nitro](/products/nitro) to explore your schema. A field without a description is a field that generates support tickets. + +```csharp +// Types/Organization.cs +[GraphQLDescription("A company or group that owns repositories.")] +public class Organization +{ + [GraphQLDescription("The unique login handle for this organization.")] + public string Login { get; set; } + + [GraphQLDescription("The display name of the organization.")] + public string? Name { get; set; } +} +``` + +**Plan for deprecation from day one.** Use `@deprecated` to phase out fields and `@requiresOptIn` to gate experimental features. Never remove a field without a deprecation period. + +[Learn more about schema documentation](/docs/hotchocolate/v16/building-a-schema/documentation) + +[Learn more about versioning and deprecation](/docs/hotchocolate/v16/building-a-schema/versioning) + +# Use Cursor-Based Pagination for All Lists + +Every list field that could grow beyond a handful of items should be a connection. Connections give clients a standardized way to page through results, and they give you control over how much data a single request can fetch. + +```csharp +// Types/OrganizationQueries.cs +[QueryType] +public static partial class OrganizationQueries +{ + [UsePaging(MaxPageSize = 100, DefaultPageSize = 25)] + public static IQueryable GetOrganizations(AppDbContext db) + => db.Organizations.OrderBy(o => o.Login); +} +``` + +Set `MaxPageSize` deliberately. This value is the upper bound on how many items a client can request in a single page, and it feeds directly into cost analysis. A `MaxPageSize` of 100 means cost analysis assumes up to 100 nodes per page when calculating query cost. Lower values give you tighter cost budgets. + +For public APIs, require clients to specify how many items they want by enabling `RequirePagingBoundaries`. Without this, clients that omit `first` or `last` still get results, but cost analysis has to assume the worst case. + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .ModifyPagingOptions(opt => + { + opt.MaxPageSize = 100; + opt.DefaultPageSize = 25; + opt.RequirePagingBoundaries = true; + }); +``` + +[Learn more about pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination) + +# Configure Cost Analysis + +Cost analysis is the most important security layer for a public GraphQL API. It calculates the cost of every query before execution and rejects queries that exceed your budget. Without it, a single deeply nested query can consume unbounded server resources. + +Hot Chocolate enables cost analysis by default. The default limits (`MaxFieldCost = 1000`, `MaxTypeCost = 1000`) work as a starting point, but you should tune them based on your schema and expected query patterns. + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .ModifyCostOptions(options => + { + options.MaxFieldCost = 5_000; + options.MaxTypeCost = 5_000; + options.EnforceCostLimits = true; + }); +``` + +## Default Weights + +Hot Chocolate assigns default cost weights automatically: + +- **Async resolvers** (fields that hit a database or service): weight `10` +- **Composite types** (object fields that resolve synchronously): weight `1` +- **Scalars**: weight `0` + +For paginated fields, these weights multiply by the page size. A resolver with weight `10` inside a connection with `MaxPageSize = 50` contributes `10 x 50 = 500` to the field cost. + +## Annotate Expensive Fields + +If a resolver calls an external API, runs a complex computation, or triggers a database-heavy operation, increase its cost weight: + +```csharp +// Types/ReportQueries.cs +[QueryType] +public static partial class ReportQueries +{ + [Cost(50)] + public static async Task GetSalesReportAsync( + DateOnly from, + DateOnly to, + ReportService reports, + CancellationToken ct) + => await reports.GenerateAsync(from, to, ct); +} +``` + +For list fields where you know the typical size differs from the default, use `[ListSize]` to give the analyzer a more accurate estimate: + +```csharp +// Types/OrganizationNode.cs +[ObjectType] +public static partial class OrganizationNode +{ + [UsePaging(MaxPageSize = 10)] + [ListSize(AssumedSize = 10, SlicingArguments = ["first", "last"], + SizedFields = ["edges", "nodes"])] + public static IQueryable GetTeams( + [Parent] Organization org, AppDbContext db) + => db.Teams.Where(t => t.OrganizationId == org.Id); +} +``` + +## Test with the Cost Header + +Use the `GraphQL-Cost: report` HTTP header to see the cost of any query without changing enforcement. Send your most complex expected queries and verify they fall within your limits before deploying. + +[Learn more about cost analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis) + +# Control Introspection + +Introspection lets anyone discover every type, field, and argument in your schema. For a public API, you have two options: + +**Option A: Keep introspection enabled.** If your API is meant to be discovered and you publish documentation, introspection is a feature, not a risk. Cost analysis already protects you from expensive introspection queries. + +**Option B: Restrict introspection in production.** If you prefer to control schema discovery, disable introspection and allow it only for authorized requests: + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AllowIntrospection(builder.Environment.IsDevelopment()); +``` + +For a more granular approach, use a request interceptor to allow introspection based on authentication or a specific header: + +```csharp +// Interceptors/IntrospectionInterceptor.cs +public class IntrospectionInterceptor : DefaultHttpRequestInterceptor +{ + public override ValueTask OnCreateAsync(HttpContext context, + IRequestExecutor requestExecutor, OperationRequestBuilder requestBuilder, + CancellationToken cancellationToken) + { + if (context.User.Identity?.IsAuthenticated == true) + { + requestBuilder.AllowIntrospection(); + } + + return base.OnCreateAsync(context, requestExecutor, requestBuilder, + cancellationToken); + } +} +``` + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AllowIntrospection(false) + .AddHttpRequestInterceptor(); +``` + +[Learn more about introspection](/docs/hotchocolate/v16/securing-your-api/introspection) + +# Set Up Authorization + +Most public APIs have fields that require authentication or specific permissions. Use the `[Authorize]` attribute to protect sensitive types and fields. + +```csharp +// Types/ViewerQueries.cs +[QueryType] +public static partial class ViewerQueries +{ + [Authorize] + public static async Task GetViewerAsync( + ClaimsPrincipal claimsPrincipal, + UserService users, + CancellationToken ct) + { + var userId = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier); + return userId is not null ? await users.GetByIdAsync(userId, ct) : null; + } +} +``` + +For role-based access: + +```csharp +// Types/AdminQueries.cs +[QueryType] +public static partial class AdminQueries +{ + [Authorize(Roles = ["Administrator"])] + public static async Task GetAuditLogsAsync( + AuditService audits, CancellationToken ct) + => await audits.GetRecentAsync(ct); +} +``` + +For policy-based access, define policies in your service configuration: + +```csharp +// Program.cs +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("CanReadBilling", policy => + policy.RequireClaim("scope", "billing:read")); +}); +``` + +Then apply them to fields: + +```csharp +// Types/BillingNode.cs +[ObjectType] +public static partial class BillingNode +{ + [Authorize(Policy = "CanReadBilling")] + public static async Task GetBillingAsync( + [Parent] Organization org, + BillingService billing, + CancellationToken ct) + => await billing.GetForOrgAsync(org.Id, ct); +} +``` + +Use `HotChocolate.Authorization.AuthorizeAttribute`, not the Microsoft one. The Microsoft attribute does not integrate with the Hot Chocolate authorization pipeline. + +[Learn more about authorization](/docs/hotchocolate/v16/securing-your-api/authorization) + +# Rate Limiting and Depth Limits + +Cost analysis handles query complexity, but you also want to limit how many requests a client can send and how deeply nested a query can be. + +## Max Execution Depth + +Set a maximum query depth to reject pathologically nested queries before cost analysis even runs: + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .AddMaxExecutionDepthRule(15); +``` + +Choose a depth that accommodates your deepest legitimate query path. For most APIs, a depth of 10 to 20 is reasonable. + +## ASP.NET Core Rate Limiting + +Combine Hot Chocolate's query-level protections with ASP.NET Core's rate limiting middleware to limit requests per client: + +```csharp +// Program.cs +builder.Services.AddRateLimiter(options => +{ + options.AddFixedWindowLimiter("graphql", opt => + { + opt.PermitLimit = 100; + opt.Window = TimeSpan.FromMinutes(1); + }); +}); + +// ... + +app.UseRateLimiter(); + +app.UseEndpoints(endpoints => +{ + endpoints.MapGraphQL().RequireRateLimiting("graphql"); +}); +``` + +Rate limiting and cost analysis complement each other. Rate limiting caps the number of requests. Cost analysis caps the complexity of each request. Together, they bound the total work your server does for any client. + +# Disable Request Batching + +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: + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .ModifyRequestOptions(opt => opt.AllowedBatchOperations = AllowedBatchOperations.None); +``` + +# Putting It All Together + +Here is a complete `Program.cs` that combines all the configuration from this guide into one starting point: + +```csharp +// Program.cs +var builder = WebApplication.CreateBuilder(args); + +// Authentication (configure for your identity provider) +builder.Services + .AddAuthentication("Bearer") + .AddJwtBearer(); + +// Authorization policies +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("CanReadBilling", policy => + policy.RequireClaim("scope", "billing:read")); +}); + +// Rate limiting +builder.Services.AddRateLimiter(options => +{ + options.AddFixedWindowLimiter("graphql", opt => + { + opt.PermitLimit = 100; + opt.Window = TimeSpan.FromMinutes(1); + }); +}); + +// GraphQL server +builder.Services + .AddGraphQLServer() + .AddAuthorization() + .AddMaxExecutionDepthRule(15) + .ModifyPagingOptions(opt => + { + opt.MaxPageSize = 100; + opt.DefaultPageSize = 25; + opt.RequirePagingBoundaries = true; + }) + .ModifyCostOptions(options => + { + options.MaxFieldCost = 5_000; + options.MaxTypeCost = 5_000; + options.EnforceCostLimits = true; + }) + .ModifyRequestOptions(opt => + opt.AllowedBatchOperations = AllowedBatchOperations.None) + .AllowIntrospection(builder.Environment.IsDevelopment()) + .AddTypes(); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); +app.UseRateLimiter(); + +app.UseEndpoints(endpoints => +{ + endpoints.MapGraphQL().RequireRateLimiting("graphql"); +}); + +app.Run(); +``` + +Adjust the specific values (`MaxPageSize`, `MaxFieldCost`, `MaxTypeCost`, depth limit, rate limit window) to match your schema and infrastructure. Use the `GraphQL-Cost: report` header to measure real query costs and tune from there. + +# Troubleshooting + +## Legitimate client queries are rejected by cost analysis + +Send the query with the `GraphQL-Cost: report` header to inspect its field cost and type cost. Common causes: the query fans out across multiple paginated fields, or a resolver has a high default cost. Either increase `MaxFieldCost`/`MaxTypeCost` or reduce `MaxPageSize` on the specific fields that cause the fan-out. + +## Clients receive "first or last argument required" errors + +This happens when `RequirePagingBoundaries` is enabled and the client does not specify `first` or `last` on a paginated field. This is the intended behavior for public APIs. Document the requirement in your API guide and include `first` in your example queries. + +## Authorization errors for authenticated users + +Verify you are using `HotChocolate.Authorization.AuthorizeAttribute`, not `Microsoft.AspNetCore.Authorization.AuthorizeAttribute`. Also check that `AddAuthorization()` is called on both `IServiceCollection` and `IRequestExecutorBuilder`, and that `UseAuthentication()` comes before `UseAuthorization()` in the middleware pipeline. + +## Introspection works in development but not in production + +If you used `AllowIntrospection(builder.Environment.IsDevelopment())`, introspection is disabled in all non-development environments. This is typically what you want. If you need introspection in production for specific clients, use a request interceptor to allow it based on authentication. + +## Rate limiting does not seem to apply + +Ensure `app.UseRateLimiter()` is called before `app.UseEndpoints()` in the middleware pipeline, and that `RequireRateLimiting("graphql")` is chained onto `MapGraphQL()`. Also verify that the rate limiter policy name matches between `AddFixedWindowLimiter` and `RequireRateLimiting`. + +# Next Steps + +- **Cost analysis reference:** [Cost Analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis) covers all options, custom weights, filtering and sorting costs, and the tuning guide. +- **Authorization reference:** [Authorization](/docs/hotchocolate/v16/securing-your-api/authorization) covers roles, policies, global authorization, and accessing `IResolverContext` in handlers. +- **Pagination reference:** [Pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination) covers the `Connection` type, total counts, extending connection types, and pagination providers. +- **Schema documentation:** [Documentation](/docs/hotchocolate/v16/building-a-schema/documentation) covers `[GraphQLDescription]`, XML docs, and priority order. +- **Schema versioning:** [Versioning](/docs/hotchocolate/v16/building-a-schema/versioning) covers `@deprecated`, `@requiresOptIn`, and feature stability. +- **Introspection:** [Introspection](/docs/hotchocolate/v16/securing-your-api/introspection) covers disabling, allowlisting, and custom error messages. +- **Trusted documents:** If you later add first-party clients that you control, [Trusted Documents](/docs/hotchocolate/v16/performance/trusted-documents) let you bypass cost analysis for pre-approved operations. diff --git a/website/src/docs/hotchocolate/v16/guides/schema-evolution.md b/website/src/docs/hotchocolate/v16/guides/schema-evolution.md new file mode 100644 index 00000000000..6af88773bea --- /dev/null +++ b/website/src/docs/hotchocolate/v16/guides/schema-evolution.md @@ -0,0 +1,319 @@ +--- +title: "Schema Evolution" +--- + +GraphQL schemas evolve. New fields get added, old ones get retired. Unlike REST APIs with URL versioning, GraphQL schemas use additive changes and deprecation to manage that lifecycle. This page covers the tools Hot Chocolate provides for evolving your schema without breaking clients. + +# Document Your Schema + +Descriptions are the first line of defense against breaking changes. When every field has a clear description, consumers understand what they depend on and can adapt when you announce deprecations. + +Hot Chocolate generates descriptions from standard C# XML documentation comments. Enable the XML documentation file in your `.csproj`: + +```xml + + true + $(NoWarn);1591 + +``` + +The `` element suppresses compiler warnings for types without documentation comments. With this in place, XML `` tags on your types and properties become GraphQL descriptions automatically. + +```csharp +// Types/Product.cs +/// +/// A product available in the catalog. +/// +public class Product +{ + /// + /// The unique product identifier. + /// + public int Id { get; set; } + + /// + /// The display name shown to customers. + /// + public string Name { get; set; } +} +``` + +For cases where the C# summary does not work well as a GraphQL description, use the `[GraphQLDescription]` attribute. This takes precedence over XML docs. + + + + +```csharp +// Types/Product.cs +public class Product +{ + public int Id { get; set; } + + [GraphQLDescription("The display name shown to customers.")] + public string Name { get; set; } + + [GraphQLDescription("The current price in the seller's default currency.")] + public decimal Price { get; set; } +} +``` + + + + +```csharp +// Types/ProductType.cs +public class ProductType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Description("A product available in the catalog."); + + descriptor + .Field(f => f.Name) + .Description("The display name shown to customers."); + + descriptor + .Field(f => f.Price) + .Description("The current price in the seller's default currency."); + } +} +``` + + + + +Descriptions appear in introspection results and in tools like [Nitro](/products/nitro). The more descriptive your schema, the fewer support questions you receive when fields change. + +[Learn more about schema documentation](/docs/hotchocolate/v16/building-a-schema/documentation) + +# Deprecate Fields Instead of Removing Them + +When a field is no longer the recommended way to access data, deprecate it. Deprecated fields remain functional, but introspection marks them with the `@deprecated` directive so tools can warn consumers. + + + + +```csharp +// Types/ProductQueries.cs +[QueryType] +public static partial class ProductQueries +{ + [GraphQLDeprecated("Use `productById` instead.")] + public static Product? GetProduct(int id, CatalogService catalog) + => catalog.GetById(id); + + public static Product? GetProductById(int id, CatalogService catalog) + => catalog.GetById(id); +} +``` + +The .NET `[Obsolete("reason")]` attribute works the same way as `[GraphQLDeprecated("reason")]`. If your field is also obsolete from the C# perspective, `[Obsolete]` covers both. + + + + +```csharp +// Types/ProductQueriesType.cs +public class ProductQueriesType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name(OperationTypeNames.Query); + + descriptor + .Field("product") + .Deprecated("Use `productById` instead.") + .Argument("id", a => a.Type>()) + .Resolve(context => + { + // ... + }); + + descriptor + .Field("productById") + .Argument("id", a => a.Type>()) + .Resolve(context => + { + // ... + }); + } +} +``` + + + + +The resulting SDL includes the `@deprecated` directive: + +```graphql +type Query { + product(id: Int!): Product @deprecated(reason: "Use `productById` instead.") + productById(id: Int!): Product +} +``` + +You can deprecate output fields, input fields, arguments, and enum values. Keep deprecated fields for at least one release cycle so consumers have time to migrate. When you are confident no consumers depend on a deprecated field, remove it. + +> Warning: You cannot deprecate non-null arguments or input fields that have no default value. Deprecating a required field would silently break queries that depend on it. Add a default value first, then apply the deprecation. + +[Learn more about deprecation](/docs/hotchocolate/v16/building-a-schema/versioning) + +# Opt-In Features with @requiresOptIn + +While `@deprecated` marks fields that are going away, `@requiresOptIn` marks fields that are not yet stable. This is useful for rolling out experimental features where consumers should make a deliberate choice to use them. + +Fields marked with `@requiresOptIn` are hidden from introspection by default. Consumers opt in by specifying the feature name in their introspection queries. + +## Enable Opt-In Features + +Opt-in support is disabled by default. Enable it in your schema options: + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .ModifyOptions(o => o.EnableOptInFeatures = true); +``` + +## Mark Fields as Opt-In + + + + +```csharp +// Types/Product.cs +public class Product +{ + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + + [RequiresOptIn("experimentalRecommendations")] + public List? Recommendations { get; set; } +} +``` + + + + +```csharp +// Types/ProductType.cs +public class ProductType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(f => f.Recommendations) + .RequiresOptIn("experimentalRecommendations"); + } +} +``` + + + + +Consumers discover opt-in fields by passing the `includeOptIn` argument in introspection: + +```graphql +{ + __type(name: "Product") { + fields(includeOptIn: ["experimentalRecommendations"]) { + name + requiresOptIn + } + } +} +``` + +## Declare Feature Stability + +You can declare the stability level of each opt-in feature so consumers understand whether it is experimental, preview, or something else. + + + + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .ModifyOptions(o => o.EnableOptInFeatures = true) + .OptInFeatureStability("experimentalRecommendations", "experimental"); +``` + + + + +```csharp +// Program.cs +builder.Services + .AddGraphQLServer() + .ModifyOptions(o => o.EnableOptInFeatures = true) + .SetSchema(s => s + .OptInFeatureStability("experimentalRecommendations", "experimental")); +``` + + + + +Consumers query feature stability through introspection: + +```graphql +{ + __schema { + optInFeatureStability { + feature + stability + } + } +} +``` + +[Learn more about opt-in features](/docs/hotchocolate/v16/building-a-schema/versioning) + +# Additive Changes Are Safe + +Not all changes are equal. Some changes are safe for every consumer, while others can break existing queries. + +**Non-breaking changes:** + +- Adding a new field to an existing type +- Adding a new type +- Adding a new argument with a default value +- Adding a new enum value (for output enums) + +**Breaking changes:** + +- Removing a field +- Renaming a field +- Changing a field's return type +- Adding a required argument (one without a default value) +- Removing an enum value + +The general principle: if an existing, valid query could fail or return different data after the change, the change is breaking. Additive changes expand what clients can query without affecting what they already query. + +When you need to make a breaking change, follow this sequence: + +1. Add the new field or type alongside the old one. +2. Deprecate the old field with a clear reason pointing to the replacement. +3. Wait for consumers to migrate (monitor usage if possible). +4. Remove the deprecated field. + +# Troubleshooting + +## Deprecated field still returned in queries + +Deprecation does not remove a field. The field continues to work normally. Deprecation marks the field in introspection so tools can warn consumers. Remove the field from the schema when you are confident no consumers depend on it. + +## Opt-in field not visible in introspection + +Fields with `@requiresOptIn` are hidden by default. Use the `includeOptIn` argument in introspection queries to reveal them. Also verify that `EnableOptInFeatures = true` is set in your schema options. + +## Descriptions not appearing in schema + +Verify that `GenerateDocumentationFile` is set to `true` in your `.csproj`. Without this setting, the XML file is not generated and Hot Chocolate has no documentation to read. + +# Next Steps + +- **Schema documentation reference:** [Documentation](/docs/hotchocolate/v16/building-a-schema/documentation) covers `[GraphQLDescription]`, XML docs, and priority order. +- **Versioning reference:** [Versioning](/docs/hotchocolate/v16/building-a-schema/versioning) covers `@deprecated`, `@requiresOptIn`, and feature stability in full detail. +- **Building a public API:** [Public API Guide](/docs/hotchocolate/v16/guides/public-api) covers cost analysis, pagination, and authorization for APIs consumed by external developers. diff --git a/website/src/docs/hotchocolate/v16/guides/testing.md b/website/src/docs/hotchocolate/v16/guides/testing.md new file mode 100644 index 00000000000..eeedd2d0f46 --- /dev/null +++ b/website/src/docs/hotchocolate/v16/guides/testing.md @@ -0,0 +1,330 @@ +--- +title: "Testing" +--- + +Testing a GraphQL server means testing resolvers, the schema shape, and the execution pipeline. Hot Chocolate provides test infrastructure for all three. This page walks through the patterns you need to write reliable tests for a Hot Chocolate server. + +# Set Up a Test Executor + +The foundation for all integration tests is an `IRequestExecutor`. You build one from a `ServiceCollection` the same way you configure the server in `Program.cs`, but without the ASP.NET Core host. + +```csharp +// Tests/ProductTests.cs +public class ProductTests +{ + [Fact] + public async Task Get_Product_Returns_Name() + { + // arrange + var executor = await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync("{ product { name } }"); + + // assert + Assert.NotNull(result); + } +} +``` + +You can register any services your resolvers depend on before calling `AddGraphQLServer()`. This lets you inject real or mock implementations. + +```csharp +// Tests/ProductTests.cs +var executor = await new ServiceCollection() + .AddSingleton(new FakeCatalogService()) + .AddGraphQLServer() + .AddQueryType() + .BuildRequestExecutorAsync(); +``` + +# Execute Test Queries + +Use `executor.ExecuteAsync()` to run a GraphQL operation and get back an `IExecutionResult`. For type-safe access to the result, call `ExpectOperationResult()`: + +```csharp +// Tests/ProductTests.cs +[Fact] +public async Task Get_Product_Returns_Expected_Data() +{ + // arrange + var executor = await new ServiceCollection() + .AddSingleton(new FakeCatalogService()) + .AddGraphQLServer() + .AddQueryType() + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync("{ product { name price } }"); + + // assert + var operationResult = result.ExpectOperationResult(); + Assert.Null(operationResult.Errors); +} +``` + +## Pass Variables + +To pass variables, use `OperationRequestBuilder`: + +```csharp +// Tests/ProductTests.cs +[Fact] +public async Task Get_Product_By_Id() +{ + // arrange + var executor = await new ServiceCollection() + .AddSingleton(new FakeCatalogService()) + .AddGraphQLServer() + .AddQueryType() + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder.New() + .SetDocument("query($id: Int!) { productById(id: $id) { name } }") + .SetVariableValues(new Dictionary { { "id", 42 } }) + .Build()); + + // assert + var operationResult = result.ExpectOperationResult(); + Assert.Null(operationResult.Errors); +} +``` + +# 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. + +## File-Based Snapshots + +Call `MatchSnapshot()` on the result. The first run creates a snapshot file in a `__snapshots__/` directory next to your test file. Subsequent runs compare against that file. + +```csharp +// Tests/ProductTests.cs +[Fact] +public async Task Get_Product_Snapshot() +{ + // arrange + var executor = await new ServiceCollection() + .AddSingleton(new FakeCatalogService()) + .AddGraphQLServer() + .AddQueryType() + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync("{ product { name price } }"); + + // assert + result.MatchSnapshot(); +} +``` + +When the schema changes and the response shape changes with it, delete the old snapshot file and re-run the test. CookieCrumble creates a new snapshot with the updated output. + +## Inline Snapshots + +For smaller results, inline the expected output directly in your test. This keeps the expectation visible next to the assertion. + +```csharp +// Tests/ProductTests.cs +[Fact] +public async Task Get_Product_Inline() +{ + // arrange + var executor = await new ServiceCollection() + .AddSingleton(new FakeCatalogService()) + .AddGraphQLServer() + .AddQueryType() + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync("{ product { name } }"); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "product": { + "name": "Widget" + } + } + } + """); +} +``` + +# Test Resolvers in Isolation + +Integration tests run the full execution pipeline, which is thorough but slower. When you want fast feedback on resolver logic, test the method directly. + +```csharp +// Types/ProductQueries.cs +[QueryType] +public static partial class ProductQueries +{ + public static Product? GetProductById(int id, ICatalogService catalog) + => catalog.GetById(id); +} +``` + +```csharp +// Tests/ProductQueriesTests.cs +public class ProductQueriesTests +{ + [Fact] + public void GetProductById_Returns_Product_When_Found() + { + // arrange + var catalog = new FakeCatalogService(); + catalog.Add(new Product { Id = 1, Name = "Widget" }); + + // act + var result = ProductQueries.GetProductById(1, catalog); + + // assert + Assert.NotNull(result); + Assert.Equal("Widget", result.Name); + } + + [Fact] + public void GetProductById_Returns_Null_When_Not_Found() + { + // arrange + var catalog = new FakeCatalogService(); + + // act + var result = ProductQueries.GetProductById(999, catalog); + + // assert + Assert.Null(result); + } +} +``` + +This approach is useful for resolvers that contain business logic. For resolvers that are thin wrappers around a service call, integration tests through the executor provide more value. + +# Test Schema Shape + +When you want to catch unintended schema changes (renamed fields, changed nullability, missing types), snapshot the schema SDL. + +```csharp +// Tests/SchemaTests.cs +public class SchemaTests +{ + [Fact] + public async Task Schema_Snapshot() + { + // arrange + var executor = await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .BuildRequestExecutorAsync(); + + // act & assert + executor.Schema.MatchSnapshot(); + } +} +``` + +`executor.Schema.MatchSnapshot()` serializes the schema to SDL and compares it against the stored snapshot. If you add a field, rename a type, or change nullability, the snapshot test fails and shows the diff. Review the diff to confirm the change is intentional, then update the snapshot. + +You can also use `executor.Schema.ToString()` to get the SDL as a string if you need to inspect it programmatically: + +```csharp +// Tests/SchemaTests.cs +[Fact] +public async Task Schema_Contains_Product_Type() +{ + var executor = await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .BuildRequestExecutorAsync(); + + var sdl = executor.Schema.ToString(); + + Assert.Contains("type Product", sdl); +} +``` + +# Test Middleware and Error Handling + +If you register custom field middleware or error filters, test them through the execution pipeline. + +## Custom Middleware + +Register middleware in the test executor the same way you register it in `Program.cs`, then execute a query that exercises it. + +```csharp +// Tests/LoggingMiddlewareTests.cs +[Fact] +public async Task Logging_Middleware_Does_Not_Alter_Result() +{ + // arrange + var executor = await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .UseField() + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync("{ product { name } }"); + + // assert + var operationResult = result.ExpectOperationResult(); + Assert.Null(operationResult.Errors); +} +``` + +## Error Filters + +To verify that your error filter transforms errors correctly, trigger an error in a resolver and assert on the error message in the result. + +```csharp +// Tests/ErrorFilterTests.cs +[Fact] +public async Task Error_Filter_Masks_Internal_Errors() +{ + // arrange + var executor = await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .AddErrorFilter(error => + error.WithMessage("An unexpected error occurred.")) + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync("{ failingField }"); + + // assert + var operationResult = result.ExpectOperationResult(); + Assert.NotNull(operationResult.Errors); + Assert.Equal( + "An unexpected error occurred.", + operationResult.Errors[0].Message); +} +``` + +# Troubleshooting + +## Test hangs on BuildRequestExecutorAsync + +This can happen when a required service is missing from the `ServiceCollection`. Check that all services your resolvers inject are registered. The executor build step resolves all types and their dependencies at startup. + +## Snapshot mismatch after intentional change + +Delete the old snapshot file from the `__snapshots__/` directory and re-run the test. CookieCrumble creates a new snapshot with the updated output. Review the new snapshot to confirm it matches your expectation. + +## ExpectOperationResult throws an exception + +`ExpectOperationResult()` throws if the execution result is not a standard operation result (for example, if it is a streaming result from `@defer` or `@stream`). For streaming results, use the appropriate streaming result type instead. + +# Next Steps + +- **Error handling reference:** [Error Handling Guide](/docs/hotchocolate/v16/guides/error-handling) covers error types, error filters, and how to structure error responses. +- **CookieCrumble:** The snapshot testing framework lives in `src/CookieCrumble/` in the repository. Explore the source for advanced snapshot configuration. +- **Schema evolution:** [Schema Evolution Guide](/docs/hotchocolate/v16/guides/schema-evolution) covers deprecation, opt-in features, and managing schema changes over time. diff --git a/website/src/docs/hotchocolate/v16/index.md b/website/src/docs/hotchocolate/v16/index.md index 9fd817fe6d3..4bee2fad391 100644 --- a/website/src/docs/hotchocolate/v16/index.md +++ b/website/src/docs/hotchocolate/v16/index.md @@ -70,9 +70,9 @@ A public API is consumed by third-party developers or external clients. GitHub's Hot Chocolate provides **cost analysis** for this scenario. You assign weights to fields and connections, and the server rejects operations that exceed the budget before execution begins. -- [Cost analysis](/docs/hotchocolate/v16/security/cost-analysis) explains field weights, type costs, and budget configuration. -- [Authorization](/docs/hotchocolate/v16/security/authorization) limits access to types and fields based on roles or policies. -- [Controlling introspection](/docs/hotchocolate/v16/server/introspection) lets you restrict schema visibility in production. +- [Cost analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis) explains field weights, type costs, and budget configuration. +- [Authorization](/docs/hotchocolate/v16/securing-your-api/authorization) limits access to types and fields based on roles or policies. +- [Controlling introspection](/docs/hotchocolate/v16/securing-your-api/introspection) lets you restrict schema visibility in production. ## Private GraphQL @@ -80,7 +80,7 @@ A private API is consumed by your own applications. This is how Meta built and o Hot Chocolate provides **trusted documents** for this scenario. You extract all operations from your client applications during their build process, register them with the server, and the server only accepts pre-registered operations. -- [Trusted documents](/docs/hotchocolate/v16/performance/persisted-operations) covers the full workflow: extraction, registration, and enforcement. +- [Trusted documents](/docs/hotchocolate/v16/performance/trusted-documents) covers the full workflow: extraction, registration, and enforcement. - [Strawberry Shake](/docs/strawberryshake/v16) and [Relay](https://relay.dev/docs/guides/persisted-queries/) both support build-time operation extraction. These two approaches complement each other. A common setup is trusted documents for your own frontend applications and cost analysis for partner integrations. @@ -113,7 +113,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/resolvers-and-data/dataloader) for batching and caching, or [Resolvers](/docs/hotchocolate/v16/resolvers-and-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/integrations/entity-framework.md b/website/src/docs/hotchocolate/v16/integrations/entity-framework.md index 864715e1c7c..23370f5ceb7 100644 --- a/website/src/docs/hotchocolate/v16/integrations/entity-framework.md +++ b/website/src/docs/hotchocolate/v16/integrations/entity-framework.md @@ -7,14 +7,14 @@ description: Learn how to integrate Entity Framework Core with Hot Chocolate v16 # Resolver Injection of a DbContext -When using the [default scope](/docs/hotchocolate/v16/server/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-and-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). ```csharp public static async Task GetBookByIdAsync( ApplicationDbContext dbContext) => // ... ``` -When using the [default scope](/docs/hotchocolate/v16/server/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-and-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. ```csharp public static async Task AddBookAsync( @@ -22,7 +22,7 @@ public static async Task AddBookAsync( AppDbContext dbContext) => // ... ``` -See the [Dependency Injection](/docs/hotchocolate/v16/server/dependency-injection) documentation for more details. +See the [Dependency Injection](/docs/hotchocolate/v16/resolvers-and-data/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. @@ -238,6 +238,6 @@ Create the `DbContext` inside the `LoadBatchAsync` method and dispose it there. # Next Steps -- [Dependency Injection](/docs/hotchocolate/v16/server/dependency-injection) for DI scope configuration -- [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader) for batching patterns -- [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) for applying filters to EF Core queries +- [Dependency Injection](/docs/hotchocolate/v16/resolvers-and-data/dependency-injection) for DI scope configuration +- [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/dataloader) for batching patterns +- [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering) for applying filters to EF Core queries diff --git a/website/src/docs/hotchocolate/v16/integrations/marten.md b/website/src/docs/hotchocolate/v16/integrations/marten.md index 9042d6e9fa0..e0b3ba13204 100644 --- a/website/src/docs/hotchocolate/v16/integrations/marten.md +++ b/website/src/docs/hotchocolate/v16/integrations/marten.md @@ -24,7 +24,7 @@ builder.Services .AddMartenFiltering(); ``` -[Learn more about filtering](/docs/hotchocolate/v16/fetching-data/filtering). +[Learn more about filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering). # Sorting @@ -37,19 +37,19 @@ builder.Services .AddMartenSorting(); ``` -[Learn more about sorting](/docs/hotchocolate/v16/fetching-data/sorting). +[Learn more about sorting](/docs/hotchocolate/v16/resolvers-and-data/sorting). # Projections Projections work out of the box with Marten. No custom configuration is needed. -[Learn more about projections](/docs/hotchocolate/v16/fetching-data/projections). +[Learn more about projections](/docs/hotchocolate/v16/resolvers-and-data/projections). # Paging Pagination works out of the box with Marten. No custom configuration is needed. -[Learn more about pagination](/docs/hotchocolate/v16/fetching-data/pagination). +[Learn more about pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination). # Troubleshooting @@ -61,6 +61,6 @@ Confirm that you registered `AddMartenSorting()`. The default sorting convention # Next Steps -- [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) for filtering concepts -- [Sorting](/docs/hotchocolate/v16/fetching-data/sorting) for sorting concepts -- [Pagination](/docs/hotchocolate/v16/fetching-data/pagination) for pagination setup +- [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering) for filtering concepts +- [Sorting](/docs/hotchocolate/v16/resolvers-and-data/sorting) for sorting concepts +- [Pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination) for pagination setup diff --git a/website/src/docs/hotchocolate/v16/integrations/mongodb.md b/website/src/docs/hotchocolate/v16/integrations/mongodb.md index 569b27bbb88..b96298dc348 100644 --- a/website/src/docs/hotchocolate/v16/integrations/mongodb.md +++ b/website/src/docs/hotchocolate/v16/integrations/mongodb.md @@ -168,7 +168,7 @@ builder.Services .AddMongoDbPagingProviders(); ``` -[Learn more about pagination providers](/docs/hotchocolate/v16/fetching-data/pagination#providers) +[Learn more about pagination providers](/docs/hotchocolate/v16/resolvers-and-data/pagination#providers) ## Cursor Pagination @@ -229,8 +229,8 @@ Not all query patterns benefit from projections. If you encounter issues, try re # Next Steps -- [Pagination](/docs/hotchocolate/v16/fetching-data/pagination) for pagination setup -- [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) for filtering concepts +- [Pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination) for pagination setup +- [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering) for filtering concepts - [Executable](/docs/hotchocolate/v16/api-reference/executable) for the `IExecutable` abstraction diff --git a/website/src/docs/hotchocolate/v16/integrations/spatial-data.md b/website/src/docs/hotchocolate/v16/integrations/spatial-data.md index 5bbadbf558c..57ded34f218 100644 --- a/website/src/docs/hotchocolate/v16/integrations/spatial-data.md +++ b/website/src/docs/hotchocolate/v16/integrations/spatial-data.md @@ -330,8 +330,8 @@ The CRS is currently fixed. The user must know the CRS of the backend to perform # Next Steps -- [Filtering](/docs/hotchocolate/v16/fetching-data/filtering) for general filtering concepts -- [Projections](/docs/hotchocolate/v16/fetching-data/projections) for projection setup +- [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering) for general filtering concepts +- [Projections](/docs/hotchocolate/v16/resolvers-and-data/projections) for projection setup - [Entity Framework integration](/docs/hotchocolate/v16/integrations/entity-framework) for EF Core setup diff --git a/website/src/docs/hotchocolate/v16/performance/automatic-persisted-operations.md b/website/src/docs/hotchocolate/v16/performance/automatic-persisted-operations.md index f59aac3dda9..554e6618564 100644 --- a/website/src/docs/hotchocolate/v16/performance/automatic-persisted-operations.md +++ b/website/src/docs/hotchocolate/v16/performance/automatic-persisted-operations.md @@ -297,7 +297,7 @@ Check that the Redis connection string is correct and the server can connect to # Next Steps -- [Persisted Operations](/docs/hotchocolate/v16/performance/persisted-operations) for pre-registering operations ahead of deployment. +- [Persisted Operations](/docs/hotchocolate/v16/performance/trusted-documents) for pre-registering operations ahead of deployment. - [HTTP Transport](/docs/hotchocolate/v16/server/http-transport) for details on HTTP GET caching. diff --git a/website/src/docs/hotchocolate/v16/performance/index.md b/website/src/docs/hotchocolate/v16/performance/index.md index f5d651a3c0c..204ecaddb20 100644 --- a/website/src/docs/hotchocolate/v16/performance/index.md +++ b/website/src/docs/hotchocolate/v16/performance/index.md @@ -22,7 +22,7 @@ The first approach stores operation documents ahead of time (before deployment). Strawberry Shake, [Relay](https://relay.dev/docs/guides/persisted-queries/), and [Apollo](https://www.apollographql.com/docs/react/api/link/persisted-queries/) client all support this approach. -[Learn more about persisted operations](/docs/hotchocolate/v16/performance/persisted-operations) +[Learn more about persisted operations](/docs/hotchocolate/v16/performance/trusted-documents) ## Automatic Persisted Operations diff --git a/website/src/docs/hotchocolate/v16/performance/persisted-operations.md b/website/src/docs/hotchocolate/v16/performance/trusted-documents.md similarity index 100% rename from website/src/docs/hotchocolate/v16/performance/persisted-operations.md rename to website/src/docs/hotchocolate/v16/performance/trusted-documents.md diff --git a/website/src/docs/hotchocolate/v16/fetching-data/dataloader.md b/website/src/docs/hotchocolate/v16/resolvers-and-data/dataloader.md similarity index 97% rename from website/src/docs/hotchocolate/v16/fetching-data/dataloader.md rename to website/src/docs/hotchocolate/v16/resolvers-and-data/dataloader.md index decc4544c15..f3b3295170c 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/dataloader.md +++ b/website/src/docs/hotchocolate/v16/resolvers-and-data/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), 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/resolvers-and-data/resolvers) first. # The N+1 Problem @@ -381,7 +381,7 @@ If your `DbContext` or other scoped service is disposed before the DataLoader ba # Next Steps -- **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). +- **Need to understand resolver basics?** See [Resolvers](/docs/hotchocolate/v16/resolvers-and-data/resolvers). +- **Need pagination?** See [Pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination) for cursor-based connections. +- **Need to filter or sort data?** See [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering) and [Sorting](/docs/hotchocolate/v16/resolvers-and-data/sorting). - **Using Entity Framework?** See [Entity Framework](/docs/hotchocolate/v16/integrations/entity-framework) for integration patterns with DataLoaders. diff --git a/website/src/docs/hotchocolate/v16/server/dependency-injection.md b/website/src/docs/hotchocolate/v16/resolvers-and-data/dependency-injection.md similarity index 100% rename from website/src/docs/hotchocolate/v16/server/dependency-injection.md rename to website/src/docs/hotchocolate/v16/resolvers-and-data/dependency-injection.md diff --git a/website/src/docs/hotchocolate/v16/fetching-data/fetching-from-databases.md b/website/src/docs/hotchocolate/v16/resolvers-and-data/fetching-from-databases.md similarity index 97% rename from website/src/docs/hotchocolate/v16/fetching-data/fetching-from-databases.md rename to website/src/docs/hotchocolate/v16/resolvers-and-data/fetching-from-databases.md index 27798f332c7..ad4344aac62 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/fetching-from-databases.md +++ b/website/src/docs/hotchocolate/v16/resolvers-and-data/fetching-from-databases.md @@ -125,7 +125,7 @@ EF Core tracks entities in the change tracker. If you modify an entity in one re # 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 batch database calls?** See [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/dataloader). +- **Need to optimize SQL queries?** See [Projections](/docs/hotchocolate/v16/resolvers-and-data/projections). - **Need to integrate with MongoDB?** See [MongoDB Integration](/docs/hotchocolate/v16/integrations/mongodb). - **Need to integrate with EF Core?** See [Entity Framework Integration](/docs/hotchocolate/v16/integrations/entity-framework). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/fetching-from-rest.md b/website/src/docs/hotchocolate/v16/resolvers-and-data/fetching-from-rest.md similarity index 93% rename from website/src/docs/hotchocolate/v16/fetching-data/fetching-from-rest.md rename to website/src/docs/hotchocolate/v16/resolvers-and-data/fetching-from-rest.md index 4d41370501d..ebec174be58 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/fetching-from-rest.md +++ b/website/src/docs/hotchocolate/v16/resolvers-and-data/fetching-from-rest.md @@ -130,7 +130,7 @@ You can now open Nitro on your GraphQL server at `/graphql` and query your REST # 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. +When multiple GraphQL fields resolve data from the same REST endpoint, use a [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/dataloader) to batch and deduplicate calls. This prevents sending redundant HTTP requests for the same resource. ```csharp // DataLoaders/TodoByIdDataLoader.cs @@ -173,6 +173,6 @@ If you see one HTTP request per item in a list, add a DataLoader to batch the ca # 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). +- **Need to batch REST calls?** See [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/dataloader). +- **Need to fetch from a database instead?** See [Fetching from Databases](/docs/hotchocolate/v16/resolvers-and-data/fetching-from-databases). +- **Need to understand resolvers?** See [Resolvers](/docs/hotchocolate/v16/resolvers-and-data/resolvers). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/filtering.md b/website/src/docs/hotchocolate/v16/resolvers-and-data/filtering.md similarity index 97% rename from website/src/docs/hotchocolate/v16/fetching-data/filtering.md rename to website/src/docs/hotchocolate/v16/resolvers-and-data/filtering.md index bfe90d4d156..03f8c2854fe 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/filtering.md +++ b/website/src/docs/hotchocolate/v16/resolvers-and-data/filtering.md @@ -326,7 +326,7 @@ Ensure your resolver returns `IQueryable`, not `IEnumerable`. When you ret # Next Steps -- **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 optimize database queries?** See [Projections](/docs/hotchocolate/v16/fetching-data/projections). -- **Need to protect against expensive filter queries?** See [Cost Analysis](/docs/hotchocolate/v16/security/cost-analysis). +- **Need to sort results?** See [Sorting](/docs/hotchocolate/v16/resolvers-and-data/sorting). +- **Need to page through results?** See [Pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination). +- **Need to optimize database queries?** See [Projections](/docs/hotchocolate/v16/resolvers-and-data/projections). +- **Need to protect against expensive filter queries?** See [Cost Analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/index.md b/website/src/docs/hotchocolate/v16/resolvers-and-data/index.md similarity index 79% rename from website/src/docs/hotchocolate/v16/fetching-data/index.md rename to website/src/docs/hotchocolate/v16/resolvers-and-data/index.md index 2a77cde4a01..45273134922 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/index.md +++ b/website/src/docs/hotchocolate/v16/resolvers-and-data/index.md @@ -39,44 +39,44 @@ 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 v16, 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-and-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) +[Learn more about DataLoaders](/docs/hotchocolate/v16/resolvers-and-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) +[Learn more about pagination](/docs/hotchocolate/v16/resolvers-and-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) +[Learn more about filtering](/docs/hotchocolate/v16/resolvers-and-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) +[Learn more about sorting](/docs/hotchocolate/v16/resolvers-and-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) +[Learn more about projections](/docs/hotchocolate/v16/resolvers-and-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) +- [Fetching from databases](/docs/hotchocolate/v16/resolvers-and-data/fetching-from-databases) +- [Fetching from REST APIs](/docs/hotchocolate/v16/resolvers-and-data/fetching-from-rest) # Troubleshooting @@ -86,11 +86,11 @@ Verify that your resolver returns the correct type. If it returns `Task`, ens ## N+1 query problem -If you see one database query per item in a list, you are likely missing a DataLoader. Use a [DataLoader](/docs/hotchocolate/v16/fetching-data/dataloader) to batch and deduplicate requests. +If you see one database query per item in a list, you are likely missing a DataLoader. Use a [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/dataloader) to batch and deduplicate requests. # 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). +- **New to resolvers?** Start with [Resolvers](/docs/hotchocolate/v16/resolvers-and-data/resolvers). +- **Need to batch data access?** See [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/dataloader). +- **Need to page through lists?** See [Pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination). +- **Need to filter or sort?** See [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering) and [Sorting](/docs/hotchocolate/v16/resolvers-and-data/sorting). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/pagination.md b/website/src/docs/hotchocolate/v16/resolvers-and-data/pagination.md similarity index 95% rename from website/src/docs/hotchocolate/v16/fetching-data/pagination.md rename to website/src/docs/hotchocolate/v16/resolvers-and-data/pagination.md index 8c77a4d4977..a6bed41201b 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/pagination.md +++ b/website/src/docs/hotchocolate/v16/resolvers-and-data/pagination.md @@ -227,7 +227,7 @@ builder.Services # MaxPageSize and Cost Analysis -The `MaxPageSize` setting works together with [cost analysis](/docs/hotchocolate/v16/security/cost-analysis) to protect your API. Cost analysis uses the `MaxPageSize` as the assumed list size when calculating the cost of a paginated field. If you increase `MaxPageSize`, the cost of queries against that field increases proportionally. +The `MaxPageSize` setting works together with [cost analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis) to protect your API. Cost analysis uses the `MaxPageSize` as the assumed list size when calculating the cost of a paginated field. If you increase `MaxPageSize`, the cost of queries against that field increases proportionally. For public APIs, keep `MaxPageSize` conservative and use `RequirePagingBoundaries = true` to force clients to declare how many items they want. @@ -323,7 +323,7 @@ public class UsersEdgeExtension } ``` -> 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. +> If you use [projections](/docs/hotchocolate/v16/resolvers-and-data/projections), some properties on your model may not be populated depending on what the client requested. # Nullable Cursor Keys @@ -395,7 +395,7 @@ You have a nullable field as a cursor key and the paging handler cannot detect y # Next Steps -- **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 optimize database queries?** See [Projections](/docs/hotchocolate/v16/fetching-data/projections). -- **Need to protect against expensive queries?** See [Cost Analysis](/docs/hotchocolate/v16/security/cost-analysis). +- **Need to filter results?** See [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering). +- **Need to sort results?** See [Sorting](/docs/hotchocolate/v16/resolvers-and-data/sorting). +- **Need to optimize database queries?** See [Projections](/docs/hotchocolate/v16/resolvers-and-data/projections). +- **Need to protect against expensive queries?** See [Cost Analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/projections.md b/website/src/docs/hotchocolate/v16/resolvers-and-data/projections.md similarity index 98% rename from website/src/docs/hotchocolate/v16/fetching-data/projections.md rename to website/src/docs/hotchocolate/v16/resolvers-and-data/projections.md index 57fa5db5a72..08991fb910d 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/projections.md +++ b/website/src/docs/hotchocolate/v16/resolvers-and-data/projections.md @@ -291,7 +291,7 @@ Ensure the resolver returns `IQueryable`, not a materialized collection. If y # Next Steps -- **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 filter results?** See [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering). +- **Need to sort results?** See [Sorting](/docs/hotchocolate/v16/resolvers-and-data/sorting). +- **Need to page through results?** See [Pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination). - **Need to integrate with Entity Framework?** See [Entity Framework Integration](/docs/hotchocolate/v16/integrations/entity-framework). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/resolvers.md b/website/src/docs/hotchocolate/v16/resolvers-and-data/resolvers.md similarity index 97% rename from website/src/docs/hotchocolate/v16/fetching-data/resolvers.md rename to website/src/docs/hotchocolate/v16/resolvers-and-data/resolvers.md index f2ba6dce57c..c37ff8dcdc9 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/resolvers.md +++ b/website/src/docs/hotchocolate/v16/resolvers-and-data/resolvers.md @@ -229,7 +229,7 @@ builder.Services 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/server/dependency-injection) +[Learn more about dependency injection](/docs/hotchocolate/v16/resolvers-and-data/dependency-injection) ## Accessing the HttpContext @@ -349,7 +349,7 @@ When using `[Parent]`, verify that the parent resolver actually returns a non-nu # 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 batch data access?** See [DataLoader](/docs/hotchocolate/v16/resolvers-and-data/dataloader). +- **Need to page through results?** See [Pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination). +- **Need to filter or sort?** See [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering) and [Sorting](/docs/hotchocolate/v16/resolvers-and-data/sorting). - **Need to understand type extensions?** See [Extending Types](/docs/hotchocolate/v16/building-a-schema/extending-types). diff --git a/website/src/docs/hotchocolate/v16/fetching-data/sorting.md b/website/src/docs/hotchocolate/v16/resolvers-and-data/sorting.md similarity index 97% rename from website/src/docs/hotchocolate/v16/fetching-data/sorting.md rename to website/src/docs/hotchocolate/v16/resolvers-and-data/sorting.md index 88a7ffadbac..7fa31a7d661 100644 --- a/website/src/docs/hotchocolate/v16/fetching-data/sorting.md +++ b/website/src/docs/hotchocolate/v16/resolvers-and-data/sorting.md @@ -279,7 +279,7 @@ Set `NullOrdering` in `PagingOptions` to match your database's native behavior. # Next Steps -- **Need to filter results?** See [Filtering](/docs/hotchocolate/v16/fetching-data/filtering). -- **Need to page through results?** See [Pagination](/docs/hotchocolate/v16/fetching-data/pagination). -- **Need to optimize database queries?** See [Projections](/docs/hotchocolate/v16/fetching-data/projections). -- **Need to protect against expensive queries?** See [Cost Analysis](/docs/hotchocolate/v16/security/cost-analysis). +- **Need to filter results?** See [Filtering](/docs/hotchocolate/v16/resolvers-and-data/filtering). +- **Need to page through results?** See [Pagination](/docs/hotchocolate/v16/resolvers-and-data/pagination). +- **Need to optimize database queries?** See [Projections](/docs/hotchocolate/v16/resolvers-and-data/projections). +- **Need to protect against expensive queries?** See [Cost Analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis). diff --git a/website/src/docs/hotchocolate/v16/security/authentication.md b/website/src/docs/hotchocolate/v16/securing-your-api/authentication.md similarity index 98% rename from website/src/docs/hotchocolate/v16/security/authentication.md rename to website/src/docs/hotchocolate/v16/securing-your-api/authentication.md index 2ec930a6d0a..e9438a0214c 100644 --- a/website/src/docs/hotchocolate/v16/security/authentication.md +++ b/website/src/docs/hotchocolate/v16/securing-your-api/authentication.md @@ -68,7 +68,7 @@ builder.Services .AddQueryType(); ``` -Calling `AddAuthorization()` on the `IRequestExecutorBuilder` registers the `@authorize` directive and makes the authenticated user's identity available to resolvers. It does not lock out unauthenticated users. To restrict access, use [authorization](/docs/hotchocolate/v16/security/authorization). +Calling `AddAuthorization()` on the `IRequestExecutorBuilder` registers the `@authorize` directive and makes the authenticated user's identity available to resolvers. It does not lock out unauthenticated users. To restrict access, use [authorization](/docs/hotchocolate/v16/securing-your-api/authorization). # Accessing the ClaimsPrincipal @@ -187,6 +187,6 @@ Verify that the identity provider includes the expected claims in the token. Som # Next Steps -- **Need to restrict access to fields?** See [Authorization](/docs/hotchocolate/v16/security/authorization). +- **Need to restrict access to fields?** See [Authorization](/docs/hotchocolate/v16/securing-your-api/authorization). - **Need an overview of security options?** See [Security Overview](/docs/hotchocolate/v16/security). - **Need to customize request handling?** See [Interceptors](/docs/hotchocolate/v16/server/interceptors). diff --git a/website/src/docs/hotchocolate/v16/security/authorization.md b/website/src/docs/hotchocolate/v16/securing-your-api/authorization.md similarity index 98% rename from website/src/docs/hotchocolate/v16/security/authorization.md rename to website/src/docs/hotchocolate/v16/securing-your-api/authorization.md index ece064834fb..5344588f3c8 100644 --- a/website/src/docs/hotchocolate/v16/security/authorization.md +++ b/website/src/docs/hotchocolate/v16/securing-your-api/authorization.md @@ -6,7 +6,7 @@ Authorization controls what an authenticated user can access. Hot Chocolate prov Authentication is a prerequisite. You must first validate a user's identity before evaluating their permissions. -[Learn how to set up authentication](/docs/hotchocolate/v16/security/authentication) +[Learn how to set up authentication](/docs/hotchocolate/v16/securing-your-api/authentication) # Setup @@ -354,6 +354,6 @@ When `RequireAuthorization()` is applied to `MapGraphQL()`, it blocks all sub-mi # Next Steps -- **Need to set up authentication first?** See [Authentication](/docs/hotchocolate/v16/security/authentication). -- **Need to protect against expensive queries?** See [Cost Analysis](/docs/hotchocolate/v16/security/cost-analysis). +- **Need to set up authentication first?** See [Authentication](/docs/hotchocolate/v16/securing-your-api/authentication). +- **Need to protect against expensive queries?** See [Cost Analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis). - **Need an overview of security options?** See [Security Overview](/docs/hotchocolate/v16/security). diff --git a/website/src/docs/hotchocolate/v16/security/cost-analysis.md b/website/src/docs/hotchocolate/v16/securing-your-api/cost-analysis.md similarity index 98% rename from website/src/docs/hotchocolate/v16/security/cost-analysis.md rename to website/src/docs/hotchocolate/v16/securing-your-api/cost-analysis.md index 1057b901e41..7db84b2ea1d 100644 --- a/website/src/docs/hotchocolate/v16/security/cost-analysis.md +++ b/website/src/docs/hotchocolate/v16/securing-your-api/cost-analysis.md @@ -414,7 +414,7 @@ When a filter argument is provided as a variable (rather than inline), the analy # Next Steps -- **Need to restrict access to fields?** See [Authorization](/docs/hotchocolate/v16/security/authorization). -- **Building a private API?** See [Trusted Documents](/docs/hotchocolate/v16/performance/persisted-operations). -- **Need to limit query depth?** See [Query Depth](/docs/hotchocolate/v16/security/query-depth). +- **Need to restrict access to fields?** See [Authorization](/docs/hotchocolate/v16/securing-your-api/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/securing-your-api/query-depth). - **Need an overview of security options?** See [Security Overview](/docs/hotchocolate/v16/security). diff --git a/website/src/docs/hotchocolate/v16/security/index.md b/website/src/docs/hotchocolate/v16/securing-your-api/index.md similarity index 87% rename from website/src/docs/hotchocolate/v16/security/index.md rename to website/src/docs/hotchocolate/v16/securing-your-api/index.md index c80cd7ff91a..c518ff36dde 100644 --- a/website/src/docs/hotchocolate/v16/security/index.md +++ b/website/src/docs/hotchocolate/v16/securing-your-api/index.md @@ -18,7 +18,7 @@ Combine cost analysis with: - **Execution depth limits** to prevent deeply nested queries. - **Execution timeouts** to abort long-running queries. -[Learn more about cost analysis](/docs/hotchocolate/v16/security/cost-analysis) +[Learn more about cost analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis) # Private APIs: Trusted Documents @@ -26,7 +26,7 @@ Private APIs serve known clients that you control, such as your own web or mobil With trusted documents, you extract all GraphQL operations from your client at build time and register them with the server. At runtime, the server only accepts operations it recognizes. This eliminates the risk of arbitrary queries entirely. -[Learn more about trusted documents](/docs/hotchocolate/v16/performance/persisted-operations) +[Learn more about trusted documents](/docs/hotchocolate/v16/performance/trusted-documents) # Defense in Depth @@ -36,13 +36,13 @@ Regardless of whether your API is public or private, apply these additional prot Authentication determines who is making a request. Hot Chocolate integrates with the ASP.NET Core authentication system, supporting JWT, cookies, and other authentication schemes. -[Learn more about authentication](/docs/hotchocolate/v16/security/authentication) +[Learn more about authentication](/docs/hotchocolate/v16/securing-your-api/authentication) ## Authorization Authorization controls what an authenticated user can access. Hot Chocolate provides the `@authorize` directive for field-level and type-level access control, integrating with ASP.NET Core roles and policies. -[Learn more about authorization](/docs/hotchocolate/v16/security/authorization) +[Learn more about authorization](/docs/hotchocolate/v16/securing-your-api/authorization) ## Execution Depth @@ -97,7 +97,7 @@ builder.Services Introspection powers developer tools but can also reveal your schema to attackers. You can restrict or disable introspection in production. -[Learn more about introspection](/docs/hotchocolate/v16/server/introspection#disabling-introspection) +[Learn more about introspection](/docs/hotchocolate/v16/securing-your-api/introspection#disabling-introspection) ## FIPS Compliance @@ -110,7 +110,7 @@ builder.Services .AddSha256DocumentHashProvider(); ``` -[Learn more about hashing providers](/docs/hotchocolate/v16/performance/persisted-operations#hashing-algorithms) +[Learn more about hashing providers](/docs/hotchocolate/v16/performance/trusted-documents#hashing-algorithms) # Troubleshooting @@ -118,7 +118,7 @@ builder.Services Review your cost analysis configuration. The default limits (`MaxFieldCost = 1000`, `MaxTypeCost = 1000`) may be too low for your schema. Use the `GraphQL-Cost: report` header to inspect the cost of your queries before adjusting limits. -[Learn how to tune cost analysis](/docs/hotchocolate/v16/security/cost-analysis) +[Learn how to tune cost analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis) ## Authenticated users receive "not authorized" errors @@ -137,7 +137,7 @@ if (app.Environment.IsDevelopment()) # Next Steps -- **Building a public API?** Start with [Cost Analysis](/docs/hotchocolate/v16/security/cost-analysis). -- **Building a private API?** Start with [Trusted Documents](/docs/hotchocolate/v16/performance/persisted-operations). -- **Need authentication?** See [Authentication](/docs/hotchocolate/v16/security/authentication). -- **Need authorization?** See [Authorization](/docs/hotchocolate/v16/security/authorization). +- **Building a public API?** Start with [Cost Analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis). +- **Building a private API?** Start with [Trusted Documents](/docs/hotchocolate/v16/performance/trusted-documents). +- **Need authentication?** See [Authentication](/docs/hotchocolate/v16/securing-your-api/authentication). +- **Need authorization?** See [Authorization](/docs/hotchocolate/v16/securing-your-api/authorization). diff --git a/website/src/docs/hotchocolate/v16/server/introspection.md b/website/src/docs/hotchocolate/v16/securing-your-api/introspection.md similarity index 100% rename from website/src/docs/hotchocolate/v16/server/introspection.md rename to website/src/docs/hotchocolate/v16/securing-your-api/introspection.md diff --git a/website/src/docs/hotchocolate/v16/security/query-depth.md b/website/src/docs/hotchocolate/v16/security/query-depth.md deleted file mode 100644 index cd9752748ca..00000000000 --- a/website/src/docs/hotchocolate/v16/security/query-depth.md +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: "Query depth" ---- diff --git a/website/src/docs/hotchocolate/v16/server/batching.md b/website/src/docs/hotchocolate/v16/server/batching.md index 8ffea4f9dfb..8d18e01908f 100644 --- a/website/src/docs/hotchocolate/v16/server/batching.md +++ b/website/src/docs/hotchocolate/v16/server/batching.md @@ -180,7 +180,7 @@ This is expected behavior. Batch results stream back as they complete. Use the ` # Next Steps - [HTTP Transport](/docs/hotchocolate/v16/server/http-transport) for details on streaming transports and incremental delivery. -- [Persisted Operations](/docs/hotchocolate/v16/performance/persisted-operations) for reducing request payload size. +- [Persisted Operations](/docs/hotchocolate/v16/performance/trusted-documents) for reducing request payload size. - [Migrate from v15 to v16](/docs/hotchocolate/v16/migrating/migrate-from-15-to-16#batching-is-now-disabled-by-default) for the batching migration details. diff --git a/website/src/docs/hotchocolate/v16/server/endpoints.md b/website/src/docs/hotchocolate/v16/server/endpoints.md index 3aafd9e2ae2..bcd5f5939af 100644 --- a/website/src/docs/hotchocolate/v16/server/endpoints.md +++ b/website/src/docs/hotchocolate/v16/server/endpoints.md @@ -54,6 +54,7 @@ The following middleware are available: - [MapGraphQLHttp](#mapgraphqlhttp) - [MapGraphQLWebsocket](#mapgraphqlwebsocket) - [MapGraphQLSchema](#mapgraphqlschema) +- [MapGraphQLPersistedOperations](#mapgraphqlpersistedoperations) ## GraphQLServerOptions @@ -286,6 +287,173 @@ app.UseEndpoints(endpoints => With the above configuration, you can download your `schema.graphql` file from the `/graphql/schema` endpoint. +# MapGraphQLPersistedOperations + +Call `MapGraphQLPersistedOperations()` on the `IEndpointRouteBuilder` to expose persisted operations via REST-like URLs. This enables clients to execute pre-registered GraphQL operations using a simple URL pattern instead of sending a full GraphQL request body. + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddGraphQLServer() + .AddQueryType() + .AddPersistedOperations(); // Register a persisted operation storage provider + +var app = builder.Build(); + +app.MapGraphQL(); +app.MapGraphQLPersistedOperations(); + +app.Run(); +``` + +The default path is `/graphql/persisted`. The endpoint supports two URL patterns: + +| Pattern | Example | Description | +| -------------------------------- | ----------------------------------- | -------------------------------------------------------------- | +| `/{operationId}` | `/graphql/persisted/abc123` | Execute a persisted operation by its ID | +| `/{operationId}/{operationName}` | `/graphql/persisted/abc123/GetUser` | Execute a specific named operation within a persisted document | + +Both GET and POST requests are supported. With POST requests, you can pass variables and extensions in the request body. + +## Custom Path + +You can customize the path: + +```csharp +app.MapGraphQLPersistedOperations("/api/operations"); +``` + +## Requiring an Operation Name + +If you want to enforce that clients always specify an operation name in the URL, set `requireOperationName` to `true`: + +```csharp +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/securing-your-api/trusted-documents). + +# AddGraphQLServer Parameters + +The `AddGraphQLServer()` method on `IServiceCollection` accepts parameters that control request parsing and default security behavior. + +```csharp +builder.Services.AddGraphQLServer( + maxAllowedRequestSize: 20 * 1000 * 1024, // ~20 MB (default) + disableDefaultSecurity: false); // default +``` + +## maxAllowedRequestSize + +Controls the maximum allowed size (in bytes) of an incoming GraphQL request body. The default is `20 * 1000 * 1024` (approximately 20 MB). If a request exceeds this limit, it is rejected before parsing. + +Reduce this value if you expect only small queries and want to protect against excessively large payloads: + +```csharp +builder.Services.AddGraphQLServer( + maxAllowedRequestSize: 1 * 1000 * 1024); // ~1 MB +``` + +## disableDefaultSecurity + +When `false` (the default), `AddGraphQLServer()` automatically enables these security features: + +- **Cost analysis**: Protects against expensive queries by analyzing the computational cost of each operation. +- **Introspection disabled in production**: Introspection is automatically turned off when `IHostEnvironment.IsDevelopment()` returns `false`. +- **MaxAllowedFieldCycleDepthRule**: Prevents deeply cyclic field selections in production. + +If you need full control over which security features are enabled, set `disableDefaultSecurity` to `true` and configure each feature individually: + +```csharp +builder.Services + .AddGraphQLServer(disableDefaultSecurity: true) + .AddCostAnalyzer(); // Opt in to specific features manually +``` + +> Warning: Disabling default security removes important protections. Only do this if you are configuring equivalent protections manually. + +# GraphQLServerOptions Reference + +The full set of properties available on `GraphQLServerOptions` is listed below. You can set these via `ModifyServerOptions` (schema-level) or `WithOptions` (per-endpoint). + +| Property | Type | Default | Description | +| ----------------------------------------- | ---------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `EnableGetRequests` | `bool` | `true` | Controls whether HTTP GET requests are accepted. | +| `AllowedGetOperations` | `AllowedGetOperations` | `Query` | Which operation types are allowed via HTTP GET. Values: `None`, `Query`, `Mutation`, `Subscription`, `QueryAndMutation`, `All`. | +| `EnableMultipartRequests` | `bool` | `true` | Controls whether multipart form requests (file uploads) are accepted. | +| `EnableSchemaRequests` | `bool` | `true` | Controls whether the schema SDL can be downloaded via `?sdl`. | +| `EnableSchemaFileSupport` | `bool` | `true` | Controls whether the schema SDL is served as a downloadable file. | +| `EnforceGetRequestsPreflightHeader` | `bool` | `false` | When `true`, GET requests must include a CSRF preflight header. | +| `EnforceMultipartRequestsPreflightHeader` | `bool` | `true` | When `true`, multipart requests must include a CSRF preflight header. | +| `Batching` | `AllowedBatching` | `None` | Which batching modes are allowed. | +| `MaxBatchSize` | `int` | `1024` | Maximum number of operations in a single batch. `0` means unlimited. | +| `Sockets` | `GraphQLSocketOptions` | _(see below)_ | WebSocket-specific options. | +| `Tool` | `NitroAppOptions` | _(see below)_ | Nitro IDE options. | + +The `Sockets` property contains a `GraphQLSocketOptions` object with these properties: + +| Property | Type | Default | Description | +| --------------------------------- | ----------- | -------------------------- | ------------------------------------------------------------------------ | +| `ConnectionInitializationTimeout` | `TimeSpan` | `TimeSpan.FromSeconds(10)` | Time the client has to send `connection_init` after opening a WebSocket. | +| `KeepAliveInterval` | `TimeSpan?` | `TimeSpan.FromSeconds(5)` | Interval for server keep-alive pings. `null` disables keep-alive. | + +# Per-Endpoint Configuration with WithOptions + +Hot Chocolate uses a delegate-based `WithOptions` pattern to configure options per-endpoint. The delegate receives the options object, and you modify it in place. These overrides are applied on top of the schema-level defaults set via `ModifyServerOptions`. + +## MapGraphQL + +```csharp +app.MapGraphQL().WithOptions(o => +{ + o.EnableGetRequests = false; + o.AllowedGetOperations = AllowedGetOperations.Query; + o.Tool.Enable = false; +}); +``` + +## MapGraphQLHttp + +```csharp +app.MapGraphQLHttp("/graphql/http").WithOptions(o => +{ + o.EnableMultipartRequests = false; + o.EnforceGetRequestsPreflightHeader = true; +}); +``` + +## MapGraphQLWebSocket + +The WebSocket endpoint accepts a delegate over `GraphQLSocketOptions` directly: + +```csharp +app.MapGraphQLWebSocket("/graphql/ws").WithOptions(o => +{ + o.ConnectionInitializationTimeout = TimeSpan.FromSeconds(30); + o.KeepAliveInterval = TimeSpan.FromSeconds(12); +}); +``` + +## Schema-Level Defaults + +To set defaults that apply to all endpoints, use `ModifyServerOptions` on the request executor builder: + +```csharp +builder.Services + .AddGraphQLServer() + .ModifyServerOptions(o => + { + o.EnableGetRequests = false; + o.Sockets.KeepAliveInterval = TimeSpan.FromSeconds(15); + o.Tool.Enable = false; + }); +``` + +Per-endpoint `WithOptions` overrides take precedence over schema-level defaults. + # Troubleshooting ## Nitro not loading in the browser @@ -296,8 +464,21 @@ Check that the `Tool.Enable` setting is not set to `false`. In production enviro Ensure that the ASP.NET Core WebSocket middleware is registered before calling `MapGraphQL()`. Add `app.UseWebSockets()` to your middleware pipeline. +## Persisted operation returns 400 "Missing operationId" + +Verify that the URL matches the expected pattern. The operation ID must be the first path segment after the persisted operations base path (for example, `/graphql/persisted/abc123`). If `requireOperationName` is `true`, you must also provide the operation name as the second path segment. + +## Large requests rejected before execution + +If large queries or mutations are rejected before reaching the execution engine, the `maxAllowedRequestSize` parameter on `AddGraphQLServer()` is likely too small. Increase it to accommodate your expected payload sizes. + +## Introspection disabled unexpectedly in production + +When `disableDefaultSecurity` is `false` (the default), introspection is automatically turned off in non-development environments. If you need introspection available in production (for example, behind authentication), either set `disableDefaultSecurity: true` and configure security features manually, or explicitly re-enable introspection after `AddGraphQLServer()`. + # Next Steps -- [HTTP Transport](/docs/hotchocolate/v16/server/http-transport) for details on request and response formatting. +- [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. -- [Introspection](/docs/hotchocolate/v16/server/introspection) for controlling schema visibility. +- [Trusted Documents](/docs/hotchocolate/v16/securing-your-api/trusted-documents) for the full persisted operations workflow. +- [Cost Analysis](/docs/hotchocolate/v16/securing-your-api/cost-analysis) for understanding the default security cost analyzer. diff --git a/website/src/docs/hotchocolate/v16/server/global-state.md b/website/src/docs/hotchocolate/v16/server/global-state.md index a997459da8f..358b17ddc39 100644 --- a/website/src/docs/hotchocolate/v16/server/global-state.md +++ b/website/src/docs/hotchocolate/v16/server/global-state.md @@ -136,4 +136,4 @@ The `[GlobalState]` attribute throws an exception when the key does not exist or # Next Steps - [Interceptors](/docs/hotchocolate/v16/server/interceptors) for initializing state before request execution. -- [Dependency Injection](/docs/hotchocolate/v16/server/dependency-injection) for injecting services into resolvers. +- [Dependency Injection](/docs/hotchocolate/v16/resolvers-and-data/dependency-injection) for injecting services into resolvers. diff --git a/website/src/docs/hotchocolate/v16/server/http-transport.md b/website/src/docs/hotchocolate/v16/server/http-transport.md index 704ed7662fc..b11bc5fea08 100644 --- a/website/src/docs/hotchocolate/v16/server/http-transport.md +++ b/website/src/docs/hotchocolate/v16/server/http-transport.md @@ -4,6 +4,21 @@ title: HTTP Transport Hot Chocolate implements the latest version of the [GraphQL over HTTP specification](https://github.com/graphql/graphql-over-http/blob/a1e6d8ca248c9a19eb59a2eedd988c204909ee3f/spec/GraphQLOverHTTP.md). +# Response Formats and Content Negotiation + +Hot Chocolate uses the HTTP `Accept` header to determine how to format the response. Four response formats are available: + +| Accept header | Format | Use case | +| ----------------------------------- | ------------------ | --------------------------------------------------- | +| `application/graphql-response+json` | Single JSON result | Standard queries and mutations (default) | +| `multipart/mixed` | Multipart | Incremental delivery (`@defer`/`@stream`), batching | +| `text/event-stream` | Server-Sent Events | Subscriptions, streaming, incremental delivery | +| `application/jsonl` | JSON Lines | Streaming, batch responses | + +When a client sends no `Accept` header or sends `*/*`, the server responds with `application/graphql-response+json` for single results. For streaming operations, the server defaults to `multipart/mixed` unless the client explicitly requests a different format. + +When the client sends `Accept: application/json`, it opts out of the GraphQL over HTTP specification and receives legacy-style responses with a `200` status code for all requests. + # Types of Requests GraphQL requests over HTTP can be performed via either the POST or GET HTTP verb. @@ -324,6 +339,152 @@ builder.Services.AddHttpResponseFormatter(new HttpResponseFormatterOptions { An `Accept` header with the value `application/json` opts you out of the [GraphQL over HTTP](https://github.com/graphql/graphql-over-http/blob/a1e6d8ca248c9a19eb59a2eedd988c204909ee3f/spec/GraphQLOverHTTP.md) specification. The response `Content-Type` becomes `application/json` and a status code of 200 is returned for every request, even if it had validation errors or a valid response could not be produced. +# WebSocket Transport + +Hot Chocolate supports GraphQL over WebSocket for real-time communication, including subscriptions. WebSocket connections stay open, allowing the server to push results to the client as they become available. + +## Supported Sub-Protocols + +Hot Chocolate supports two WebSocket sub-protocols: + +| Sub-protocol | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `graphql-transport-ws` | The modern protocol defined by the [graphql-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) library. This is the recommended protocol for new projects. | +| `graphql-ws` | The legacy protocol defined by Apollo's [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md). Use this for backward compatibility with older clients. | + +The client selects its preferred sub-protocol via the standard WebSocket `Sec-WebSocket-Protocol` header during the handshake. Hot Chocolate negotiates and accepts whichever protocol the client requests. + +## Enabling WebSocket Support + +You must register the ASP.NET Core WebSocket middleware before calling `MapGraphQL()`. Without this, WebSocket upgrade requests are not handled. + +```csharp +// Program.cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddGraphQLServer() + .AddQueryType() + .AddSubscriptionType(); + +var app = builder.Build(); + +app.UseWebSockets(); // Required before MapGraphQL() +app.MapGraphQL(); + +app.Run(); +``` + +## WebSocket Options + +The `GraphQLSocketOptions` class controls WebSocket behavior: + +| Property | Type | Default | Description | +| --------------------------------- | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `ConnectionInitializationTimeout` | `TimeSpan` | `TimeSpan.FromSeconds(10)` | The time a client has to send a `connection_init` message after opening the WebSocket. If the client does not initialize within this window, the server closes the connection. | +| `KeepAliveInterval` | `TimeSpan?` | `TimeSpan.FromSeconds(5)` | The interval at which the server sends keep-alive pings to prevent idle connections from being dropped. Set to `null` to disable keep-alive. | + +Configure these options through `ModifyServerOptions`: + +```csharp +builder.Services + .AddGraphQLServer() + .ModifyServerOptions(o => + { + o.Sockets.ConnectionInitializationTimeout = TimeSpan.FromSeconds(30); + o.Sockets.KeepAliveInterval = TimeSpan.FromSeconds(12); + }); +``` + +You can also configure WebSocket options per-endpoint when using `MapGraphQLWebSocket`: + +```csharp +app.MapGraphQLWebSocket("/graphql/ws") + .WithOptions(o => + { + o.ConnectionInitializationTimeout = TimeSpan.FromSeconds(30); + o.KeepAliveInterval = TimeSpan.FromSeconds(12); + }); +``` + +## Connection Lifecycle + +A WebSocket connection follows this sequence: + +1. The client opens a WebSocket connection and specifies the sub-protocol. +2. The client sends a `connection_init` message within the `ConnectionInitializationTimeout` window. +3. The server responds with `connection_ack`. +4. The client subscribes to operations by sending `subscribe` messages. +5. The server pushes results via `next` messages. +6. When an operation completes, the server sends a `complete` message. +7. The server sends periodic keep-alive pings at the `KeepAliveInterval`. +8. Either side can close the connection. + +# Server-Sent Events (SSE) + +Server-Sent Events provide an HTTP-based alternative to WebSocket for receiving streaming results. SSE is content-negotiated: the client requests it by sending `Accept: text/event-stream` on the standard GraphQL HTTP endpoint. There is no separate SSE endpoint. + +SSE follows the [GraphQL over SSE](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverSSE.md) specification. + +## When to Use SSE + +SSE is useful in the following scenarios: + +- **Subscriptions over HTTP**: When WebSocket connections are blocked by firewalls, proxies, or load balancers, SSE provides an alternative path for receiving real-time updates. +- **Incremental delivery**: `@defer` and `@stream` results can be streamed via SSE. +- **Browser compatibility**: The browser `EventSource` API natively supports SSE without additional libraries. + +## SSE Wire Format + +The server sends each result as an SSE event: + +```text +event: next +data: {"data":{"onMessageReceived":{"body":"Hello"}}} + +event: next +data: {"data":{"onMessageReceived":{"body":"World"}}} + +event: complete +data: +``` + +Each result is delivered as an `event: next` message with the JSON payload in the `data:` field. A final `event: complete` message signals the end of the stream. + +## SSE for Single Results + +SSE is not limited to streaming. A client can send `Accept: text/event-stream` for a standard query, and the server responds with a single `next` event followed by `complete`. This can be useful when you want a uniform transport across all operation types. + +# Preflight Header Enforcement + +Hot Chocolate provides two settings for enforcing preflight headers as a defense against cross-site request forgery (CSRF) attacks. These settings require that certain requests include a non-standard header (such as `X-Requested-With` or `GraphQL-Preflight`), which triggers a CORS preflight check in browsers. + +| Property | Type | Default | Description | +| ----------------------------------------- | ------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `EnforceGetRequestsPreflightHeader` | `bool` | `false` | When `true`, HTTP GET requests must include a preflight header. Prevents a browser from issuing GET requests via `