# ChilliCream full site context > All substantive ChilliCream pages in the XML sitemap, converted from final rendered HTML without repetitive archive pagination. This generated compatibility export can exceed many model context windows. Prefer the scoped llms.txt and llms-full.txt files when you only need one product or content area. --- # ChilliCream: GraphQL Platform for .NET > Build, federate, observe, and evolve GraphQL APIs on .NET with open-source Hot Chocolate, Fusion, Strawberry Shake, and Mocha, plus the Nitro control plane. Canonical source: https://chillicream.com/ Unify all your APIs into a comprehensive company graph, streamlining data accessibility and enhancing integration. Transform the way you manage and interact with your data. Trusted by Enterprises [](https://www.galaxus.ch/) [](https://www.swisslife.ch/) [](https://www.microsoft.com/) ## Start where you are. Add what you need. Pick the pieces that fit what you are building: a monolith, a federated API, a message-heavy service, or a client application. We give you tools that work on their own and fit together when your platform grows. Products ## Built apart. Queried together. Let teams split the backend where it makes sense: catalog, billing, orders, shipping, identity. Fusion composes the service contracts into one API, so apps keep one place to query while each service can keep moving on its own. Catalog Billing Ordering Shipping User ## Compose before it runs. Each part publishes its contract. Fusion checks that the pieces fit, catches missing lookups and incompatible fields, and produces the gateway artifact your runtime loads. Fusion Composition ## One API for every consumer. Apps, tools, and agents ask for what they need through one gateway. Fusion plans the request across the backend and returns one response. gRPC GraphQL OpenAPI MCP ## Different protocols. One source. Let each caller use the protocol that fits: GraphQL for apps, gRPC for services, OpenAPI for HTTP integrations, and MCP for agents. The API model stays in one place, so every surface can evolve from the same source. ### Web SPA / MPA GraphQL ### Mobile Android / iOS GraphQL ### AI Agents MCP Tools GraphQLMCP ### Partners Federated API OpenAPIgRPC ## The platform, with wheels attached. A GraphQL IDE, a telemetry dashboard, a schema and client registry, and a Fusion query-plan viewer. All of it is the same app. [Explore Nitro](https://chillicream.com/products/nitro) [Nitro / Author](https://chillicream.com/products/nitro) [Nitro / Observe](https://chillicream.com/products/nitro) [Nitro / Evolve](https://chillicream.com/products/nitro) [Nitro / Compose](https://chillicream.com/products/nitro) ## Built for ![](https://chillicream.com/agent-logos/claude.svg)Claude. Use Claude, Codex, Copilot, Cursor, Windsurf, or another supported agent. Checked-in skills give them the same reviewed patterns and conventions; generated results still depend on the model, prompt, and review. Add the skills to your agent. `$ dnx skills add chillicream/agent-skills` [Explore agentic development](https://chillicream.com/platform/agentic-coding) - ![](https://chillicream.com/agent-logos/claude.svg)Claude - ![](https://chillicream.com/agent-logos/codex.svg)Codex - ![](https://chillicream.com/agent-logos/copilot.svg)Copilot - ![](https://chillicream.com/agent-logos/cursor.svg)Cursor - ![](https://chillicream.com/agent-logos/windsurf.svg)Windsurf - ![](https://chillicream.com/agent-logos/gemini.svg)Gemini - ![](https://chillicream.com/agent-logos/cline.svg)Cline - [and many more](https://chillicream.com/platform/agentic-coding) [Keep the time your agent saves you.feat: add reviewsApprovedAddReview.cs+\[Mutation\]+static Task AddReviewAsync(...)ReviewAddedHandler.cs+class ReviewAddedHandler :+ IEventHandler](https://chillicream.com/platform/agentic-coding#review) [Reviewed patterns your agent can follow.Query\[Query\]DataLoader\[DataLoader\]Pagination\[UseConnection\]Authorization\[Authorize\]Message handlerIEventHandlerSagaSagaBatch handlerIBatchEventHandlerRequest handlerIEventRequestHandler](https://chillicream.com/platform/agentic-coding#patterns) [One reviewed skill for every supported agent.MDSKILL.mdskills/1\---2name: graphql-schema-design3description: design + review4 schema changes5\---67\# GraphQL Schema Design89\## Mutations10\- Return a payload type.11\- Model errors as a union.mainmarkdownreviewed](https://chillicream.com/platform/agentic-coding#skills) ## See what the API is doing. Rank reported GraphQL operations by impact and inspect traces from services configured to export supported OpenTelemetry data. The documented .NET setup covers REST, gRPC, and background jobs. [Explore API analytics](https://chillicream.com/platform/analytics) Illustrative incident data [Rank operations by impact.#1checkoutgraphql98#2Billing.Chargegrpc71#3POST /ordersrest46](https://chillicream.com/platform/analytics) [See where time is lost.checkout318 msusers-svc34 mscache19 msbilling201 mspayments127 msorders-db44 ms0100200318 ms](https://chillicream.com/platform/analytics) [Inspect the slow spans.checkout p99318 mscheckoutbilling.Charge+180 msorders](https://chillicream.com/platform/analytics) ## Messaging for work that outlives a request. Mocha lets .NET services publish events, send commands, request replies, and orchestrate sagas across RabbitMQ, Azure Event Hubs, in-memory, and a preview PostgreSQL transport. Configure outbox and inbox middleware for effectively exactly-once processing, and enable OpenTelemetry to trace message flows. [Explore Mocha messaging](https://chillicream.com/products/mocha) [](https://chillicream.com/products/mocha) ## Change contracts with a safety net. Classify schema changes, validate them against operations registered by published client versions, apply lint rules, and keep version history. [Explore schema checks](https://chillicream.com/platform/release-safety) [Use validation as a required CI check.Check operations registered by published client versions.schema publishIllustrative configured checknitro schema publishBreakingProduct.rating removed4,213 req / 7d, web@2.4.0publish blocked](https://chillicream.com/platform/release-safety) [One style, whoever's typing.Naming, structure, and deprecation on every change.schema-lintfailednaming get\_user \-> getUserdeprecation User.email removedstyle product \-> Product](https://chillicream.com/platform/release-safety) [Keep published version history.See what changed, who changed it, and when.schema.graphqlhistoryv14v14Product.ratingalice2dv13User.emailagent/codex5dv12Order.totalAmountbob1w](https://chillicream.com/platform/release-safety) ## Brew it your Way Nitro is the Control Plane and CLI that keeps you in control, whether you’re deploying a new schema, rolling out a new client, or gaining insights into your API environments. ### Free Shared cloud, fully managed. $0 - Shared multi-tenant cloud - Schemas & environments included - 1M operations / month - 2 GB ingest / month - 3-day log & trace retention - Community support [Start Nitro for Free](https://nitro.chillicream.com/) ### Pay as you go Shared cloud, usage based. $20 per month - Shared multi-tenant cloud - 5M operations included, then $2 / million - 2 GB ingest per 1M ops, then $1.15 / GB - 60-day log & trace retention - Email support [Start Nitro for Free](https://nitro.chillicream.com/) Most Popular ### Dedicated Single-tenant, volume based. From $400 per month - Single-tenant cloud or BYOC - Priced by instance size - Configurable retention - Private networking - SSO, audit log, role-based access [Discuss Dedicated](https://chillicream.com/services/support/contact?subject=Sales&context=Dedicated%20Nitro%20Deployment) ### Self-Hosted Your infrastructure. Custom - Run on your own infrastructure - Air-gapped & on-prem supported - Configurable retention - Priority engineering support - Long-term release channel [Discuss Self-Hosted](https://chillicream.com/services/support/contact?subject=Sales&context=Self-Hosted%20Nitro) ## Fancy a drink? Pick a starting point and we’ll help you from there. Explore the platform on your own, or talk to us about the API stack you’re building. [Start Nitro for Free](https://nitro.chillicream.com/) [Talk to Us](https://chillicream.com/services/support/contact) ## From our blog [View all](https://chillicream.com/blog) - [![](https://chillicream.com/images/blog/2026-08-03-directives-all-the-way-down/header.png)Aug 2026Directives All the Way DownGraphQL directives can now be applied to directive definitions themselves in Hot Chocolate 16.4, so you can finally deprecate a directive and attach metadata to your schema's own extension points.Read](https://chillicream.com/blog/2026-08-03-directives-all-the-way-down) - [![](https://chillicream.com/images/blog/2026-07-12-fusion-16-5/header.png)ReleaseJul 2026Fusion 16.5: The Gateway for EveryoneBuilt with C# and .NET, Fusion 16.5 is the only gateway supporting both federation standards, achieves 100% Apollo Federation compliance, and leads in real-world performance.Read](https://chillicream.com/blog/2026-07-12-fusion-16-5) - [![](https://chillicream.com/images/blog/2026-07-06-federated-event-streams/header.png)ReleaseJul 2026Introducing Federated Event Streams for Fusion 16.4Federated Event Streams add broker-backed, resumable GraphQL subscriptions to Fusion 16.4, with stateless gateway scaling and client-owned resume cursors.Read](https://chillicream.com/blog/2026-07-06-federated-event-streams) --- # ChilliCream authors > Browse the authors of the ChilliCream blog. Canonical source: https://chillicream.com/authors Browse ChilliCream blog authors and their published articles. ## Authors [![](https://chillicream.com/_optimized/images/remote/5a65a00398c8fc0afb627eb1d2557dd0da8c797404be8aa6a5e3c3fc8c636689.png)GlenView profile](https://chillicream.com/authors/glen) [![](https://chillicream.com/_optimized/images/remote/2bb0dd6e1b9347a1d3732300ea752f39f1ac4f1f76587433ca3a407245b31786.jpg)Michael StaibView profile](https://chillicream.com/authors/michael-staib) [![](https://chillicream.com/_optimized/images/remote/3ebc6212aa4091005799d46f902a92e2eb7e77cd966797ecb5f4eaeab3bb26fe.png)Pascal SennView profile](https://chillicream.com/authors/pascal-senn) [![](https://chillicream.com/_optimized/images/remote/47f10f3229fb2cfedca9993792f328fe15c7670ac3ed9d4e777b11374b3e4907.png)Rafael StaibView profile](https://chillicream.com/authors/rafael-staib) [![](https://chillicream.com/_optimized/images/remote/3fcbe13df9d90f1d5cb925ee6882b518af2e786a80703bfc0ad4fa00780da77e.jpg)Salome RuckstuhlView profile](https://chillicream.com/authors/salome-ruckstuhl) [![](https://chillicream.com/_optimized/images/remote/301b320e0f0f7a6e613d74ab800c49d42280e6a0d046325f5d65d7112ed71084.jpg)Tobias TenglerView profile](https://chillicream.com/authors/tobias-tengler) --- # Glen, ChilliCream author > Glen works at ChilliCream and writes about Hot Chocolate, Fusion, MCP, and agent-ready GraphQL APIs. Canonical source: https://chillicream.com/authors/glen [All authors](https://chillicream.com/authors) ![Glen's avatar](https://chillicream.com/_optimized/images/remote/5a65a00398c8fc0afb627eb1d2557dd0da8c797404be8aa6a5e3c3fc8c636689.png) Glen works at ChilliCream and writes about Hot Chocolate, Fusion, MCP, and agent-ready GraphQL APIs. - [](https://github.com/glen-84 "GitHub") ## Published articles - [![](https://chillicream.com/images/blog/2026-08-03-directives-all-the-way-down/header.png)Aug 2026Directives All the Way DownGraphQL directives can now be applied to directive definitions themselves in Hot Chocolate 16.4, so you can finally deprecate a directive and attach metadata to your schema's own extension points.Read](https://chillicream.com/blog/2026-08-03-directives-all-the-way-down) - [![](https://chillicream.com/images/blog/2026-05-28-mcp-hotchocolate-fusion/header.png)AIMay 2026From GraphQL to MCP in Two LinesHot Chocolate and Fusion now ship an MCP adapter. Add two lines, author tools and prompts on disk, publish them with Nitro, and connect any MCP host to your GraphQL API.Read](https://chillicream.com/blog/2026-05-28-mcp-hotchocolate-fusion) --- # Michael Staib, ChilliCream author > Michael is the author of Hot Chocolate and works on GraphQL server performance, Fusion, and distributed GraphQL standards. Canonical source: https://chillicream.com/authors/michael-staib [All authors](https://chillicream.com/authors) ![Michael Staib's avatar](https://chillicream.com/_optimized/images/remote/2bb0dd6e1b9347a1d3732300ea752f39f1ac4f1f76587433ca3a407245b31786.jpg) Michael is the author of Hot Chocolate and works on GraphQL server performance, Fusion, and distributed GraphQL standards. - [](https://github.com/michaelstaib "GitHub") - [](https://www.linkedin.com/in/michael-staib-31519571 "LinkedIn") - [](https://x.com/michael%5Fstaib "X") ## Published articles - [![](https://chillicream.com/images/blog/2026-07-12-fusion-16-5/header.png)ReleaseJul 2026Fusion 16.5: The Gateway for EveryoneBuilt with C# and .NET, Fusion 16.5 is the only gateway supporting both federation standards, achieves 100% Apollo Federation compliance, and leads in real-world performance.Read](https://chillicream.com/blog/2026-07-12-fusion-16-5) - [![](https://chillicream.com/images/blog/2026-07-06-federated-event-streams/header.png)ReleaseJul 2026Introducing Federated Event Streams for Fusion 16.4Federated Event Streams add broker-backed, resumable GraphQL subscriptions to Fusion 16.4, with stateless gateway scaling and client-owned resume cursors.Read](https://chillicream.com/blog/2026-07-06-federated-event-streams) - [![](https://chillicream.com/images/blog/2026-05-15-fusion-16/header.png)ReleaseMay 2026What's new in Fusion 16Fusion 16 is a GraphQL federation gateway built on ASP.NET Core, featuring Aspire-driven composition, incremental delivery, Semantic Introspection, OpenAPI adapters, and connectors for REST and gRPC.Read](https://chillicream.com/blog/2026-05-15-fusion-16) - [![](https://chillicream.com/images/blog/2026-05-11-hot-chocolate-16/header.png)ReleaseMay 2026What's new for Hot Chocolate 16Hot Chocolate 16 brings a new type system, better scalar contracts, safer defaults, improved batching, semantic introspection, and a new GraphQL error mode.Read](https://chillicream.com/blog/2026-05-11-hot-chocolate-16) - [![](https://chillicream.com/images/blog/2025-02-01-hot-chocolate-15/hot-chocolate-15.png)ReleaseFeb 2025What's new for Hot Chocolate 15Hot Chocolate 15 updates the type system and supported .NET versions, and adds new projection, filtering, sorting, pagination, and DataLoader capabilities.Read](https://chillicream.com/blog/2025-02-01-hot-chocolate-15) - [![](https://chillicream.com/images/blog/2024-08-30-hot-chocolate-14/hot-chocolate-14.png)ReleaseAug 2024What's new for Hot Chocolate 14Hot Chocolate 14 introduces simpler dependency injection, query inspection, source-generated DataLoaders, pagination improvements, and stronger security.Read](https://chillicream.com/blog/2024-08-30-hot-chocolate-14) - [![](https://chillicream.com/images/blog/2023-08-15-fusion/fusion-banner.png)Aug 2023GraphQL-Fusion: An open approach towards distributed GraphQLTogether, we'll explore the new GraphQL-Fusion, the open approach towards distributed GraphQL.Read](https://chillicream.com/blog/2023-08-15-fusion) - [![](https://chillicream.com/images/blog/2023-02-08-new-in-hot-chocolate-13/hot-chocolate-13-banner.png)ReleaseFeb 2023What's new for Hot Chocolate 13Hot Chocolate 13 improves GraphQL over HTTP, developer experience, authorization, subscriptions, data access, performance, and Strawberry Shake.Read](https://chillicream.com/blog/2023-02-08-new-in-hot-chocolate-13) - [![](https://chillicream.com/images/blog/2022-01-13-hot-chocolate-12-5/hot-chocolate-12-5-banner.png)ReleaseJan 2022Pushing ahead with Hot Chocolate 12.5Hot Chocolate 12.5 adds Banana Cake Pop themes, OpenTelemetry instrumentation, oneOf input objects, and client-controlled nullability.Read](https://chillicream.com/blog/2022-01-13-hot-chocolate-12-5) --- # Pascal Senn, ChilliCream author > Pascal works on ChilliCream's GraphQL platform, focusing on data APIs, OpenTelemetry, semantic introspection, and developer tooling. Canonical source: https://chillicream.com/authors/pascal-senn [All authors](https://chillicream.com/authors) ![Pascal Senn's avatar](https://chillicream.com/_optimized/images/remote/3ebc6212aa4091005799d46f902a92e2eb7e77cd966797ecb5f4eaeab3bb26fe.png) Pascal works on ChilliCream's GraphQL platform, focusing on data APIs, OpenTelemetry, semantic introspection, and developer tooling. - [](https://github.com/pascalsenn "GitHub") - [](https://www.linkedin.com/in/pascal-senn-90899a15a "LinkedIn") - [](https://x.com/Pascal%5FSenn "X") ## Published articles - [![](https://chillicream.com/images/blog/2026-06-06-newsletter-may-2026/header.png)NewsletterJun 2026Newsletter May 2026Hot Chocolate 16, Fusion 16, MCP, OpenAPI, Semantic Introspection, skillz, and more. Read the newsletter to learn about all the things we shipped in May and what comes next.Read](https://chillicream.com/blog/2026-06-06-newsletter-may-2026) - [![](https://chillicream.com/images/blog/2026-06-05-introducing-skillz/header.png)AIJun 2026Introducing skillz: the .NET CLI for Agent Skillsskillz is a .NET CLI for installing, updating, and authoring Agent Skills, with dnx support for a one-shot workflow the way npx skills works in JavaScript.Read](https://chillicream.com/blog/2026-06-05-introducing-skillz) - [![](https://chillicream.com/images/blog/2026-04-22-semantic-introspection/header.png)AIApr 2026Semantic IntrospectionThe agentic age of software brings new challenges for our APIs. Semantic Introspection makes GraphQL discoverable, scalable, and precise for LLMs.Read](https://chillicream.com/blog/2026-04-22-semantic-introspection) - [![](https://chillicream.com/images/blog/2025-03-17-open-telemetry-for-everyone/header.png)Mar 2025Open Telemetry for All Your ServicesSend OpenTelemetry logs and traces from GraphQL APIs, REST services, and background workers to Nitro, with PAT and CLI automation updates.Read](https://chillicream.com/blog/2025-03-17-open-telemetry-for-everyone) - [![](https://chillicream.com/images/blog/2024-10-30-newsletter-october/header.png)NewsletterOct 2024Newsletter OctoberHot Chocolate 14 is released, BCP is now Nitro and there is a new DDD WorkshopRead](https://chillicream.com/blog/2024-10-30-newsletter-october) - [![](https://chillicream.com/images/blog/2024-08-11-logging/header.png)Aug 2024Logging in Banana Cake PopWe just released logging in Banana Cake Pop. Checkout the blog post to learn more!Read](https://chillicream.com/blog/2024-08-11-logging) - [![](https://chillicream.com/images/blog/2024-05-21-newsletter-may/header.png)NewsletterMay 2024Recent HighlightsWe just released the Operation Builder, Telemetry, and a new Full Stack GraphQL Workshop. Checkout the blog post to learn more!Read](https://chillicream.com/blog/2024-05-21-newsletter-may) - [![](https://chillicream.com/images/blog/2024-04-01-fullstack-workshop/header.png)Apr 2024Full Stack GraphQL WorkshopWe're excited to announce our new Full Stack GraphQL Workshop. Learn more about the workshop here!Read](https://chillicream.com/blog/2024-04-01-fullstack-workshop) --- # Rafael Staib, ChilliCream author > Rafael is a software architect and engineer at ChilliCream who writes about Nitro, Banana Cake Pop, and GraphQL developer tooling. Canonical source: https://chillicream.com/authors/rafael-staib [All authors](https://chillicream.com/authors) ![Rafael Staib's avatar](https://chillicream.com/_optimized/images/remote/47f10f3229fb2cfedca9993792f328fe15c7670ac3ed9d4e777b11374b3e4907.png) Rafael is a software architect and engineer at ChilliCream who writes about Nitro, Banana Cake Pop, and GraphQL developer tooling. - [](https://github.com/rstaib "GitHub") - [](https://www.linkedin.com/in/rafaelstaib "LinkedIn") ## Published articles - [![](https://chillicream.com/images/blog/2024-10-07-introducing-nitro/introducing-nitro.png)Oct 2024Introducing Nitro: A New Name, A Unified GraphQL EcosystemMeet Nitro, the unified name for the former Banana Cake Pop app and services and Barista CLI, with the package and local-data migration details.Read](https://chillicream.com/blog/2024-10-07-introducing-nitro) - [![](https://chillicream.com/images/blog/2023-03-15-banana-cake-pop-graphql-apis/lets-boost-your-productivity-with-apis.png)Mar 2023Let’s Boost Your Productivity With APIsTogether, we'll explore the new API feature coming with Banana Cake Pop 5 very soon.Read](https://chillicream.com/blog/2023-03-15-banana-cake-pop-graphql-apis) - [![](https://chillicream.com/images/blog/2023-02-07-new-in-banana-cake-pop-4/new-in-banana-cake-pop-4.png)ReleaseFeb 2023New in Banana Cake Pop 4New document on paste cURL or fetch, schema reload enhancements, new ways of closing tabs, menu enhancements/standardization, and UI polishing.Read](https://chillicream.com/blog/2023-02-07-new-in-banana-cake-pop-4) - [![](https://chillicream.com/images/blog/2023-01-08-new-in-banana-cake-pop-3/new-in-banana-cake-pop-3.png)ReleaseJan 2023New in Banana Cake Pop 3Team Workspaces, Express Middleware, Progressive Web Application (PWA) Support, Enterprise Single Sign-On (SSO), and many more features.Read](https://chillicream.com/blog/2023-01-08-new-in-banana-cake-pop-3) - [![](https://chillicream.com/images/blog/2022-10-05-new-in-banana-cake-pop-2/new-in-banana-cake-pop-2.png)ReleaseOct 2022New in Banana Cake Pop 2Drag & drop support for documents, GraphQL defer/stream support, GraphQL operation extraction, and many further improvements.Read](https://chillicream.com/blog/2022-10-05-new-in-banana-cake-pop-2) - [![](https://chillicream.com/images/blog/2022-09-01-new-in-banana-cake-pop-1/new-in-banana-cake-pop-1.png)ReleaseSep 2022New in Banana Cake Pop 1Subscription protocol auto-detection, Workspace auto synchronization, a Status Bar, and many further improvements.Read](https://chillicream.com/blog/2022-09-01-new-in-banana-cake-pop-1) --- # Salome Ruckstuhl, ChilliCream author > Salome writes about the GraphQL community, federation, AI agents, and the standards shaping the ecosystem. Canonical source: https://chillicream.com/authors/salome-ruckstuhl [All authors](https://chillicream.com/authors) ![Salome Ruckstuhl's avatar](https://chillicream.com/_optimized/images/remote/3fcbe13df9d90f1d5cb925ee6882b518af2e786a80703bfc0ad4fa00780da77e.jpg) Salome writes about the GraphQL community, federation, AI agents, and the standards shaping the ecosystem. - [](https://github.com/sal-ome "GitHub") - [](https://www.linkedin.com/in/salome-ruckstuhl "LinkedIn") ## Published articles - [![](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/header.jpg)Jun 2026Agents, Federation, and a CommunityNotes from GraphQLConf 2026 and Working Group Day at Meta: AI agents meeting GraphQL schemas, the Composite Schema federation standard, and a community building what comes next.Read](https://chillicream.com/blog/2026-06-24-graphqlconf-2026) --- # Tobias Tengler, ChilliCream author > Tobias works at ChilliCream on its GraphQL platform and documentation, with a focus on federation, schema design, REST, and OpenAPI. Canonical source: https://chillicream.com/authors/tobias-tengler [All authors](https://chillicream.com/authors) ![Tobias Tengler's avatar](https://chillicream.com/_optimized/images/remote/301b320e0f0f7a6e613d74ab800c49d42280e6a0d046325f5d65d7112ed71084.jpg) Tobias works at ChilliCream on its GraphQL platform and documentation, with a focus on federation, schema design, REST, and OpenAPI. - [](https://github.com/tobias-tengler "GitHub") - [](https://www.linkedin.com/in/tobiastengler "LinkedIn") - [](https://x.com/tobiastengler "X") ## Published articles - [![](https://chillicream.com/images/blog/2026-06-11-open-your-graphql-api-for-the-rest/header.png)Jun 2026Open Your GraphQL API for the RESTThe new OpenAPI adapter for Hot Chocolate and Fusion turns GraphQL operations into REST endpoints with shared auth, telemetry, and Swagger docs. No second API.Read](https://chillicream.com/blog/2026-06-11-open-your-graphql-api-for-the-rest) --- # Blog > The ChilliCream blog: announcements, deep dives, and how-tos. Canonical source: https://chillicream.com/blog - [![](https://chillicream.com/images/blog/2026-08-03-directives-all-the-way-down/header.png)Aug 2026Directives All the Way DownGraphQL directives can now be applied to directive definitions themselves in Hot Chocolate 16.4, so you can finally deprecate a directive and attach metadata to your schema's own extension points.Read](https://chillicream.com/blog/2026-08-03-directives-all-the-way-down) - [![](https://chillicream.com/images/blog/2026-07-12-fusion-16-5/header.png)ReleaseJul 2026Fusion 16.5: The Gateway for EveryoneBuilt with C# and .NET, Fusion 16.5 is the only gateway supporting both federation standards, achieves 100% Apollo Federation compliance, and leads in real-world performance.Read](https://chillicream.com/blog/2026-07-12-fusion-16-5) - [![](https://chillicream.com/images/blog/2026-07-06-federated-event-streams/header.png)ReleaseJul 2026Introducing Federated Event Streams for Fusion 16.4Federated Event Streams add broker-backed, resumable GraphQL subscriptions to Fusion 16.4, with stateless gateway scaling and client-owned resume cursors.Read](https://chillicream.com/blog/2026-07-06-federated-event-streams) - [![](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/header.jpg)Jun 2026Agents, Federation, and a CommunityNotes from GraphQLConf 2026 and Working Group Day at Meta: AI agents meeting GraphQL schemas, the Composite Schema federation standard, and a community building what comes next.Read](https://chillicream.com/blog/2026-06-24-graphqlconf-2026) - [![](https://chillicream.com/images/blog/2026-06-11-open-your-graphql-api-for-the-rest/header.png)Jun 2026Open Your GraphQL API for the RESTThe new OpenAPI adapter for Hot Chocolate and Fusion turns GraphQL operations into REST endpoints with shared auth, telemetry, and Swagger docs. No second API.Read](https://chillicream.com/blog/2026-06-11-open-your-graphql-api-for-the-rest) - [![](https://chillicream.com/images/blog/2026-06-06-newsletter-may-2026/header.png)NewsletterJun 2026Newsletter May 2026Hot Chocolate 16, Fusion 16, MCP, OpenAPI, Semantic Introspection, skillz, and more. Read the newsletter to learn about all the things we shipped in May and what comes next.Read](https://chillicream.com/blog/2026-06-06-newsletter-may-2026) - [![](https://chillicream.com/images/blog/2026-06-05-introducing-skillz/header.png)AIJun 2026Introducing skillz: the .NET CLI for Agent Skillsskillz is a .NET CLI for installing, updating, and authoring Agent Skills, with dnx support for a one-shot workflow the way npx skills works in JavaScript.Read](https://chillicream.com/blog/2026-06-05-introducing-skillz) - [![](https://chillicream.com/images/blog/2026-05-28-mcp-hotchocolate-fusion/header.png)AIMay 2026From GraphQL to MCP in Two LinesHot Chocolate and Fusion now ship an MCP adapter. Add two lines, author tools and prompts on disk, publish them with Nitro, and connect any MCP host to your GraphQL API.Read](https://chillicream.com/blog/2026-05-28-mcp-hotchocolate-fusion) - [![](https://chillicream.com/images/blog/2026-05-15-fusion-16/header.png)ReleaseMay 2026What's new in Fusion 16Fusion 16 is a GraphQL federation gateway built on ASP.NET Core, featuring Aspire-driven composition, incremental delivery, Semantic Introspection, OpenAPI adapters, and connectors for REST and gRPC.Read](https://chillicream.com/blog/2026-05-15-fusion-16) --- # Documentation > Documentation for the ChilliCream GraphQL Platform. Canonical source: https://chillicream.com/docs - [NitroObservability and governance for APIs](https://chillicream.com/docs/nitro) - [Hot ChocolateGraphQL Server for .NET](https://chillicream.com/docs/hotchocolate) - [FusionFederated GraphQL Gateway](https://chillicream.com/docs/fusion) - [Strawberry ShakeGraphQL Client for .NET](https://chillicream.com/docs/strawberryshake) - [MochaMessaging Bus for .NET](https://chillicream.com/docs/mocha) - [SkillsAgent Skills CLI for .NET](https://chillicream.com/docs/skills) --- # Hot Chocolate: GraphQL Server for .NET > Hot Chocolate is an open-source GraphQL server for .NET that turns your C# classes into a spec-compliant schema and handles parsing, validation, and execution. Canonical source: https://chillicream.com/docs/hotchocolate Hot Chocolate is an open-source [GraphQL](https://graphql.org/) server for .NET. You define your API shape using C# classes and methods, and Hot Chocolate translates that into a spec-compliant GraphQL schema. It handles parsing, validation, execution, and transport so you can focus on your domain logic. ## What Is Hot Chocolate Hot Chocolate is a GraphQL server framework that runs on [ASP.NET Core](https://learn.microsoft.com/aspnet/core). You write C# types and resolvers. Hot Chocolate turns them into a GraphQL schema, validates incoming operations against that schema, executes them, and returns results over HTTP, WebSocket, or Server-Sent Events. Hot Chocolate implements the [GraphQL 2025 specification](https://spec.graphql.org/) and several draft features including `@defer`, `@stream`, and `@requiresOptIn`. It implements the [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/) for transport. It is compatible with all spec-compliant clients, including [Strawberry Shake](https://chillicream.com/docs/strawberryshake), [Relay](https://relay.dev/), and [Apollo Client](https://www.apollographql.com/docs/react/). ## How You Build a Schema Hot Chocolate supports two approaches to building a GraphQL schema. Both produce the same result: a fully typed, spec-compliant GraphQL schema. They differ in how you express it in C#. ### Implementation-First With implementation-first, your C# implementation is the single source of truth for your GraphQL schema. Define your API using familiar C# classes and attributes like `[QueryType]`. At build time, a source generator analyzes your code and creates the GraphQL types for you. You focus on your business logic, since your implementation is your schema. You don’t need to manually keep your code and schema in sync, deal with GraphQL-specific boilerplate, or write large type definitions in C#. C# ``` [QueryType] public static partial class ProductQueries { public static async Task GetProductByIdAsync( int id, CatalogContext db, CancellationToken ct) => await db.Products.FindAsync([id], ct); } ``` This approach is similar to how Meta built their GraphQL server. Your schema stays close to your domain code, while the tooling handles the translation. ### Code-First The code-first approach lets you define your GraphQL types and schema structure directly in C# using Hot Chocolate’s fluent type descriptor API. C# ``` public class ProductType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor .Field(p => p.Id) .Type>(); descriptor .Field(p => p.Name) .Type>(); } } ``` Code-first is useful when you need to decouple the GraphQL schema shape from your C# model, or when you are building infrastructure that generates schemas programmatically. 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. ## GraphQL API Security Strategies GraphQL APIs are typically designed for one of two usage models. Either your API is exclusively consumed by applications you control (first-party), such as your own web, mobile, or internal services, where you define and manage every client operation. Or your API is open to external developers or partners (third-party), and you have no control over the GraphQL operations they send. This distinction shapes how you configure Hot Chocolate and operate your GraphQL server. ### First-party GraphQL A first-party API is consumed exclusively by your own applications. This is how Meta built and operates GraphQL internally. Because you control both the server and every client, you know every operation at build time. This enables you to maintain a precise schema usage history and strictly allow only approved GraphQL operations. Hot Chocolate supports this scenario with **trusted documents**. 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](https://chillicream.com/docs/hotchocolate/performance/trusted-documents) covers the full workflow: extraction, registration, and enforcement. - [Strawberry Shake](https://chillicream.com/docs/strawberryshake) and [Relay](https://relay.dev/docs/guides/persisted-queries/) both support build-time operation extraction. When in the future you want to change or phase out parts of your schema you know the impact this change will have to your system before you apply it. This is a super power for API evolution. ### Third-party GraphQL A third-party API is consumed by external developers or clients outside your organization. GitHub’s GraphQL API is a 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. Hot Chocolate provides **cost analysis** for this scenario. You assign weights to fields and connections, and the server rejects operations that exceed the performance budget before execution begins. - [Cost analysis](https://chillicream.com/docs/hotchocolate/security/cost-analysis) explains field weights, type costs, and budget configuration. - [Controlling introspection](https://chillicream.com/docs/hotchocolate/security/introspection) lets you restrict schema visibility in production. These two approaches complement each other. A common setup is to host both a public and an internal GraphQL API. The internal API uses trusted documents to strictly control operations, while the public API relies on cost analysis and other safeguards to manage external traffic and protect against abuse. ## Key Terminology | Term | Definition | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Schema** | The contract that describes what data clients can query. Hot Chocolate generates it from your C# code. | | **Query type** | The root type for read operations. Clients enter the graph through fields on this type. | | **Mutation type** | The root type for write operations. Mutations execute serially and are expected to cause side effects. | | **Subscription type** | The root type for real-time operations. Clients subscribe to events and receive updates as they occur. | | **Resolver** | A function that fetches data for a single field. In implementation-first, each public method on a \[QueryType\] class is a resolver for third-party or first-party APIs. | | **Batch resolver** | A resolver that fetches data for multiple parent objects in a single call, improving performance by reducing the number of backend requests. Useful for solving the N+1 problem and optimizing data access patterns. | | **DataLoader** | A batching and caching layer that groups multiple individual data requests into a single batch call, eliminating the N+1 problem. | | **Source generator** | A Roslyn source generator that inspects your C# code at build time and generates the schema registration, resolver pipelines, and DataLoader infrastructure. | | **Cost analysis** | A static analysis pass that calculates the cost of a query before execution and rejects queries that exceed configured limits. Based on the [IBM Cost Analysis specification](https://ibm.github.io/graphql-specs/cost-spec.html). | | **Trusted documents** | Pre-registered operations that the server accepts by hash. Operations not in the store are rejected. Also known as persisted operations. | ## Scaling Beyond a Single Server When your API grows beyond what a single service can handle, [Fusion](https://chillicream.com/docs/fusion) lets you split your schema across multiple independent services. Each service owns part of the API surface. A gateway composes them into one unified schema that clients query as a single endpoint. Fusion is not a separate product. It builds on Hot Chocolate. A standard Hot Chocolate server can act as a Fusion subgraph without changes to its resolvers or type definitions. You can start with a single Hot Chocolate server and add Fusion later when you need independent deployment or team-level ownership boundaries. ## Next Steps Where you go from here depends on what you need: - **"I want to build something."** Start with the [Getting Started](https://chillicream.com/docs/hotchocolate/get-started-with-graphql-in-net-core) tutorial. You will create a running GraphQL server in under five minutes. - **"I want to understand the schema system."** Read [Defining a Schema](https://chillicream.com/docs/hotchocolate/defining-a-schema). It covers queries, mutations, subscriptions, and all the GraphQL types. - **"I need to fetch data efficiently."** Go to [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader) for batching and caching, or [Resolvers](https://chillicream.com/docs/hotchocolate/resolvers) for the full resolver API. - **"I need to secure my API."** See [Securing Your API](https://chillicream.com/docs/hotchocolate/security) for authentication, authorization, cost analysis, and trusted documents. - **"I'm migrating from an older version."** Read the [migration guide from v15 to v16](https://chillicream.com/docs/hotchocolate/migrating/migrate-from-15-to-16). - **"I want to split my API across services."** See [Fusion](https://chillicream.com/docs/fusion) for distributed GraphQL with a gateway. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/index.md) Maintained by ChilliCream. Last updated on **July 01, 2026** by **Tobias Tengler** --- # MCP - Hot Chocolate > Turn a Hot Chocolate GraphQL server into an MCP server with `AddMcp()` and `MapGraphQLMcp()`, exposing tools and prompts to AI assistants over Streamable HTTP. Canonical source: https://chillicream.com/docs/hotchocolate/adapters/mcp The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that lets AI assistants connect to external systems through a uniform tool and prompt interface. The `HotChocolate.Adapters.Mcp` package turns a Hot Chocolate GraphQL server into an MCP server. You supply tool and prompt definitions through an `IMcpStorage`, and the adapter handles execution, transport, and live updates. Definitions can come from Nitro (file-based authoring with a publish workflow) or from a custom `IMcpStorage` you build (programmatic, database-backed, or any source you choose). You wire MCP onto an existing Hot Chocolate server with two calls: `AddMcp()` during service registration and `MapGraphQLMcp()` during endpoint mapping. The adapter exposes the MCP server over Streamable HTTP at `/graphql/mcp` by default, so any MCP client (Claude Desktop, an editor extension, an agent runtime) can connect directly to your server. This page covers wiring and configuration. For authoring tools and prompts, see the [Nitro MCP](https://chillicream.com/docs/nitro/adapters/mcp) section. ## Prerequisites You need an existing Hot Chocolate GraphQL server. If you do not have one yet, follow the [Get started with Hot Chocolate](https://chillicream.com/docs/hotchocolate/get-started-with-graphql-in-net-core) tutorial first. Add the adapter package to the server project: Bash ``` dotnet add package HotChocolate.Adapters.Mcp ``` ## Enabling MCP on the Server Two adapter calls turn a Hot Chocolate server into an MCP server. `AddMcp()` registers the MCP server, schema services, and a startup warmup that loads tool and prompt definitions from storage. `MapGraphQLMcp()` exposes the MCP transport endpoints. C# ``` builder .AddGraphQL() .AddMcp(); // Storage is required: see "Connecting a Tool and Prompt Source" below. // ... app.MapGraphQLMcp(); ``` The server needs a tool and prompt source. Without one, `MapGraphQLMcp()` throws `InvalidOperationException` during startup. Wire up storage by either [using Nitro](#using-nitro-for-tools-and-prompts) or providing a custom `IMcpStorage`. ## Connecting a Tool and Prompt Source The adapter does not ship tools or prompts of its own. It asks an `IMcpStorage` implementation for them at startup, and listens for change notifications afterwards. You have two options: 1. **Use Nitro** (recommended for production). Nitro publishes versioned MCP feature collections to the server and supplies an `IMcpStorage` automatically. Skip ahead to [Using Nitro](#using-nitro-for-tools-and-prompts). 2. **Provide your own `IMcpStorage`** for self-hosted scenarios where you manage tool definitions outside Nitro. To register a custom storage, implement `IMcpStorage` and pass it to `AddMcpStorage()`: C# ``` builder .AddGraphQL() .AddMcp() .AddMcpStorage(); ``` Three overloads cover the common registration patterns: | Overload | Use when | | -------------------------------------------------- | ----------------------------------------------------------------------- | | AddMcpStorage(IMcpStorage instance) | You already have a singleton instance. | | AddMcpStorage() | You want DI to construct the storage. T is activated from app services. | | AddMcpStorage(Func) | You need a factory for custom construction or scoping. | `IMcpStorage` returns `OperationToolDefinition` and `PromptDefinition` collections, and exposes `IObservable` streams so the server can apply update and remove events without restarting. Reach for this extension point only when you cannot use Nitro, since implementing it correctly involves change diffing, caching, and reactive subscriptions. ## Configuring the MCP Server `AddMcp()` accepts two configuration delegates. The first targets `McpServerOptions` (server behavior), the second targets `IMcpServerBuilder` (tool, prompt, and resource registration from the underlying MCP SDK): C# ``` builder .AddGraphQL() .AddMcp( configureServerOptions: options => { options.InitializationTimeout = TimeSpan.FromSeconds(30); }, configureServer: server => { // Register additional MCP server features here. }); ``` Tools registered through `configureServer` appear alongside the GraphQL-derived tools, so you can mix native MCP tools with operation tools in the same server. ## Mapping the MCP Endpoint `MapGraphQLMcp()` accepts two optional arguments: C# ``` app.MapGraphQLMcp(pattern: "/graphql/mcp", schemaName: null); ``` - **`pattern`**: the URL prefix for the MCP transport. Defaults to `/graphql/mcp`. Change it when the default conflicts with another route or when you expose multiple schemas from the same host. - **`schemaName`**: the named schema to expose. The adapter resolves this automatically when the server has a single schema. Pass it explicitly when the host registers multiple named schemas, so each schema gets its own MCP endpoint: C# ``` app.MapGraphQLMcp("/graphql/public/mcp", schemaName: "Public"); app.MapGraphQLMcp("/graphql/internal/mcp", schemaName: "Internal"); ``` The endpoint speaks Streamable HTTP. POST carries JSON-RPC requests and returns either `text/event-stream` for streaming responses or `202 Accepted` for queued ones. ## Using Nitro for Tools and Prompts Nitro is the easiest way to manage MCP tools and prompts. You author tools and prompts on disk, upload them as a tagged version of a feature collection with the Nitro CLI, and publish that version to a stage. The server loads the collection from the configured stage and picks up new versions automatically. When Nitro is wired up alongside `AddMcp()`, it registers an `IMcpStorage` for you, so you do not call `AddMcpStorage()` yourself. ### Install the Nitro packages Bash ``` dotnet add package ChilliCream.Nitro dotnet add package ChilliCream.Nitro.HotChocolate ``` `ChilliCream.Nitro` is the core package and includes a source generator that emits an `AddDefaults()` extension method based on which integration packages are referenced in the project. With `ChilliCream.Nitro.HotChocolate` referenced, `AddDefaults()` calls `AddHotChocolate()` for you. ### Wire Nitro into the server Call `AddNitro().AddDefaults()` (or the explicit `AddNitro().AddHotChocolate()`) before configuring the GraphQL server. `AddNitro()` configures the shared connection options (`ApiId`, `ApiKey`, `Stage`) on `NitroServiceOptions`. `ModifyNitroOptions()` on the GraphQL builder configures schema-specific options (MCP, OpenAPI, persisted operations, metrics, and so on): C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddNitro(o => { o.ApiId = builder.Configuration["Nitro:ApiId"]!; o.ApiKey = builder.Configuration["Nitro:ApiKey"]!; o.Stage = builder.Configuration["Nitro:Stage"]!; }) .AddDefaults(); builder .AddGraphQL() .ModifyNitroOptions(o => { // Modify Nitro options here. }) .AddMcp(); var app = builder.Build(); app.MapGraphQL(); app.MapGraphQLMcp(); app.Run(); ``` If you prefer environment variables over inline configuration, set `NITRO_API_ID`, `NITRO_STAGE`, and `NITRO_API_KEY`. The Nitro service options bind to these automatically and you can drop the `AddNitro` configuration delegate entirely. > Order matters: `AddNitro().AddDefaults()` (or `AddHotChocolate()`) must run before the GraphQL builder calls so that the Hot Chocolate pipeline picks up the Nitro contributions during registration. ### What you get With Nitro and MCP both enabled: - The server loads the published MCP feature collection for the configured stage on startup. - Tool and prompt definitions are cached locally so cold starts work without a round trip to Nitro. - Stage change events flow over the Nitro change feed. When you publish a new version, the server updates its tool and prompt set in place, no restart required. - If Nitro configuration is incomplete (any of `ApiId`, `ApiKey`, or `Stage` not set), MCP integration is disabled with a warning, the storage returns no definitions, and the host continues to start. - If configuration is set but the API key is rejected, the storage uses the local cache when one is available and logs the sync failure. Without a usable cache, the exception propagates and host startup fails. For authoring tools and prompts, publishing feature collection versions, and managing stages, see the [Nitro MCP](https://chillicream.com/docs/nitro/adapters/mcp) section. ## Troubleshooting ### `InvalidOperationException: Call AddMcp() when configuring the GraphQL server.` `MapGraphQLMcp()` was called but `AddMcp()` was not registered on the GraphQL server builder. Add `.AddMcp()` to the chain that starts with `AddGraphQL()`. ### MCP endpoint returns 404 Not Found The route pattern does not match what your client uses. The default is `/graphql/mcp`. If you passed a custom pattern to `MapGraphQLMcp()`, point your client at it. ### `InvalidOperationException: No IMcpStorage is registered for schema ''.` Two possible causes: - **No storage source for that schema.** `AddMcp()` was registered, but no `IMcpStorage` is wired up. Either reference `ChilliCream.Nitro.HotChocolate` and call `AddNitro().AddDefaults()` (or `AddNitro().AddHotChocolate()`), or register a custom storage with `AddMcpStorage(...)`. - **Endpoint mapped to the wrong schema.** `MapGraphQLMcp(pattern, schemaName)` was called with a `schemaName` that does not match any schema registered through `AddGraphQL(name)` plus `AddMcp()`. With multiple named schemas, pass the matching name. With a single unnamed schema, omit `schemaName` and the adapter resolves it automatically. ### Tools and prompts list is empty Storage is registered but returned no definitions. With Nitro, ensure a published MCP feature collection version exists for the configured stage and that the API key has read access to it. With a custom `IMcpStorage`, verify that the implementation completes its initial fetch before the warmup task times out and that it returns the expected definitions. ### Nitro logs `MCP integration is disabled because Nitro is not properly configured.` `NitroServiceOptions` is missing one or more of `ApiId`, `ApiKey`, or `Stage`. Set them through the `AddNitro()` configuration delegate or via the `NITRO_API_ID`, `NITRO_API_KEY`, and `NITRO_STAGE` environment variables. ## Next Steps - Author tools and prompts and publish them with Nitro in the [Nitro MCP](https://chillicream.com/docs/nitro/adapters/mcp) section. - Customize how GraphQL errors surface in MCP tool results with [Errors](https://chillicream.com/docs/hotchocolate/resolvers/errors). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/adapters/mcp.md) Maintained by ChilliCream. Last updated on **August 12, 2026** by **Glen** --- # OpenAPI Adapter - Hot Chocolate > Expose REST endpoints from a Hot Chocolate GraphQL schema with the AddOpenApi() adapter: annotate operations with @http and get generated OpenAPI docs. Canonical source: https://chillicream.com/docs/hotchocolate/adapters/openapi 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: C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddRouting() .AddOpenApi(options => options.AddGraphQLTransformer()); builder .AddGraphQL() .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. ### One model per document A fragment-only document defines exactly one shared model. The **first** fragment in the document names the model, and that name is what other documents spread. Any further fragments in the same document are private helpers: fragments within that document can spread them, but no other document can reference them by name. GraphQL ``` # A model named "User", plus a helper only this document can spread fragment User on User { ...UserContact id name } fragment UserContact on User { email address { street } } ``` To make a fragment referenceable from another document, give it a document of its own. ### Fragment name uniqueness Fragment names must be unique across every document that contributes to a single endpoint, which is the endpoint's own document plus each model it spreads, transitively. When two of those documents define a fragment with the same name, the endpoint's composed document is invalid and the endpoint responds with HTTP 500\. Private helper fragments are not namespaced by their document, so this applies to them as well. An endpoint that spreads a fragment no document defines is also invalid, and likewise responds with HTTP 500\. In both cases the endpoint is still routed, and it is omitted from the OpenAPI specification. Register a diagnostic event listener (see [Diagnostics](#diagnostics)) to see the reason. ## Storage The `IOpenApiDefinitionStorage` interface provides endpoint and fragment definitions to the adapter: C# ``` using HotChocolate.Adapters.OpenApi; using HotChocolate.Adapters.OpenApi.Storage; using HotChocolate.Language; public class MyOpenApiStorage : IOpenApiDefinitionStorage { 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); } // IOpenApiDefinitionStorage also extends IObservable, enabling // hot-reload when definitions change. public IDisposable Subscribe( IObserver observer) => /* your subscription logic */; } ``` Register it with your GraphQL server: C# ``` var storage = new MyOpenApiStorage(); builder .AddGraphQL() .AddQueryType() .AddOpenApiDefinitionStorage(storage); ``` The storage implements `IObservable`. When you push `Updated` or `Removed` events through this observable, 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. ## Diagnostics A definition that the adapter cannot use is skipped rather than throwing at startup, so an endpoint can be routed and still fail every call with HTTP 500\. Derive from `OpenApiDiagnosticEventListener` to observe why: C# ``` using HotChocolate.Adapters.OpenApi; public class LoggingOpenApiDiagnosticEventListener( ILogger logger) : OpenApiDiagnosticEventListener { public override void ValidationErrors( IReadOnlyList errors) { foreach (var error in errors) { logger.LogError("{ValidationError}", error.Message); } } } ``` Register it on the GraphQL server. The listener is activated from the schema services, so any application service it takes as a constructor argument, `ILogger` included, must be made available with `AddApplicationService()`: C# ``` builder .AddGraphQL() .AddQueryType() .AddOpenApi() .AddApplicationService>() .AddDiagnosticEventListener(); ``` The listener receives an error for every definition the adapter cannot use: one that fails a validation rule, an endpoint whose composed document does not validate against the schema, an endpoint that could not be initialized, and a definition that could not be added to the OpenAPI document. Errors are reported whenever definitions are loaded or reloaded, so a hot-reload that breaks an endpoint reports it at that moment. ## 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 `AddGraphQL()` with `AddGraphQLGateway()` and the rest of the configuration remains the same: C# ``` builder .AddGraphQLGateway() .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. ## Next Steps - [MCP](https://chillicream.com/docs/hotchocolate/adapters/mcp) to expose your GraphQL schema as MCP tools for AI agents. - [Errors](https://chillicream.com/docs/hotchocolate/resolvers/errors) to customize error responses in generated endpoints. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/adapters/openapi.md) Maintained by ChilliCream. Last updated on **August 12, 2026** by **Glen** --- # Type System - Hot Chocolate > Overview of the Hot Chocolate type system: how C# classes map to GraphQL SDL for objects, inputs, enums, interfaces, unions, and scalars in your schema. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema A GraphQL schema defines the contract that clients interact with. It specifies the available operations, selectable fields, accepted arguments, and the structure of responses. Hot Chocolate enables you to define this contract in C# and review the resulting GraphQL SDL. This page serves as your guide: it introduces the main type system concepts, demonstrates how a C# model translates to SDL, and directs you to detailed pages for each modeling task. ## Understanding the Schema as the API Contract When working with GraphQL, everything starts with the schema. The schema defines the contract between your API and its clients, describing the available operations and the data that can be requested. Clients interact with your API by sending GraphQL operations. They do not call your C# methods or classes directly. Instead, they rely on the schema to understand what is possible. To introduce the main concepts of the GraphQL type system, consider the following example. This schema highlights the essential building blocks you will encounter: GraphQL ``` type Query { bookById(id: ID!): Book } type Mutation { createBook(input: CreateBookInput!): Book! } type Book { id: ID! title: String! authors: [Author!]! } type Author { id: ID! name: String! } input CreateBookInput { title: String! authorIds: [ID!]! } ``` This example presents queries, mutations, object types, input types, and scalar values. Each part of the schema plays a specific role in shaping how clients interact with your API. The table below breaks down these elements and explains their purpose within the type system. | SDL part | Type system member | What it means | Learn more | | ----------------------- | ------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------- | | Query | Operation root type | Entry point for read operations. | [Queries](https://chillicream.com/docs/hotchocolate/defining-a-schema/queries) | | Mutation | Operation root type | Entry point for write operations. | [Mutations](https://chillicream.com/docs/hotchocolate/defining-a-schema/mutations) | | bookById and createBook | Fields | Selectable members on a type. | [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types) | | id and input | Arguments | Values supplied to a field. | [Arguments](https://chillicream.com/docs/hotchocolate/defining-a-schema/arguments) | | Book and Author | Object types | Returned data shapes. | [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types) | | CreateBookInput | Input object type | Structured data sent by a client. | [Input Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types) | | ID and String | Scalars | Leaf values with no subfields. | [Scalars](https://chillicream.com/docs/hotchocolate/defining-a-schema/scalars) | | ! and \[\] | Type modifiers | Non-null and list wrappers. | [Lists and Non-Null](https://chillicream.com/docs/hotchocolate/defining-a-schema/lists) | If you are unsure how to identify the parts of your schema, start by looking at the SDL. Elements that a client can select in a query are fields. Values that a client supplies to those fields are arguments or input fields. When you see `!` or `[]` wrapping another type, these are type modifiers that indicate non-nullability or lists. For example, a client might send the following query and mutation: GraphQL ``` query { bookById(id: "1") { title authors { name } } } mutation { createBook(input: { title: "New Book", authorIds: ["2"] }) { id title } } ``` In these examples, `bookById` and `createBook` are fields. The `id` and `input` values are arguments. The selections inside the curly braces, such as `title` and `authors`, are fields on the returned types. ## Schema Authoring Styles Hot Chocolate offers two C# authoring styles, both of which produce GraphQL SDL. ### Implementation-First The implementation-first approach allows you to define your GraphQL schema using standard C# types and attributes. This keeps your contract close to your domain code with minimal ceremony, provides compile-time feedback, and ensures a clear mapping between code and schema. This workflow is streamlined for most application schemas. Common building blocks include: - `[QueryType]`, `[MutationType]`, and `[SubscriptionType]` for operation root fields - `partial` source-generator classes for annotated root type classes - Attributes such as `[GraphQLName]`, `[GraphQLDescription]`, `[GraphQLIgnore]`, `[ID]`, `[Node]`, `[InterfaceType]`, `[UnionType]`, and `[OneOf]` when the inferred schema needs guidance - Generated dependency injection modules for GraphQL types ### Code-First The code-first approach allows you to define GraphQL types and their structure directly in C# using the Hot Chocolate type descriptor API. This is useful when your GraphQL schema needs to differ from your C# model or when building reusable schema components. C# ``` using HotChocolate.Types; public sealed class BookType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor .Field(t => t.Title) .Type>() .Description("The title displayed to readers."); } } ``` ## Navigating the Type System Map The following map helps you select the next detailed page without needing to learn every rule here. ### Root Types GraphQL defines a single root type for each operation kind. In C#, you can organize root fields across multiple semantic classes, and Hot Chocolate will merge them into the final root type. | Element | Purpose | Authoring cue | Learn more | | ------------ | -------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------ | | Query | Read entry point. Query fields should be side-effect-free and may execute in parallel. | \[QueryType\] or AddQueryType. | [Queries](https://chillicream.com/docs/hotchocolate/defining-a-schema/queries) | | Mutation | Write entry point. Top-level mutation fields execute serially. | \[MutationType\] or AddMutationType. | [Mutations](https://chillicream.com/docs/hotchocolate/defining-a-schema/mutations) | | Subscription | Event stream entry point. | \[SubscriptionType\] or AddSubscriptionType. | [Subscriptions](https://chillicream.com/docs/hotchocolate/defining-a-schema/subscriptions) | ### Output Types Output types describe the shapes of data that clients can select after a field resolves. | Element | Use it for | Learn more | | ----------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | Object type | Normal returned data shapes, such as Book or Author. | [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types) | | Scalar | Leaf values such as String, Int, Boolean, ID, UUID, URI, DateTime, Any, and custom scalars. | [Scalars](https://chillicream.com/docs/hotchocolate/defining-a-schema/scalars) | | Enum | A closed set of symbolic values in input or output positions. | [Enums](https://chillicream.com/docs/hotchocolate/defining-a-schema/enums) | | Interface | Polymorphic output types that share fields. | [Interfaces](https://chillicream.com/docs/hotchocolate/defining-a-schema/interfaces) | | Union | Polymorphic output types that do not need shared fields. | [Unions](https://chillicream.com/docs/hotchocolate/defining-a-schema/unions) | ### Fields and Arguments Fields and arguments define how clients interact with your schema. Fields are selectable members on types, while arguments allow clients to supply values to those fields. | Element | Purpose | Learn more | | -------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Field | A selectable member on a root type or object type. Root fields start operations. Nested fields shape returned data. | [Queries](https://chillicream.com/docs/hotchocolate/defining-a-schema/queries), [Mutations](https://chillicream.com/docs/hotchocolate/defining-a-schema/mutations), [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types) | | Argument | A value supplied to a field. Common uses include lookup IDs, filters, paging arguments, and mutation payloads. | [Arguments](https://chillicream.com/docs/hotchocolate/defining-a-schema/arguments) | Not every C# method parameter becomes an argument. Service parameters, parent values, cancellation tokens, and resolver context parameters are resolver concerns. ### Input Types Input types define the shapes of data that clients can send to your API, such as arguments and input objects for mutations and queries. | Element | Use it for | Learn more | | ----------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | Argument | A scalar, enum, ID, list, or input object supplied to a field. | [Arguments](https://chillicream.com/docs/hotchocolate/defining-a-schema/arguments) | | Input object type | Structured payloads for mutations, filters, and other complex field inputs. | [Input Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types) | GraphQL separates input and output type systems. Input objects can use defaults, `Optional`, and `@oneOf`, but those rules are detailed on the input pages. ### Type Modifiers Type modifiers such as non-null and list indicate whether a field or argument is required or can accept multiple values. | Modifier | Example | What it says | Learn more | | ----------------------- | ---------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------- | | Non-null | String! | The field or argument must not be null in the contract. | [Lists and Non-Null](https://chillicream.com/docs/hotchocolate/defining-a-schema/lists) | | List | \[Book\] | The value is a collection. | [Lists and Non-Null](https://chillicream.com/docs/hotchocolate/defining-a-schema/lists) | | List plus item non-null | \[Book!\]! | The list is required, and every item is required. | [Lists and Non-Null](https://chillicream.com/docs/hotchocolate/defining-a-schema/lists) | Input optionality, default values, and output nullability are related but not identical. Refer to the type modifier and input object pages when the distinction matters. ### Schema Organization and Advanced Modeling Organize your schema for maintainability and support advanced modeling scenarios using type extensions, Relay helpers, and dynamic schemas. | Topic | Use it for | Learn more | | --------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | Type extensions | Split large object or root type definitions across classes. Extensions are merged into the final schema. | [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types) | | Relay helpers | Use stable IDs, global object identification, node, nodes, \[ID\], \[Node\], and \[NodeResolver\]. | [Relay](https://chillicream.com/docs/hotchocolate/defining-a-schema/relay) | | Dynamic schemas | Generate types from CMS, multi-tenant, or configuration-driven metadata with ITypeModule. | [Dynamic Schemas](https://chillicream.com/docs/hotchocolate/defining-a-schema/dynamic-schemas) | ### Contract Metadata and Lifecycle Metadata and lifecycle features help you document, annotate, and evolve your schema safely over time. | Topic | Use it for | Learn more | | ------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | Descriptions | Add schema documentation from XML comments, \[GraphQLDescription\], or descriptors. | [Documentation Comments](https://chillicream.com/docs/hotchocolate/defining-a-schema/documentation) | | Directives | Add schema metadata or executable behavior, depending on the directive kind. | [Directives](https://chillicream.com/docs/hotchocolate/defining-a-schema/directives) | | Deprecation and evolution | Communicate lifecycle changes and plan compatible schema updates. | [Versioning](https://chillicream.com/docs/hotchocolate/defining-a-schema/versioning) | Use type extensions for static modularity. Choose dynamic schemas only when the schema must change based on external metadata or runtime configuration. ## Next steps - Build a read entry point with [Queries](https://chillicream.com/docs/hotchocolate/defining-a-schema/queries). - Learn how returned shapes are inferred and configured in [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). - Add field inputs with [Arguments](https://chillicream.com/docs/hotchocolate/defining-a-schema/arguments) and [Input Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types). - Strengthen the contract with [Lists and Non-Null](https://chillicream.com/docs/hotchocolate/defining-a-schema/lists). - Move to runtime behavior with [Resolvers](https://chillicream.com/docs/hotchocolate/resolvers) and [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader) after the schema shape is clear. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/index.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Arguments - Hot Chocolate > Define field arguments in Hot Chocolate: resolver method parameters become GraphQL arguments, with default values, the [ID] attribute, and input object arguments. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/arguments GraphQL arguments let clients pass values to individual fields. In Hot Chocolate, each parameter on a resolver method becomes a field argument in the schema, unless it is a recognized service type (like `CancellationToken` or a registered service). **GraphQL schema** GraphQL ``` type Query { user(id: ID!): User users(role: UserRole, limit: Int = 10): [User!]! } ``` **Client query** GraphQL ``` { user(id: "UHJvZHVjdAppMQ==") { name } } ``` Arguments are frequently provided through variables, which separate the static query structure from the dynamic runtime values: GraphQL ``` query ($userId: ID!) { user(id: $userId) { name } } ``` ## Defining Arguments Method parameters on a resolver become GraphQL arguments. C# ``` [QueryType] public static partial class UserQueries { public static User? GetUser(string username, UserService users) => users.FindByName(username); } ``` The `username` parameter becomes a `username: String!` argument. The `UserService` parameter is recognized as a service and is not exposed in the schema. ## Renaming Arguments Use `[GraphQLName]` to change the argument name in the schema while keeping the C# parameter name unchanged. C# ``` [QueryType] public static partial class UserQueries { public static User? GetUser( [GraphQLName("name")] string username, UserService users) => users.FindByName(username); } ``` This produces `user(name: String!): User` in the schema. ## Optional Arguments An argument is required when its C# type is non-nullable. Make an argument optional by using a nullable type. C# ``` [QueryType] public static partial class ProductQueries { public static List GetProducts(string? category, int? limit) { // Both arguments are optional // ... } } ``` This produces: GraphQL ``` type Query { products(category: String, limit: Int): [Product!]! } ``` When using nullable reference types (recommended), `string` maps to `String!` and `string?` maps to `String`. See [Non-Null](https://chillicream.com/docs/hotchocolate/defining-a-schema/non-null) for details. ## Default Values Use `[DefaultValue]` to assign a default to an argument. The default appears in the schema and is used when the client omits the argument. C# ``` [QueryType] public static partial class ProductQueries { public static List GetProducts( [DefaultValue(10)] int limit) { // ... } } ``` This produces `products(limit: Int! = 10): [Product!]!`. C# default parameter values also work: C# ``` public static List GetProducts(int limit = 10) ``` For complex default values that cannot be expressed as C# constants (such as input objects), use `[DefaultValueSyntax]` with GraphQL value syntax: C# ``` [QueryType] public static partial class ProductQueries { public static List GetProducts( [DefaultValueSyntax("{ title: null, year: 2024 }")] BookFilterInput filter) { // ... } } ``` This produces `products(filter: BookFilterInput! = { title: null, year: 2024 }): [Product!]!`. The string is parsed as a GraphQL value literal at schema build time. ## The ID Attribute The `[ID]` attribute marks a parameter as a GraphQL `ID` scalar. When combined with [global object identification](https://chillicream.com/docs/hotchocolate/defining-a-schema/relay), it also deserializes the opaque global ID back to the underlying value. C# ``` [QueryType] public static partial class ProductQueries { public static Product? GetProduct([ID] int id, CatalogContext db) => db.Products.Find(id); } ``` To restrict the ID to a specific type (ensuring only IDs serialized for `Product` are accepted): C# ``` public static Product? GetProduct( [ID(nameof(Product))] int id, CatalogContext db) => db.Products.Find(id); ``` You can also use the generic form `[ID]` which infers the type name automatically. ## Complex Arguments When an argument needs multiple fields, use an [input object type](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types) instead of multiple scalar arguments. C# ``` public record BookFilterInput(string? Title, string? Author, int? Year); [QueryType] public static partial class BookQueries { public static List GetBooks(BookFilterInput filter, CatalogContext db) { // ... } } ``` This produces: GraphQL ``` input BookFilterInput { title: String author: String year: Int } type Query { books(filter: BookFilterInput!): [Book!]! } ``` ## Next Steps - **Need structured input?** See [Input Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types). - **Need to understand nullability?** See [Non-Null](https://chillicream.com/docs/hotchocolate/defining-a-schema/non-null). - **Need global IDs?** See [Relay](https://chillicream.com/docs/hotchocolate/defining-a-schema/relay). - **Need to set up resolvers?** See [Resolvers](https://chillicream.com/docs/hotchocolate/resolvers). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/arguments.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Directives - Hot Chocolate > Create and apply GraphQL directives in Hot Chocolate, from built-ins like @skip and @include to custom directive types that alter execution behavior. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/directives Directives let you add metadata for client tools (such as code generators and IDEs) or modify a GraphQL server’s runtime execution and type validation behavior. There are two kinds of directives: executable directives, which annotate parts of GraphQL documents, and type-system directives, which annotate SDL types. The GraphQL specification defines five built-in directives that every server must support: | Directive | Kind | SDL | | | | ------------ | ----------- | ----------------------------------------------------------------------------------------------------------- | ------------------------ | ----------- | | @skip | Executable | directive @skip(if: Boolean!) on FIELD \| FRAGMENT\_SPREAD | INLINE\_FRAGMENT | | | @include | Executable | directive @include(if: Boolean!) on FIELD \| FRAGMENT\_SPREAD | INLINE\_FRAGMENT | | | @deprecated | Type-system | directive @deprecated(reason: String! = "No longer supported") on FIELD\_DEFINITION \| ARGUMENT\_DEFINITION | INPUT\_FIELD\_DEFINITION | ENUM\_VALUE | | @specifiedBy | Type-system | directive @specifiedBy(url: String!) on SCALAR | | | | @oneOf | Type-system | directive @oneOf on INPUT\_OBJECT | | | `@skip` and `@include` are executable directives used in queries to conditionally exclude or include fields. `@deprecated` marks schema elements as deprecated. `@specifiedBy` provides a URL pointing to the specification of a custom scalar type. `@oneOf` marks an input object as requiring exactly one of its fields to be set. ## Structure Directives consist of a name and zero or more arguments. `@skip`, for example, has the name **skip** and a mandatory argument named **if**. Also, `@skip` carries a piece of hidden information only examinable in SDL, namely the location, which specifies where a directive is applicable. Let's take a look at the SDL of the `@skip` directive. SDL ``` directive @skip(if: Boolean!) on | FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT ``` The `directive` keyword in SDL indicates that we're dealing with a directive type declaration. The `@` sign also indicates that this is a directive but more from a usage perspective. The word `skip` represents the directive's name followed by a pair of parentheses that includes a list of arguments, consisting, in our case, of one argument named `if` of type non-nullable boolean (meaning it is required). The `on` keyword indicates the location where or at which part a directive is applicable, followed by a list of exact locations separated by pipes `|`. In the case of `@skip`, we can see that we're dealing with an executable directive because this directive is only applicable to fields, fragment-spreads, and inline-fragments. ## Usage Let's say we have a GraphQL document and want to exclude details under certain circumstances; it would probably look something like this. GraphQL ``` query me($excludeDetails: Boolean!) { me { id name ...Details @skip(if: $excludeDetails) } } fragment Details on User { mobileNumber phoneNumber } ``` With `@skip`, we've successfully altered the GraphQL's runtime execution behavior. If `$excludeDetails` is set to `true`, the execution engine will exclude the fields `mobileNumber` and `phoneNumber`; the response would look like this. JSON ``` { "data": { "me": { "id": "VXNlcgox", "name": "Henry" } } } ``` Now that we know how to use directives in GraphQL, let's head over to the next section, which is about one crucial aspect of directives. ### Order Matters **The order of directives is significant**, because the execution is in **sequential order**, which means one after the other. If we have something like the following example, we can see how directives can affect each other. GraphQL ``` query me { me { name @skip(if: true) @include(if: true) } } ``` Since we excluded the field `name` in the first place, `@include` does not affect the field `name` anymore. We then just get an empty `me` object in return. JSON ``` { "data": { "me": {} } } ``` Note We will have a deep dive on directives' order under the [Middleware](#order) section. Now that we have a basic understanding of what directives are, how they work, and what we can do with them, let's create a custom directive. ## Custom Directives To create a custom directive we need to define its name, location, and optionally its arguments. We also have to register the directive explicitly. Annotate a C# class with the `[DirectiveType]` attribute. The directive name is inferred from the class name (minus the "Directive" suffix if present). Public properties automatically become directive arguments. C# ``` [DirectiveType(DirectiveLocation.Field)] public class MyDirective { } ``` C# ``` builder.Services .AddGraphQLServer() .AddDirectiveType(); ``` [Learn more about Locations](#locations) We have registered a new directive named `my` without any arguments and limited the usage to fields only. A GraphQL query request with our new directive could look like this. GraphQL ``` query foo { bar @my } ``` As of now, our custom directive provides no functionality. We will handle that part in the [Middleware](#middleware) section. But before that, let's talk about repeatable directives and arguments. ### Repeatable By default, directives are not repeatable, which means directives are unique and can only be applied once at a specific location. For example, if we use the `my` directive twice at the field `bar`, we will encounter a validation error. So the following GraphQL query request results in an error if the directive is not repeatable. GraphQL ``` query foo { bar @my @my } ``` We can enable repeatability like the following. Set `IsRepeatable = true` on the attribute. C# ``` [DirectiveType(DirectiveLocation.Field, IsRepeatable = true)] public class MyDirective { } ``` This configuration will translate into the following SDL. SDL ``` directive @my repeatable on FIELD ``` ### Arguments A directive can provide additional information through arguments. They might also come in handy, in combination with repeatable directives, for reusability purposes. Any public property on the class becomes a directive argument automatically. C# ``` [DirectiveType(DirectiveLocation.FieldDefinition)] public class MyDirective { public string Name { get; set; } } ``` This configuration will translate into the following SDL. SDL ``` directive @my(name: String!) on FIELD ``` ### Usage within Types We could associate the `MyDirectiveType` with an object type like the following. C# ``` public class FooType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor.Name("Foo"); descriptor.Directive("my", new ArgumentNode("name", "bar")); } } ``` Note For this to work the `MyDirectiveType` directive needs to have the appropriate location within the schema. In this example it would be `DirectiveLocation.Object`. Referencing directives using their name is not type-safe and could lead to runtime errors, which are avoidable by using our generic variant of the directive type. Once we have defined our directive using `DirectiveType`, we can pass an instance of the backing POCO (``) instead of the name of the directive and an `ArgumentNode`. C# ``` public class FooType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor.Name("Foo"); descriptor.Directive(new MyDirective { Name = "bar" }); } } ``` Since the directive instance that we have added to our type is now a strong .NET type, we don't have to fear changes to the directive structure or name anymore. ### Directives on Directive Definitions A directive definition is itself a schema element, so it can carry directives. You can use this to mark a directive definition as deprecated or to attach metadata to it, in the same way you annotate object types, fields, or enum values. To apply a directive to a directive definition, that directive must declare the `DIRECTIVE_DEFINITION` location. #### Declaring a Directive That Targets Directive Definitions A directive can only be applied to a directive definition when its own definition includes the `DIRECTIVE_DEFINITION` location. C# ``` [DirectiveType(DirectiveLocation.DirectiveDefinition)] public class OnDirectiveDefinition { } ``` This configuration translates into the following SDL. SDL ``` directive @onDirectiveDefinition on DIRECTIVE_DEFINITION ``` #### Applying a Directive to a Directive Definition Once a directive declares the `DIRECTIVE_DEFINITION` location, you can apply it to another directive definition. In schema-first SDL you place the applied directives after the argument definitions (if any) and before the optional `repeatable` keyword and the `on` keyword. SDL ``` directive @onDirectiveDefinition on DIRECTIVE_DEFINITION directive @custom @onDirectiveDefinition on OBJECT ``` In code-first, call `Directive(...)` on the `IDirectiveTypeDescriptor` to apply a directive to the directive definition you are configuring. C# ``` public class CustomDirectiveType : DirectiveType { protected override void Configure(IDirectiveTypeDescriptor descriptor) { descriptor.Name("custom"); descriptor.Location(DirectiveLocation.Object); descriptor.Directive("onDirectiveDefinition"); } } ``` The descriptor offers the following overloads to apply a directive to the directive definition: `Directive(string name, params ArgumentNode[] arguments)`, `Directive(T instance)`, and `Directive()`. Note Applying a custom directive to a directive definition is done through the descriptor (Code) or through schema-first SDL. #### Deprecating a Directive Definition `@deprecated` is allowed on directive definitions and on their arguments. Use it to signal that a directive (or one of its arguments) should no longer be used. Annotate the directive class with `[Obsolete(...)]` or `[GraphQLDeprecated(...)]`. Both set the deprecation. C# ``` [Obsolete("Use @custom instead.")] [DirectiveType(DirectiveLocation.Object)] public class OldDirective { } ``` C# ``` [GraphQLDeprecated("Use @custom instead.")] [DirectiveType(DirectiveLocation.Object)] public class OldDirective { } ``` In schema-first SDL, apply `@deprecated` to the directive definition or to one of its arguments. SDL ``` directive @old @deprecated(reason: "Use @custom.") on OBJECT directive @custom( legacyArg: Int @deprecated(reason: "Use newArg instead.") newArg: String ) on OBJECT ``` #### Extending a Directive In schema-first you can use `extend directive` to add directives, including a deprecation, to an existing directive definition. The added directives merge into the existing definition. SDL ``` directive @custom on OBJECT extend directive @custom @onDirectiveDefinition extend directive @custom @deprecated(reason: "Use something else.") ``` #### Introspection Introspection exposes this surface, mirroring how deprecated fields, enum values, and arguments behave. - `__Directive` exposes `isDeprecated: Boolean!` and `deprecationReason: String`. - `__Schema.directives(includeDeprecated: Boolean = false)` hides deprecated directives by default. Pass `includeDeprecated: true` to include them. - `__DirectiveLocation` includes `DIRECTIVE_DEFINITION`. GraphQL ``` { __schema { directives(includeDeprecated: true) { name isDeprecated deprecationReason locations } } } ``` #### Troubleshooting **Problem:** The directive definition `@custom` must not reference itself. - **Cause:** A directive is applied to its own definition or to one of its own arguments. Self-reference is not allowed. - **Solution:** Apply a different directive, or remove the self-application. **Problem:** The specified directive `@onObject` is not allowed on the current location `DirectiveDefinition`. - **Cause:** The applied directive's definition does not include the `DIRECTIVE_DEFINITION` location. - **Solution:** Add `DIRECTIVE_DEFINITION` to that directive's locations (`on DIRECTIVE_DEFINITION` in SDL, or `descriptor.Location(DirectiveLocation.DirectiveDefinition)` in code-first). **Problem:** The directive extension `extend directive @unknown` targets an undefined directive. - **Cause:** `extend directive` references a directive that is not defined. - **Solution:** Define the directive before extending it. ### Locations A directive can define one or multiple locations, where it can be applied. Multiple locations are separated by a pipe `|`. C# ``` descriptor.Location(DirectiveLocation.Field | DirectiveLocation.Object); ``` Generally we distinguish between two types of locations: Type system and executable locations. #### Type System Locations Type system locations specify where we can place a specific directive in the schema. The arguments of directives specified in these locations are fixed. We can query such directives through introspection. The following schema shows where type system directives can be applied. SDL ``` directive @schema on SCHEMA directive @object on OBJECT directive @argumentDefinition on ARGUMENT_DEFINITION directive @fieldDefinition on FIELD_DEFINITION directive @inputObject on INPUT_OBJECT directive @inputFieldDefinition on INPUT_FIELD_DEFINITION directive @interface on INTERFACE directive @enum on ENUM directive @enumValue on ENUM_VALUE directive @union on UNION directive @scalar on SCALAR directive @directiveDefinition on DIRECTIVE_DEFINITION directive @custom @directiveDefinition on OBJECT schema @schema { query: Query } type Query @object { search(by: SearchInput! @argumentDefinition): SearchResult @fieldDefinition } input SearchInput @inputObject { searchTerm: String @inputFieldDefinition } interface HasDescription @interface { description: String } type Product implements HasDescription { added: DateTime description: String } enum UserKind @enum { Administrator @enumValue Moderator } type User { name: String userKind: UserKind } union SearchResult @union = Product | User scalar DateTime @scalar ``` The `DIRECTIVE_DEFINITION` location lets a directive be applied to other directive definitions, as with `@directiveDefinition` on the `@custom` definition above. See [Directives on directive definitions](#directives-on-directive-definitions) for the details. #### Executable Locations Executable locations specify where a client can place a specific directive, when executing an operation. Our server defines the following directives. SDL ``` directive @query on QUERY directive @field on FIELD directive @fragmentSpread on FRAGMENT_SPREAD directive @inlineFragment on INLINE_FRAGMENT directive @fragmentDefinition on FRAGMENT_DEFINITION directive @mutation on MUTATION directive @subscription on SUBSCRIPTION ``` The following request document shows where we, as a client, can apply these directives. GraphQL ``` query getUsers @query { search(by: { searchTerm: "Foo" }) @field { ...DescriptionFragment @fragmentSpread ... on User @inlineFragment { userKind } } } fragment DescriptionFragment on HasDescription @fragmentDefinition { description } mutation createNewUser @mutation { createUser(input: { name: "Ada Lovelace" }) { user { name } } } subscription subscribeToUser @subscription { onUserChanged(id: 1) { user { name } } } ``` ### Middleware What makes directives in Hot Chocolate very useful is the ability to associate a middleware with it. A middleware can alternate the result, or even produce the result, of a field. A directive middleware is only added to a field middleware pipeline when the directive was annotated to the object definition, the field definition or the field. Moreover, if the directive is repeatable the middleware will be added multiple times to the middleware allowing to build a real pipeline with it. In order to add a middleware to a directive we could declare it with the descriptor as a delegate. C# ``` public class MyDirectiveType : DirectiveType { protected override void Configure( IDirectiveTypeDescriptor descriptor) { descriptor.Name("my"); descriptor.Location(DirectiveLocation.Object); descriptor.Use((next, directive) => context => { context.Result = "Bar"; return next.Invoke(context); }); } } ``` Directives with middleware or executable directives can be put on object types and on their field definitions or on the field selection in a query. Executable directives on an object type will replace the field resolver of every field of the annotated object type. #### Order In GraphQL the order of directives is significant and with our middleware we use this order to create a resolver pipeline through which the result flows. The resolver pipeline consists of a sequence of directive delegates, called one after the other. Each delegate can perform operations before and after the next delegate. A delegate can also decide to not pass a resolver request to the next delegate, which is called short-circuiting the resolver pipeline. Short-circuiting is often desirable because it avoids unnecessary work. The order of the middleware pipeline is defined by the order of the directives. Since executable directives will flow from the object type to its field definitions, the directives of the type would be called first in the order that they were annotated. SDL ``` type Query { foo: Bar } type Bar @a @b { baz: String @c @d } ``` So, the directives in the above example would be called in the following order `a, b, c, d`. If there were more directives in the query, they would be appended to the directives from the type. GraphQL ``` { foo { baz @e @f } } ``` So, now the order would be like the following: `a, b, c, d, e, f`. Every middleware can execute the original resolver function by calling `ResolveAsync()` on the `IDirectiveContext`. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/directives.md) Maintained by ChilliCream. Last updated on **July 01, 2026** by **Tobias Tengler** --- # Documentation - Hot Chocolate > Add GraphQL schema descriptions in Hot Chocolate with the [GraphQLDescription] attribute or XML documentation comments, visible in tooling and introspection. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/documentation GraphQL descriptions enrich your schema with information that consumers see in developer tools, IDE autocompletion, and introspection results. Every type, field, argument, and enum value can carry a description string. GraphQL ``` "Represents a registered user." type User { "The unique username." username: String! } ``` Hot Chocolate provides two ways to add descriptions: the `[GraphQLDescription]` attribute and XML documentation comments. ## Using GraphQLDescription The `[GraphQLDescription]` attribute sets a description on any schema element. C# ``` [GraphQLDescription("Represents a registered user.")] public class User { [GraphQLDescription("The unique username.")] public string Username { get; set; } } [GraphQLDescription("Available user roles.")] public enum UserRole { [GraphQLDescription("Full system access.")] Administrator, [GraphQLDescription("Content moderation access.")] Moderator } [QueryType] public static partial class UserQueries { [GraphQLDescription("Finds a user by username.")] public static User? GetUser( [GraphQLDescription("The username to search for.")] string username, UserService users) => users.FindByName(username); } ``` ## Using XML Documentation Comments Hot Chocolate generates descriptions from standard C# XML documentation comments. The source generator extracts them at build time, so your C# code is the single source of documentation for both your code and GraphQL schema. C# ``` /// /// Represents a registered user. /// public class User { /// /// The unique username. /// public string Username { get; set; } } /// /// Available user roles. /// public enum UserRole { /// /// Full system access. /// Administrator, /// /// Content moderation access. /// Moderator } [QueryType] public static partial class UserQueries { /// /// Finds a user by username. /// /// The username to search for. public static User? GetUser(string username, UserService users) => users.FindByName(username); } ``` ### Source Generator (Default) Hot Chocolate's source generator extracts XML documentation comments directly from the source code during compilation. This is the default behavior, no additional project configuration is required. The source generator reads ``, ``, ``, and `` tags and embeds the extracted text into the generated type configuration. Because this happens at build time, you do not need to ship XML documentation files with your application. #### Supported Tags | Tag | Usage | | --------------------------------- | ------------------------------------------------------------------ | | | Sets the description of the type, field, or enum value. | | | Sets the description of a field argument. | | | Appended to the field description under a **Returns** heading. | | | Appended under an **Errors** heading. Requires the code attribute. | | | Resolves documentation from a base class or interface. | | | Resolves documentation from a specific member. | #### Example with Returns and Errors C# ``` [QueryType] public static partial class UserQueries { /// /// Finds a user by their unique username. /// /// The username to search for. /// The matching user, or null if not found. /// /// The caller does not have permission to search users. /// public static User? GetUser(string username, UserService users) => users.FindByName(username); } ``` This produces a field description that includes the summary, a **Returns** section, and an **Errors** section. #### Disabling Source Generator Documentation To prevent the source generator from extracting XML documentation, add the `Module` attribute with the `DisableXmlDocumentation` option: C# ``` using HotChocolate; [assembly: Module("MyModule", ModuleOptions.Default | ModuleOptions.DisableXmlDocumentation)] ``` When `DisableXmlDocumentation` is set, `[GraphQLDescription]` attributes continue to work. Only the automatic extraction of XML comments is suppressed. ### Runtime XML Documentation If you are not using the source generator or need XML documentation from referenced assemblies outside the source generator's scope, Hot Chocolate can read XML documentation files at runtime. Enable `GenerateDocumentationFile` in your `.csproj`: XML ``` true $(NoWarn);1591 ``` The `` element is optional. It suppresses compiler warnings for types without documentation comments. #### Disabling Runtime XML Documentation If you do not want runtime XML comments to appear in the schema: C# ``` builder .AddGraphQL() .ModifyOptions(opt => opt.UseXmlDocumentation = false); ``` ## Priority Order When both `[GraphQLDescription]` and XML documentation are present, they follow this priority: 1. **`[GraphQLDescription]` attribute** (implementation-first): Used if the value is non-null and non-empty. If null or empty, XML documentation is used as a fallback. 2. **`Description()` method** (code-first): Always takes precedence, even if null or empty. 3. **XML documentation comments**: Used as a fallback when no explicit description is set. ## Custom Naming Conventions If you use a custom naming convention and runtime XML documentation, pass an `XmlDocumentationProvider` to the convention so descriptions are preserved. This does not apply when using the source generator, which handles documentation extraction at build time. C# ``` public class CustomNamingConventions : DefaultNamingConventions { public CustomNamingConventions( IDocumentationProvider documentationProvider) : base(documentationProvider) { } } ``` C# ``` IReadOnlySchemaOptions capturedSchemaOptions; builder .AddGraphQL() .ModifyOptions(opt => capturedSchemaOptions = opt) .AddConvention(sp => new CustomNamingConventions( new XmlDocumentationProvider( new XmlDocumentationFileResolver( capturedSchemaOptions.ResolveXmlDocumentationFileName), sp.GetApplicationService>() ?? new NoOpStringBuilderPool()))); ``` ## Next Steps - **Need to deprecate fields?** See [Versioning](https://chillicream.com/docs/hotchocolate/defining-a-schema/versioning). - **Need to define enums?** See [Enums](https://chillicream.com/docs/hotchocolate/defining-a-schema/enums). - **Need to define object types?** See [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/documentation.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Dynamic Schemas - Hot Chocolate > Build dynamic GraphQL schemas in Hot Chocolate with `ITypeModule`: provide types at runtime and trigger hot schema reloads when configuration changes. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/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. C# ``` builder .AddGraphQL() .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. C# ``` 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 C# ``` 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 C# ``` 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 C# ``` 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: C# ``` 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: C# ``` 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 C# ``` 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 .AddGraphQL() .AddQueryType() .AddMutationType(mutationType) .AddType(productInputType) .AddType(productType); ``` ## Next Steps - **Need to extend existing types?** See [Extending Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). - **Need to define types with the descriptor API?** See [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). - **Need to understand type modules in depth?** Explore the `ITypeModule` interface in the Hot Chocolate source code under `src/HotChocolate/Core/src/Types/`. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/dynamic-schemas.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Enums - Hot Chocolate > Define GraphQL enum types in Hot Chocolate: C# enums map automatically, and `EnumType` descriptors let you rename values and control value binding. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/enums A GraphQL enum is a special scalar restricted to a fixed set of allowed values. Enums work as both input and output types. In C#, a standard `enum` maps directly to a GraphQL enum type. **GraphQL schema** GraphQL ``` enum UserRole { GUEST STANDARD ADMINISTRATOR } type Query { role: UserRole usersByRole(role: UserRole): [User] } ``` **Client query** GraphQL ``` { usersByRole(role: ADMINISTRATOR) { id } } ``` When an enum appears in a JSON response or in a variables object, it is represented as a string (`"ADMINISTRATOR"`). When used directly in a query argument, it is a literal without quotes (`ADMINISTRATOR`). ## Defining an Enum Type Hot Chocolate picks up any C# `enum` that appears in a resolver's return type or parameters and exposes it as a GraphQL enum. C# ``` public enum UserRole { Guest, Standard, Administrator } [QueryType] public static partial class UserQueries { public static User[] GetUsersByRole(UserRole role) { // ... } } ``` No extra registration is needed. The source generator discovers `UserRole` through the resolver parameter. ## Naming Conventions Hot Chocolate converts C# enum member names to `UPPER_SNAKE_CASE` following the GraphQL convention: | C# member | GraphQL value | | ---------------- | -------------------- | | Guest | GUEST | | HeadOfDepartment | HEAD\_OF\_DEPARTMENT | The enum type name defaults to the C# type name (`UserRole`). ### Overriding Names Use `[GraphQLName]` to set an explicit name on the type or individual values. C# ``` [GraphQLName("Role")] public enum UserRole { [GraphQLName("VISITOR")] Guest, Standard, Administrator } ``` Both approaches produce the following schema: GraphQL ``` enum Role { VISITOR STANDARD ADMINISTRATOR } ``` ## Ignoring Values You can exclude individual enum members from the GraphQL schema. C# ``` public enum UserRole { [GraphQLIgnore] Internal, Guest, Standard, Administrator } ``` ## Binding to Non-Enum Types In code-first, you can bind an enum type to any .NET type, such as `string`. C# ``` public class UserRoleType : EnumType { protected override void Configure(IEnumTypeDescriptor descriptor) { descriptor.Name("UserRole"); descriptor .Value("Default") .Name("STANDARD"); } } ``` This is useful when enum values come from configuration or a database rather than a compile-time C# enum. ## Next Steps - **Need to define output types?** See [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). - **Need nullable or required fields?** See [Non-Null](https://chillicream.com/docs/hotchocolate/defining-a-schema/non-null). - **Need to document enum values?** See [Documentation](https://chillicream.com/docs/hotchocolate/defining-a-schema/documentation). - **Need to deprecate an enum value?** See [Versioning](https://chillicream.com/docs/hotchocolate/defining-a-schema/versioning). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/enums.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # GraphQL Input Object Types in Hot Chocolate > Define GraphQL input object types in Hot Chocolate to pass structured arguments, with records, default values, optional properties, and @oneOf inputs. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types GraphQL input object types let you pass structured data as arguments. While [scalar arguments](https://chillicream.com/docs/hotchocolate/defining-a-schema/arguments) work for simple values, input types let you group related fields into a single object. Input types differ from output object types: their fields cannot have arguments and they use the `input` keyword in the schema. **GraphQL schema** GraphQL ``` input CreateBookInput { title: String! author: String! } type Mutation { createBook(input: CreateBookInput!): Book } ``` ## Defining an Input Type Any C# class or record used as a resolver parameter (that is not a scalar, enum, or service) becomes an input object type in the schema. C# ``` public class CreateBookInput { public string Title { get; set; } public string Author { get; set; } } [MutationType] public static partial class BookMutations { public static async Task CreateBookAsync( CreateBookInput input, CatalogContext db, CancellationToken ct) { var book = new Book { Title = input.Title, Author = input.Author }; db.Books.Add(book); await db.SaveChangesAsync(ct); return book; } } ``` If a class used as an argument does not end in `Input`, Hot Chocolate appends `Input` to the type name in the schema automatically. ## Using Records and Immutable Types Input types can use immutable classes or C# records. Hot Chocolate calls the constructor instead of setting properties. The rules are: 1. Each constructor parameter type must match the corresponding property type. 2. Each constructor parameter name must match the property name (with a lowercase first letter). 3. All properties must have a matching constructor parameter. Hot Chocolate validates input constructors at schema build time, so mismatches are caught early. C# ``` public record CreateBookInput(string Title, string Author); ``` This record is equivalent to a class with a constructor and get-only properties. ## Default Values The `[DefaultValue]` attribute assigns a default value to an input field. When the client omits the field, the default is used. C# ``` public class UserFilterInput { public string? Name { get; set; } [DefaultValue(true)] public bool IsActive { get; set; } } ``` This produces: GraphQL ``` input UserFilterInput { name: String isActive: Boolean! = true } ``` Default values maintain backward compatibility. When you add a new field to an input type, providing a default value keeps existing queries working. ### Default Values with GraphQL Syntax For complex defaults (objects or lists), use `[DefaultValueSyntax]` with GraphQL value literal syntax. C# ``` public class UserProfileInput { public string? Name { get; set; } [DefaultValueSyntax("{ notifications: true, theme: \"light\" }")] public Preferences? Preferences { get; set; } } ``` In code-first, use the `DefaultValueSyntax` method: C# ``` descriptor .Field(f => f.Preferences) .DefaultValueSyntax("{ notifications: true, theme: \"light\" }"); ``` ## Optional Properties Use `Optional` to distinguish between a field that was not provided and a field explicitly set to `null`. This is important for partial updates where you need to know whether the client intended to clear a value. C# ``` public class UpdateBookInput { [DefaultValue("")] public Optional Title { get; set; } public string Author { get; set; } } ``` When using `Optional` on a non-nullable field, you must add `[DefaultValue]` to make the field optional in the schema. Records work too: C# ``` public record UpdateBookInput( [property: DefaultValue("")] Optional Title, string Author); ``` ## OneOf Input Objects A `@oneOf` input type requires that exactly one field is set and non-null. This provides input polymorphism, letting a single argument accept different shapes of data. **GraphQL schema** GraphQL ``` input PetInput @oneOf { cat: CatInput dog: DogInput } ``` C# ``` [OneOf] public class PetInput { public CatInput? Cat { get; set; } public DogInput? Dog { get; set; } } public class CatInput { public string Name { get; set; } public int NumberOfLives { get; set; } } public class DogInput { public string Name { get; set; } public bool WagsTail { get; set; } } [MutationType] public static partial class PetMutations { public static Pet CreatePet(PetInput input) { // ... } } ``` All fields on a `@oneOf` input must be nullable. Hot Chocolate validates at runtime that exactly one field is provided. A `@oneOf` input must also have at least one field that can hold a finite value, that is, a scalar, an enum, a list, or an input object that is itself finite. `input A @oneOf { self: A }` is rejected when the schema is built. ## Next Steps - **Need scalar arguments?** See [Arguments](https://chillicream.com/docs/hotchocolate/defining-a-schema/arguments). - **Need to write mutations?** See [Mutations](https://chillicream.com/docs/hotchocolate/defining-a-schema/mutations). - **Need to understand nullability?** See [Non-Null](https://chillicream.com/docs/hotchocolate/defining-a-schema/non-null). - **Need to document input fields?** See [Documentation](https://chillicream.com/docs/hotchocolate/defining-a-schema/documentation). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/input-object-types.md) Maintained by ChilliCream. Last updated on **September 04, 2026** by **Glen** --- # Interfaces - Hot Chocolate > Define GraphQL interfaces in Hot Chocolate with the [InterfaceType] attribute, sharing fields across object types and mapping them from C# interfaces. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/interfaces A GraphQL interface defines a set of fields that multiple object types share. When a field returns an interface type, the client can query the shared fields directly and use fragments to access type-specific fields. Interfaces are output-only types and cannot be used as arguments or input fields. **GraphQL schema** GraphQL ``` interface Message { author: User! createdAt: DateTime! } type TextMessage implements Message { author: User! createdAt: DateTime! content: String! } type Query { messages: [Message]! } ``` **Client query** GraphQL ``` { messages { createdAt ... on TextMessage { content } } } ``` The shared `createdAt` field is queried directly on the interface. The `content` field, which exists only on `TextMessage`, is accessed through an inline fragment. ## Defining an Interface Type Hot Chocolate maps C# interfaces and abstract classes to GraphQL interface types. C# ``` [InterfaceType("Message")] public interface IMessage { User Author { get; set; } DateTime CreatedAt { get; set; } } public class TextMessage : IMessage { public User Author { get; set; } public DateTime CreatedAt { get; set; } public string Content { get; set; } } [QueryType] public static partial class MessageQueries { public static IMessage[] GetMessages() { // ... } } ``` C# ``` builder .AddGraphQL() .AddType(); ``` You must register each implementing type explicitly so Hot Chocolate knows which object types belong to the interface. You can also use an abstract class instead of an interface: C# ``` [InterfaceType] public abstract class Message { public User Author { get; set; } public DateTime CreatedAt { get; set; } } ``` ## Ignoring Fields You can exclude specific fields from the GraphQL interface. C# ``` [InterfaceType("Message")] public interface IMessage { [GraphQLIgnore] User Author { get; set; } DateTime CreatedAt { get; set; } } ``` ## Overriding Names Use `[GraphQLName]` or the `Name` method to override inferred names. C# ``` [GraphQLName("Post")] public interface IMessage { User Author { get; set; } [GraphQLName("addedAt")] DateTime CreatedAt { get; set; } } ``` You can also specify the name through the `[InterfaceType]` attribute: C# ``` [InterfaceType("Post")] public interface IMessage ``` Both produce the following schema: GraphQL ``` interface Post { author: User! addedAt: DateTime! } ``` ## Interfaces Implementing Interfaces GraphQL interfaces can implement other interfaces, forming a hierarchy. C# ``` [InterfaceType("Message")] public interface IMessage { User Author { get; set; } } [InterfaceType("DatedMessage")] public interface IDatedMessage : IMessage { DateTime CreatedAt { get; set; } } public class TextMessage : IDatedMessage { public User Author { get; set; } public DateTime CreatedAt { get; set; } public string Content { get; set; } } ``` C# ``` builder .AddGraphQL() .AddType() .AddType(); ``` Register intermediate interfaces (like `DatedMessage`) explicitly if they are not returned directly from a resolver field. ## Default Resolvers Interface fields can define default resolvers, similar to default interface methods in C#. When an object type implements the interface, it automatically inherits the resolver for any field where it does not define its own. If the object type does not even declare the field, the field is created automatically with the interface's resolver. This is useful when multiple types share the same resolution logic for a field and you want to define it once on the interface rather than repeating it in every implementing type. C# ``` [InterfaceType("Message")] public interface IMessage { User Author { get; } DateTime CreatedAt { get; } } [InterfaceType] public static partial class MessageNode { public static string DisplayName([Parent] IMessage message) => $"{message.Author.Name} - {message.CreatedAt:d}"; } ``` All object types implementing `IMessage` inherit the `displayName` field and its resolver. No additional configuration is needed on the implementing types. Object types can override an inherited resolver by defining their own resolver for the same field. ## Next Steps - **Need types without shared fields?** See [Unions](https://chillicream.com/docs/hotchocolate/defining-a-schema/unions). - **Need to define output types?** See [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). - **Need to extend an existing type?** See [Extending Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). - **Need to document interface fields?** See [Documentation](https://chillicream.com/docs/hotchocolate/defining-a-schema/documentation). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/interfaces.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Lists - Hot Chocolate > Expose .NET collections as GraphQL list types in Hot Chocolate, covering supported collection types, list nullability, and nested list definitions. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/lists GraphQL lists represent ordered collections of elements. When a resolver returns any .NET collection type, Hot Chocolate exposes it as a list in the schema. **GraphQL schema** GraphQL ``` type Query { users: [User!]! } ``` **Client query** GraphQL ``` { users { id name } } ``` The response contains an ordered array of objects matching the requested fields. ## Supported Collection Types Hot Chocolate recognizes common .NET collection types and maps them to GraphQL lists. | C# return type | GraphQL type (NRT enabled) | | ------------------- | -------------------------- | | List | \[User!\]! | | User\[\] | \[User!\]! | | IEnumerable | \[User!\]! | | IReadOnlyList | \[User!\]! | | IQueryable | \[User!\]! | | List | \[User\]! | | List? | \[User!\] | Any type implementing `IEnumerable` is treated as a list. ## Defining List Fields C# ``` [QueryType] public static partial class UserQueries { public static List GetUsers(CatalogContext db) => db.Users.ToList(); } ``` The return type `List` is automatically mapped to `[User!]!` when NRT is enabled. ## List Nullability Lists have two layers of nullability: the list itself and its items. With [nullable reference types](https://chillicream.com/docs/hotchocolate/defining-a-schema/non-null) enabled, Hot Chocolate infers both layers from your C# types. | C# type | GraphQL type | Meaning | | -------------- | ------------ | ------------------------------------------- | | List | \[String!\]! | Non-null list of non-null items | | List | \[String\]! | Non-null list, items can be null | | List? | \[String!\] | List itself can be null, items are non-null | | List? | \[String\] | Both list and items can be null | If you need to override the inferred nullability, use `[GraphQLType]` or the descriptor API: C# ``` // Override to allow null items [GraphQLType(typeof(ListType))] public List Tags { get; set; } ``` ## Nested Lists Hot Chocolate supports nested lists (lists of lists). This pattern is useful for representing matrix-like data. C# ``` [QueryType] public static partial class GridQueries { public static List> GetMatrix() => [[1, 2], [3, 4]]; } ``` This produces `matrix: [[Int!]!]!` in the schema. ## Next Steps - **Need to control nullability?** See [Non-Null](https://chillicream.com/docs/hotchocolate/defining-a-schema/non-null). - **Need pagination instead of full lists?** See [Pagination](https://chillicream.com/docs/hotchocolate/fetching-data/pagination). - **Need to filter or sort lists?** See [Filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering) and [Sorting](https://chillicream.com/docs/hotchocolate/fetching-data/sorting). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/lists.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Mutations - Hot Chocolate > Define GraphQL mutations in Hot Chocolate with the [MutationType] attribute and enable mutation conventions for inputs, payloads, and typed errors. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/mutations The Mutation type is the entry point for write operations. Unlike query fields, mutation fields are expected to cause side effects: creating, updating, or deleting data. GraphQL executes top-level mutation fields serially, one after another, to guarantee ordering. Child fields of a mutation result are executed in parallel, like any other object type. **GraphQL schema** GraphQL ``` type Mutation { addBook(input: AddBookInput!): AddBookPayload! publishBook(input: PublishBookInput!): PublishBookPayload! } ``` **Client mutation** GraphQL ``` mutation { addBook(input: { title: "C# in depth" }) { book { id title } } } ``` ## Defining a Mutation Type Mark a class with `[MutationType]` and the source generator registers it as part of the Mutation type. Like query types, the class must be `partial`. C# ``` [MutationType] public static partial class BookMutations { public static async Task AddBookAsync( string title, string author, CatalogContext db, CancellationToken ct) { var book = new Book { Title = title, Author = author }; db.Books.Add(book); await db.SaveChangesAsync(ct); return book; } } ``` ## Splitting Across Classes Like query types, you can annotate multiple classes with `[MutationType]`. The source generator merges them into one Mutation type. C# ``` [MutationType] public static partial class BookMutations { public static async Task AddBookAsync( string title, CatalogContext db, CancellationToken ct) { var book = new Book { Title = title }; db.Books.Add(book); await db.SaveChangesAsync(ct); return book; } } ``` C# ``` [MutationType] public static partial class AuthorMutations { public static async Task AddAuthorAsync( string name, CatalogContext db, CancellationToken ct) { var author = new Author { Name = name }; db.Authors.Add(author); await db.SaveChangesAsync(ct); return author; } } ``` ## Mutation Conventions In GraphQL, it is best practice for each mutation to accept a single `input` argument and return a payload object. The payload contains the changed data and any domain errors. This pattern keeps the schema evolvable but requires boilerplate. Hot Chocolate generates the input and payload types for you when mutation conventions are enabled. C# ``` builder .AddGraphQL() .AddMutationConventions(applyToAllMutations: true); ``` With conventions enabled, you write the resolver with plain parameters and a return type. Hot Chocolate wraps them in an input type and payload type automatically. C# ``` [MutationType] public static partial class UserMutations { public static async Task UpdateUserNameAsync( [ID] Guid userId, string username, UserService users, CancellationToken ct) => await users.UpdateNameAsync(userId, username, ct); } ``` This produces the following schema: GraphQL ``` type Mutation { updateUserName(input: UpdateUserNameInput!): UpdateUserNamePayload! } input UpdateUserNameInput { userId: ID! username: String! } type UpdateUserNamePayload { user: User } ``` Services (`UserService`, `CancellationToken`) are not included in the generated input type. Only parameters that map to GraphQL arguments appear in the input. If you prefer to opt in per mutation instead of globally, use `[UseMutationConvention]` on individual methods: C# ``` [UseMutationConvention] public static async Task UpdateUserNameAsync(/* ... */) ``` ### Opting Out To exclude a specific mutation from global conventions: C# ``` [UseMutationConvention(Disable = true)] public static async Task UpdateUserNameAsync(/* ... */) ``` You can also partially opt out by providing your own input or payload type. If your method already accepts a type named `{MutationName}Input` or returns `{MutationName}Payload`, the convention recognizes it and does not generate a replacement. C# ``` public static UpdateUserNamePayload UpdateUserNameAsync(UpdateUserNameInput input) { // Custom payload and input — conventions leave them as-is } ``` ### Customizing Names Override the global naming patterns through `MutationConventionOptions`: C# ``` builder .AddGraphQL() .AddMutationConventions( new MutationConventionOptions { InputArgumentName = "input", InputTypeNamePattern = "{MutationName}Input", PayloadTypeNamePattern = "{MutationName}Payload", PayloadErrorTypeNamePattern = "{MutationName}Error", PayloadErrorsFieldName = "errors", ApplyToAllMutations = true }); ``` Override per mutation with `[UseMutationConvention]`: C# ``` [UseMutationConvention( InputTypeName = "RenameUserInput", PayloadTypeName = "RenameUserPayload")] public static async Task UpdateUserNameAsync(/* ... */) ``` ## Domain Errors Mutation conventions support typed domain errors on the payload. Annotate a mutation with `[Error]` to declare which exceptions represent domain errors. Hot Chocolate catches those exceptions and maps them to error types on the payload. All other exceptions remain runtime errors. C# ``` [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); } ``` This produces a payload with an `errors` field: GraphQL ``` type UpdateUserNamePayload { user: User errors: [UpdateUserNameError!] } interface Error { message: String! } type UserNameTakenError implements Error { message: String! } type InvalidUserNameError implements Error { message: String! } union UpdateUserNameError = UserNameTakenError | InvalidUserNameError ``` Exception class names are rewritten: `UserNameTakenException` becomes `UserNameTakenError` in the schema. ### Controlling Error Shape There are three ways to map an exception to a schema error: **Map the exception directly.** Annotate `[Error(typeof(MyException))]`. The exception's `Message` property becomes the error message. This is the quickest approach. **Map with a factory method.** Create an error class with a `public static CreateErrorFrom(MyException ex)` method. This lets you control the error shape and hide internal details. C# ``` public class UserNameTakenError { private UserNameTakenError(string message) => Message = message; public string Message { get; } public static UserNameTakenError CreateErrorFrom(UserNameTakenException ex) => new($"The username {ex.Username} is already taken."); } ``` **Map with a constructor.** Give the error class a constructor that accepts the exception. C# ``` public class UserNameTakenError { public UserNameTakenError(UserNameTakenException ex) => Message = $"The username {ex.Username} is already taken."; public string Message { get; } } ``` Factory methods can also be instance methods implementing `IPayloadErrorFactory`, which supports dependency injection. Errors and error factories can be shared across multiple mutations. You can use `AggregateException` to return multiple errors at once. ### Custom Error Interface The default error interface requires a `message` field. To add an error code or other fields, define your own interface: C# ``` [GraphQLName("UserError")] public interface IUserError { string Message { get; } string Code { get; } } ``` C# ``` builder .AddGraphQL() .AddErrorInterfaceType(); ``` All error types must declare the fields required by the interface. They do not need to implement the C# interface, but they must have matching properties. ## Next Steps - **Need to read data?** See [Queries](https://chillicream.com/docs/hotchocolate/defining-a-schema/queries). - **Need real-time updates?** See [Subscriptions](https://chillicream.com/docs/hotchocolate/defining-a-schema/subscriptions). - **Need to understand input types?** See [Input Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types). - **Need to fetch data efficiently?** See [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/mutations.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Non-Null - Hot Chocolate > How Hot Chocolate maps C# nullable reference types to the GraphQL non-null modifier, with explicit overrides like [GraphQLNonNullType] and non-null list items. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/non-null By default, every GraphQL field can return either its declared type or `null`. The non-null modifier (`!`) tells clients that a field will never be `null`. If a resolver returns `null` for a non-null field, the execution engine raises an error rather than sending unexpected null values to clients. GraphQL ``` type User { name: String! bio: String } ``` In this schema, `name` always has a value. The `bio` field may be `null`. ## Implicit Nullability from C# Types Hot Chocolate infers nullability from your C# types. When [nullable reference types](https://docs.microsoft.com/dotnet/csharp/nullable-references) (NRT) are enabled in your project, the mapping is straightforward. ### Value Types Value types are non-null by default. Use `?` to make them nullable. | C# type | GraphQL type | | ------- | ------------ | | int | Int! | | int? | Int | | bool | Boolean! | | bool? | Boolean | ### Reference Types (NRT Enabled) With NRT enabled (recommended), non-nullable references map to non-null GraphQL types. | C# type | GraphQL type | | ------- | ------------ | | string | String! | | string? | String | | User | User! | | User? | User | ### Reference Types (NRT Disabled) Without NRT, all reference types are nullable by default. Hot Chocolate cannot distinguish `string` from `string?` because the compiler treats them identically. | C# type | GraphQL type | | ------- | ------------ | | string | String | | User | User | We strongly recommend enabling NRT. It provides accurate schema nullability without extra attributes and catches null-related bugs at compile time. ## Enabling Nullable Reference Types Add the following to your `.csproj` file to enable NRT across the project: XML ``` enable ``` You can also enable it per file with `#nullable enable` at the top of the file. ## Explicit Nullability When you need to override the inferred nullability, use attributes or the descriptor API. C# ``` public class Book { [GraphQLNonNullType] public string Title { get; set; } public string? Author { get; set; } } ``` `[GraphQLNonNullType]` forces the field to be non-null in the schema regardless of the C# nullability. ## Non-Null List Items Lists have two layers of nullability: the list itself and its items. With NRT enabled: | C# type | GraphQL type | | -------------- | ------------ | | List | \[String!\]! | | List? | \[String!\] | | List | \[String\]! | | List? | \[String\] | To override nullability on list items explicitly: C# ``` public class Book { [GraphQLType(typeof(ListType>))] public List Genres { get; set; } } ``` Both produce `genres: [String!]` in the schema. ## Next Steps - **Need to define lists?** See [Lists](https://chillicream.com/docs/hotchocolate/defining-a-schema/lists). - **Need to understand arguments?** See [Arguments](https://chillicream.com/docs/hotchocolate/defining-a-schema/arguments). - **Need input types?** See [Input Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types). - **Need to learn about scalars?** See [Scalars](https://chillicream.com/docs/hotchocolate/defining-a-schema/scalars). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/non-null.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # GraphQL Object Types in Hot Chocolate > Define GraphQL object types in Hot Chocolate with the [ObjectType] attribute, adding fields, resolvers, and type extensions to build your schema graph. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types Object types are the building blocks of a GraphQL schema. Each object type has a name and a set of fields. Fields can return scalars like `String` and `Int`, or other object types, forming a graph that clients traverse through their queries. GraphQL ``` type Product { name: String! price: Decimal! inStock: Boolean! } type Author { name: String! bio: String books: [Book!]! } type Book { title: String! author: Author! } ``` Every field in a query resolves to a concrete value. Object types define the shape of that value. Understanding how to define and configure them is the foundation of building a Hot Chocolate schema. ## Defining Object Types In the implementation-first approach, a C# class becomes a GraphQL object type automatically. The source generator picks up public properties and methods and maps them to fields. In the code-first approach, you create a class that inherits from `ObjectType` and configure it explicitly. C# ``` public class Author { public string Name { get; set; } public string? Bio { get; set; } } ``` Public properties become fields on the `Author` object type. No additional registration or configuration is required beyond the standard `AddTypes` call generated by the source generator. ## Properties as Fields Public properties with a getter are automatically mapped to GraphQL fields. Hot Chocolate converts the property name to camelCase for the schema. C# ``` public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public bool InStock { get; set; } } ``` This produces the following schema: GraphQL ``` type Product { id: Int! name: String! price: Decimal! inStock: Boolean! } ``` ## Methods as Resolvers Public methods on a class become resolver fields. This is how you add computed fields or fields that fetch data from external sources. Method parameters that are registered services are injected automatically. You can define resolver methods directly on your C# models, as shown below: C# ``` public class Author { public string Name { get; set; } public async Task> GetBooksAsync( BookService bookService, CancellationToken ct) => await bookService.GetBooksByAuthorAsync(Name, ct); } ``` Here, the `BookService` parameter is injected from the dependency injection container, and `CancellationToken` is provided by the execution engine. Neither appears as a GraphQL argument. However, a cleaner approach is to separate GraphQL-specific resolvers from your domain models by using a dedicated resolver class. This keeps your domain types free of API concerns and makes them more reusable: C# ``` public class Author { public string Name { get; set; } } [ObjectType] public static partial class AuthorNode { public static async Task> GetBooksAsync( [Parent] Author author, BookService bookService, CancellationToken ct) => await bookService.GetBooksByAuthorAsync(author.Name, ct); } ``` The `[ObjectType]` attribute tells the source generator that this class contributes fields to the `Author` type. The `[Parent]` parameter receives the resolved `Author` instance. The class must be `static partial` so the source generator can wire it up. Both approaches produce this schema: GraphQL ``` type Author { name: String! books: [Book!]! } ``` The naming rules for methods are the same as for [query fields](https://chillicream.com/docs/hotchocolate/defining-a-schema/queries#naming-conventions): `Get` prefixes and `Async` suffixes are stripped, and the result is camelCased. ## Field Configuration You can rename fields, ignore them, and add descriptions without changing the shape of your C# classes. ### Renaming Fields Use `[GraphQLName]` on a property to change its name in the schema: C# ``` public class Author { [GraphQLName("fullName")] public string Name { get; set; } } ``` When using a separate resolver class, the method name determines the field name. The `Get` prefix is stripped and the result is camelCased, so `GetFullName` becomes `fullName`: C# ``` public class Author { public string Name { get; set; } } [ObjectType] public static partial class AuthorNode { public static string GetFullName([Parent] Author author) => author.Name; } ``` If the naming convention does not produce the name you want, apply `[GraphQLName]` to the resolver method: C# ``` public class Author { public string Name { get; set; } } [ObjectType] public static partial class AuthorNode { [GraphQLName("fullName")] public static string GetFullName([Parent] Author author) => author.Name; } ``` You can also rename the type itself. Use `[GraphQLName]` on the domain class: C# ``` [GraphQLName("BookAuthor")] public class Author { public string Name { get; set; } } ``` When using a separate resolver class, apply `[GraphQLName]` to the resolver class instead: C# ``` public class Author { public string Name { get; set; } } [ObjectType] [GraphQLName("BookAuthor")] public static partial class AuthorNode { } ``` If only one client needs different names, prefer using [aliases](https://graphql.org/learn/queries/#aliases) in that client's queries instead of changing the schema. ### Ignoring Fields Use the `[GraphQLIgnore]` attribute to prevent a property or method from appearing in the schema: C# ``` public class Product { public string Name { get; set; } [GraphQLIgnore] public string InternalSku { get; set; } } ``` For resolver types, you can use the internal fluent API to ignore a field: C# ``` public class Product { public string Name { get; set; } public string InternalSku { get; set; } } [ObjectType] public static partial class ProductNode { static partial void Configure(IObjectTypeDescriptor descriptor) => descriptor.Ignore(t => t.InternalSku); } ``` Often, you’ll ignore a field because you want to introduce a resolver that replaces a model property. This can be done with the `[BindMember]` attribute: C# ``` public class Product { public string Name { get; set; } public int BrandId { get; set; } } [ObjectType] public static partial class ProductNode { [BindMember(nameof(Product.BrandId))] public static async Task GetBrandAsync( [Parent] Product product, BrandService brandService, CancellationToken cancellationToken) => await brandService.GetBrandByIdAsync(product.BrandId, cancellationToken); } ``` You can also bind multiple members from your model to a single resolver. ### Descriptions Descriptions appear in GraphQL introspection and tooling like Nitro. They help consumers of your API understand the purpose of each type and field. Use `[GraphQLDescription]` on a class or property: C# ``` [GraphQLDescription("A product in the catalog.")] public class Product { [GraphQLDescription("The display name shown to customers.")] public string Name { get; set; } public decimal Price { get; set; } } ``` You can also use XML documentation comments. Hot Chocolate reads `` tags when `UseXmlDocumentation` is enabled (it is enabled by default). C# ``` /// /// A product in the catalog. /// public class Product { /// /// The display name shown to customers. /// public string Name { get; set; } public decimal Price { get; set; } } ``` When using a separate resolver class, apply `[GraphQLDescription]` to the resolver class or its methods. XML documentation comments work the same way: C# ``` public class Product { public string Name { get; set; } public decimal Price { get; set; } } [ObjectType] [GraphQLDescription("A product in the catalog.")] public static partial class ProductNode { [GraphQLDescription("The display name shown to customers.")] public static string GetName([Parent] Product product) => product.Name; } ``` ### Explicit Binding By default, all public properties and methods are included as fields. You can switch to explicit binding, where you opt in to each field individually. C# ``` public class ProductType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor.BindFieldsExplicitly(); descriptor.Field(f => f.Name); descriptor.Field(f => f.Price); } } ``` Only `name` and `price` appear in the schema. All other properties on `Product` are excluded. You can also set this globally, which affects all types. C# ``` builder .AddGraphQL() .ModifyOptions(options => { options.DefaultBindingBehavior = BindingBehavior.Explicit; }); ``` ## Nullability Hot Chocolate uses C# nullability to determine whether a GraphQL field is nullable or non-null. When [nullable reference types](https://learn.microsoft.com/dotnet/csharp/nullable-references) are enabled in your project, the mapping is straightforward. | C# Type | GraphQL Type | | ------------- | ------------ | | string | String! | | string? | String | | int | Int! | | int? | Int | | List | \[String!\]! | | List | \[String\]! | | List? | \[String!\] | Value types (`int`, `bool`, `decimal`) are non-null by default. Their nullable counterpart (`int?`, `bool?`) maps to a nullable GraphQL field. Reference types follow your project's nullable reference type settings. With nullable reference types enabled (recommended), `string` maps to `String!` and `string?` maps to `String`. Without nullable reference types enabled, all reference type fields are nullable by default. You can override the inferred nullability when needed. C# ``` public class Product { [GraphQLNonNullType] public string? Name { get; set; } } ``` For full details on nullability, see [Non-Null](https://chillicream.com/docs/hotchocolate/defining-a-schema/non-null). ## Dictionary Support Hot Chocolate automatically maps `Dictionary` properties to a list of key-value pair objects. This eliminates the need for custom resolvers when exposing dictionary data. C# ``` public class Product { public string Name { get; set; } public Dictionary Attributes { get; set; } } ``` This produces the following schema: GraphQL ``` type Product { name: String! attributes: [KeyValuePairOfStringAndString!]! } type KeyValuePairOfStringAndString { key: String! value: String! } ``` Clients query dictionary fields like any other list. GraphQL ``` { product { name attributes { key value } } } ``` This works with any key and value types. For example, `Dictionary` produces `KeyValuePairOfStringAndInt32` with the appropriate scalar types. ## Next Steps - **Need to define query entry points?** See [Queries](https://chillicream.com/docs/hotchocolate/defining-a-schema/queries). - **Need to understand resolver patterns?** See [Resolvers](https://chillicream.com/docs/hotchocolate/resolvers). - **Need to compose types from multiple classes?** See [Extending Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). - **Need to define input for mutations?** See [Input Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types). - **Need to fetch data efficiently?** See [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/object-types.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Queries - Hot Chocolate > Define the GraphQL Query type in Hot Chocolate with the [QueryType] attribute, splitting root fields across classes and writing side-effect-free resolvers. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/queries The Query type is the entry point for reading data from a GraphQL server. It is the only required root type. Every public method or property on a query class becomes a field that clients can request. Query fields are expected to be side-effect-free, which allows the execution engine to run them in parallel. **GraphQL schema** GraphQL ``` type Query { books: [Book!]! author(id: Int!): Author } ``` **Client query** GraphQL ``` query { books { title } author(id: 1) { name } } ``` Both `books` and `author` run concurrently. The execution engine is free to parallelize and reorder query fields because they have no side effects. ## Defining a Query Type Mark a class with `[QueryType]` and the source generator registers it as part of the Query type. The class must be `partial` so the source generator can add code at build time. C# ``` [QueryType] public static partial class BookQueries { public static Book GetBook() => new Book { Title = "C# in depth", Author = "Jon Skeet" }; } ``` The source generator creates a `book` field on the Query type. No additional registration is needed beyond the `AddTypes` call generated by the source generator. ## Splitting the Query Type Across Classes GraphQL allows only one Query type per schema, but your codebase does not have to define all query fields in a single class. With the source generator, you can annotate multiple classes with `[QueryType]`. The source generator merges them into one Query type. C# ``` [QueryType] public static partial class ProductQueries { public static async Task GetProductByIdAsync( int id, CatalogContext db, CancellationToken ct) => await db.Products.FindAsync([id], ct); } ``` C# ``` [QueryType] public static partial class AuthorQueries { public static async Task GetAuthorByIdAsync( int id, CatalogContext db, CancellationToken ct) => await db.Authors.FindAsync([id], ct); } ``` This produces a schema with both fields on the Query type: GraphQL ``` type Query { productById(id: Int!): Product authorById(id: Int!): Author } ``` Group your query classes by domain area. This keeps each file focused and makes it clear which team or module owns each part of the API. These query classes can also be split across multiple assemblies, as long as each assembly uses the Hot Chocolate source generator. ## Static vs Instance Classes A `[QueryType]` class can be either static or non-static. **Static classes** are the recommended default. They have no instance state, which makes resolvers predictable and testable. C# ``` [QueryType] public static partial class ProductQueries { public static Product? GetProductById(int id, CatalogContext db) => db.Products.Find(id); } ``` **Non-static classes** are registered as singletons on the service collection by the source generator. Use this when you need constructor-injected dependencies, though injecting services directly into resolver method parameters is preferred in most cases. C# ``` [QueryType] public partial class ProductQueries { public Product? GetProductById(int id, CatalogContext db) => db.Products.Find(id); } ``` ## Naming Conventions Hot Chocolate converts C# method names to GraphQL field names using these rules: | C# Method | GraphQL Field | Rule | | ------------------ | ------------- | ------------------------------------ | | GetBook() | book | Get prefix stripped, camelCased | | GetBookByIdAsync() | bookById | Get prefix and Async suffix stripped | | Books() | books | camelCased as-is | You can override the generated name with `[GraphQLName]`: C# ``` [GraphQLName("allBooks")] public static List GetBooks() => /* ... */; ``` ## Next Steps - **Need to write data?** See [Mutations](https://chillicream.com/docs/hotchocolate/defining-a-schema/mutations). - **Need real-time updates?** See [Subscriptions](https://chillicream.com/docs/hotchocolate/defining-a-schema/subscriptions). - **Need to understand how types map to the schema?** See [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). - **Need to fetch data efficiently?** See [Resolvers](https://chillicream.com/docs/hotchocolate/resolvers) and [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/queries.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Relay - Hot Chocolate > Implement the Relay server specification in Hot Chocolate: global object identification with [Node] and [ID], node refetching, and mutation payload query fields. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/relay The Relay GraphQL Server Specification defines patterns for globally unique identifiers, object refetching, and cursor-based pagination. While these patterns originated in Facebook's Relay client, they improve schema design for any GraphQL client. Note The patterns on this page benefit all GraphQL clients, not only Relay. We recommend them for every Hot Chocolate project. ## Global Identifiers GraphQL clients often use the `id` field to build a client-side cache. If two different types both have a row with `id: 1`, the cache encounters collisions. Global identifiers solve this by encoding the type name and the underlying ID into an opaque, Base64-encoded string that is unique across the entire schema. Hot Chocolate handles this through a middleware. The `[ID]` attribute opts a field into global identifier behavior. At runtime, Hot Chocolate combines the type name with the raw ID to produce a globally unique value. Your business code continues to work with the original ID. ### Output Fields C# ``` public class Product { [ID] public int Id { get; set; } public string Name { get; set; } } ``` The `[ID]` attribute rewrites the field type to `ID!` and serializes the value as a global identifier. By default, it uses the owning type name (`Product`) for serialization. For foreign key fields that reference another type, specify the target type name: C# ``` public class OrderItem { [ID] public int Id { get; set; } [ID] public int ProductId { get; set; } } ``` The generic `[ID]` form infers the GraphQL type name from the type argument. You can also use `[ID("Product")]` to specify it as a string. ### Input Arguments When a field returns a serialized global ID, any argument that accepts that ID must also be marked with `[ID]` to deserialize it back to the raw value. C# ``` [QueryType] public static partial class ProductQueries { public static Product? GetProduct( [ID] int id, CatalogContext db) => db.Products.Find(id); } ``` To restrict the argument to IDs serialized for a specific type: C# ``` public static Product? GetProduct( [ID] int id, CatalogContext db) => db.Products.Find(id); ``` This rejects IDs that were serialized for a different type. ### Input Object Fields Mark input object properties with `[ID]` to deserialize global IDs in input types. C# ``` public class UpdateProductInput { [ID] public int ProductId { get; set; } public string Name { get; set; } } ``` ### ID Serializer You can access the `IIdSerializer` service directly to serialize or deserialize global IDs in custom code. C# ``` [QueryType] public static partial class ProductQueries { public static string GetGlobalId(int productId, IIdSerializer serializer) { return serializer.Serialize(null, "Product", productId); } } ``` The `Serialize` method takes the schema name (or `null` for the default schema), the type name, and the raw ID. ## Global Object Identification Global object identification extends global identifiers by enabling clients to refetch any object by its ID through a standardized `node` query field. This requires three things: 1. The type implements the `Node` interface. 2. The type has an `id: ID!` field. 3. A node resolver method can fetch the object by its ID. ### Enabling Global Object Identification C# ``` builder .AddGraphQL() .AddGlobalObjectIdentification(); ``` This adds the `Node` interface and the `node` / `nodes` query fields: GraphQL ``` interface Node { id: ID! } type Query { node(id: ID!): Node nodes(ids: [ID!]!): [Node]! } ``` You can configure options when enabling global object identification: C# ``` builder .AddGraphQL() .AddGlobalObjectIdentification(opts => { opts.MaxAllowedNodeBatchSize = 50; }); ``` At least one type in the schema must implement `Node`, or the schema fails to build. ### Implementing Node Annotate your class with `[Node]`. Hot Chocolate looks for a static method named `Get`, `GetAsync`, `Get{TypeName}`, or `Get{TypeName}Async` that accepts the ID as its first parameter and returns the type. C# ``` [Node] public class Product { public int Id { get; set; } public string Name { get; set; } public static async Task GetAsync( int id, CatalogContext db, CancellationToken ct) => await db.Products.FindAsync([id], ct); } ``` The `[Node]` attribute causes the type to implement the `Node` interface and turns the `Id` property into a global identifier. If your ID property is not named `Id`, specify it: C# ``` [Node(IdField = nameof(ProductId))] public class Product { public int ProductId { get; set; } // ... } ``` If your resolver method does not follow the naming convention, annotate it with `[NodeResolver]`: C# ``` [NodeResolver] public static async Task FetchByIdAsync(int id, CatalogContext db, CancellationToken ct) => await db.Products.FindAsync([id], ct); ``` To place the node resolver in a separate class: C# ``` [Node( NodeResolverType = typeof(ProductNodeResolver), NodeResolver = nameof(ProductNodeResolver.GetProductAsync))] public class Product { public int Id { get; set; } } public class ProductNodeResolver { public static async Task GetProductAsync( int id, CatalogContext db, CancellationToken ct) => await db.Products.FindAsync([id], ct); } ``` Node resolvers are ideal places to use [DataLoaders](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader) for efficient batched fetching. ### Node with Type Extensions When adding Node support through a type extension, place the `[Node]` attribute on the extension class: C# ``` [Node] [ExtendObjectType] public static partial class ProductExtensions { public static async Task GetAsync( int id, CatalogContext db, CancellationToken ct) => await db.Products.FindAsync([id], ct); } ``` ## Complex IDs Some data models use composite keys (multiple fields forming a unique identifier). Hot Chocolate supports complex IDs through custom ID types and type converters. C# ``` public readonly record struct ProductId(string Sku, int BatchNumber) { public override string ToString() => $"{Sku}:{BatchNumber}"; public static ProductId Parse(string value) { var parts = value.Split(':'); return new ProductId(parts[0], int.Parse(parts[1])); } } public class Product { [ID] public ProductId Id { get; set; } } ``` Register type converters so Hot Chocolate can serialize and deserialize the complex ID: C# ``` builder .AddGraphQL() .AddTypeConverter(ProductId.Parse) .AddTypeConverter(x => x.ToString()) .AddGlobalObjectIdentification(); ``` The source generator can produce a `NodeIdValueSerializer` for your custom ID type, reducing the need for manual converter registration. ## Query Field in Mutation Payloads Mutation payloads can include a `query` field that gives clients access to the full Query type. This lets a client fetch everything it needs to update its state in a single round trip. C# ``` builder .AddGraphQL() .AddQueryFieldToMutationPayloads(); ``` By default, a `query: Query` field is added to every mutation payload type whose name ends in `Payload`. You can customize this: C# ``` builder .AddGraphQL() .AddQueryFieldToMutationPayloads(options => { options.QueryFieldName = "rootQuery"; options.MutationPayloadPredicate = (type) => type.Name.Value.EndsWith("Result"); }); ``` ## Next Steps - **Need to fetch data efficiently?** See [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader). - **Need pagination?** See [Pagination](https://chillicream.com/docs/hotchocolate/fetching-data/pagination). - **Need to understand ID types?** See [Scalars](https://chillicream.com/docs/hotchocolate/defining-a-schema/scalars). - **Need to extend types?** See [Extending Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/relay.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # GraphQL Scalars in Hot Chocolate for .NET > Reference for GraphQL scalars in Hot Chocolate: built-in types like ID and DateTime, the NodaTime package, binding behavior, and custom scalar definitions. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/scalars Scalars are the leaf types in a GraphQL schema. They represent concrete values like strings, numbers, and dates. Unlike object types, scalars cannot be decomposed further. They are where the query ends and actual data is returned. Every scalar defines how values convert between the GraphQL wire format (JSON) and the .NET runtime representation. The GraphQL specification specifies five core scalars (`String`, `Int`, `Float`, `Boolean`, and `ID`), which form the foundation of every GraphQL server. Hot Chocolate comes with many more scalars than the GraphQL core scalars, mapping to common .NET primitive types and structs. | .NET Type | GraphQL Scalar | Binding | Notes | Spec | | ----------------- | -------------- | -------- | --------------------------------------------------------- | -------------------------------------------------------------------- | | string | String | Implicit | UTF-8 character sequence | [Spec](https://spec.graphql.org/draft/#sec-String) | | bool | Boolean | Implicit | true or false | [Spec](https://spec.graphql.org/draft/#sec-Boolean) | | int | Int | Implicit | Signed 32-bit integer | [Spec](https://spec.graphql.org/draft/#sec-Int) | | float, double | Float | Implicit | IEEE 754 double-precision | [Spec](https://spec.graphql.org/draft/#sec-Float) | | string, int, Guid | ID | Explicit | Unique identifier, always serialized as string | [Spec](https://spec.graphql.org/draft/#sec-ID) | | decimal | Decimal | Implicit | High-precision decimal (separate from Float) | [Spec](https://scalars.graphql.org/chillicream/decimal.html) | | long | Long | Implicit | Signed 64-bit integer | [Spec](https://scalars.graphql.org/chillicream/long.html) | | short | Short | Implicit | Signed 16-bit integer | [Spec](https://scalars.graphql.org/chillicream/short.html) | | DateTime | DateTime | Implicit | Date and time with time zone offset | [Spec](https://scalars.graphql.org/chillicream/date-time.html) | | DateTimeOffset | DateTime | Implicit | Date and time with time zone offset | [Spec](https://scalars.graphql.org/chillicream/date-time.html) | | DateOnly | LocalDate | Implicit | Date without time or time zone | [Spec](https://scalars.graphql.org/chillicream/local-date.html) | | DateOnly | Date | Explicit | Date in UTC | [Spec](https://scalars.graphql.org/chillicream/date.html) | | DateTime | LocalDateTime | Explicit | Date and time without time zone | [Spec](https://scalars.graphql.org/chillicream/local-date-time.html) | | TimeOnly | LocalTime | Implicit | Time of day without date or time zone | [Spec](https://scalars.graphql.org/chillicream/local-time.html) | | TimeSpan | Duration | Implicit | Duration of time | [Spec](https://scalars.graphql.org/chillicream/duration.html) | | Guid | UUID | Implicit | Universally unique identifier (RFC 9562) | [Spec](https://scalars.graphql.org/chillicream/uuid.html) | | Uri | URI | Implicit | Uniform resource identifier (replaces URL for System.Uri) | [Spec](https://scalars.graphql.org/chillicream/uri.html) | | Uri | URL | Explicit | Deprecated, use URI instead | [Spec](https://scalars.graphql.org/chillicream/url.html) | | byte\[\] | Base64String | Implicit | Base64-encoded byte array (replaces deprecated ByteArray) | [Spec](https://scalars.graphql.org/chillicream/base64-string.html) | | byte | UnsignedByte | Implicit | Unsigned 8-bit integer | [Spec](https://scalars.graphql.org/chillicream/unsigned-byte.html) | | sbyte | Byte | Implicit | Signed 8-bit integer | [Spec](https://scalars.graphql.org/chillicream/byte.html) | | ushort | UnsignedShort | Implicit | Unsigned 16-bit integer | [Spec](https://scalars.graphql.org/chillicream/unsigned-short.html) | | uint | UnsignedInt | Implicit | Unsigned 32-bit integer | [Spec](https://scalars.graphql.org/chillicream/unsigned-int.html) | | ulong | UnsignedLong | Implicit | Unsigned 64-bit integer | [Spec](https://scalars.graphql.org/chillicream/unsigned-long.html) | | JsonElement | Any | Implicit | Any valid GraphQL value | [Spec](https://scalars.graphql.org/chillicream/any.html) | Note Hot Chocolate only exposes scalars that your schema uses. Unused scalars do not appear in the generated schema. ### ID The GraphQL `ID` scalar is not automatically mapped to a .NET type because it is a semantic type representing a unique identifier. You must annotate fields explicitly to use `ID`. `ID` values are always serialized as strings in responses, but clients can provide `int` or `string` values as variables or GraphQL literals. On the server side, you can use `string`, `int`, or `Guid` as the runtime type for `ID` fields. C# ``` public sealed class Product { [ID] public int Id { get; set; } } [QueryType] public static partial class ProductQueries { public static Product GetProduct([ID] int id) { // Omitted code for brevity } } ``` ### DateTime Scalars You can use `HotChocolate.Types.DateTimeOptions` to configure the built-in BCL-backed `DateTime`, `LocalDateTime`, and `LocalTime` scalars. With these options, you can: - Set how many fractional second digits are accepted during parsing (`InputPrecision`, up to 9) - Control how many fractional second digits are written during serialization (`OutputPrecision`, up to 7) - Require input to match the expected scalar format before parsing (`ValidateInputFormat`) - Always emit fractional seconds in serialized output (when `OutputPrecision > 0`), padded with trailing zeros up to `OutputPrecision` (`AlwaysOutputFractionalSeconds`) Although the built-in scalars can parse up to 9 fractional second digits, the underlying BCL types only preserve up to 7 digits (100-nanosecond precision), so additional digits are rounded during parsing. By default, trailing zeros are stripped from the fractional component and the fractional component is omitted entirely when zero (for example, `2023-12-24T15:30:00.5000000Z` is emitted as `2023-12-24T15:30:00.5Z`). Set `AlwaysOutputFractionalSeconds = true` to keep a fixed-width representation regardless of value. The option has no effect when `OutputPrecision` is `0`, since there are no fractional second digits to emit. To customize the built-in scalars, register configured scalar instances explicitly: C# ``` builder .AddGraphQL() .AddType(new DateTimeType(new DateTimeOptions { OutputPrecision = 3, AlwaysOutputFractionalSeconds = true })) .AddType(new LocalDateTimeType(new DateTimeOptions { OutputPrecision = 3, AlwaysOutputFractionalSeconds = true })) .AddType(new LocalTimeType(new DateTimeOptions { OutputPrecision = 3, AlwaysOutputFractionalSeconds = true })); ``` ### UUID Format The `UUID` scalar supports multiple serialization formats: | Specifier | Format | | ----------- | -------------------------------------------------------------------- | | N | 00000000000000000000000000000000 | | D (default) | 00000000-0000-0000-0000-000000000000 | | B | {00000000-0000-0000-0000-000000000000} | | P | (00000000-0000-0000-0000-000000000000) | | X | {0x00000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}} | The `UuidType` always returns values in the specified format. When parsing input, it tries the specified format first, then falls back to other formats. To change the default format: C# ``` builder .AddGraphQL() .AddType(new UuidType('N')); ``` ### Any Scalar The `Any` scalar is comparable to `object` in C#. It accepts any literal and can return any output type. SDL ``` type Query { metadata(filter: Any): Any } ``` All of the following queries are valid against an `Any` argument: GraphQL ``` { a: metadata(filter: 1) b: metadata(filter: [1, 2, 3]) c: metadata(filter: "text") d: metadata(filter: true) e: metadata(filter: { key: "value", nested: { count: 1 } }) } ``` #### Runtime type The `Any` scalar uses `System.Text.Json.JsonElement` as its .NET runtime type. Fields annotated with `Any` expect resolvers to return a `JsonElement`. To access an argument dynamically: C# ``` JsonElement value = context.ArgumentValue("filter"); if (value.ValueKind == JsonValueKind.Object) { string? name = value.GetProperty("name").GetString(); } ``` To deserialize into a strongly typed model: C# ``` MyFilter filter = context.ArgumentValue("filter"); ``` You can also inspect the value kind to determine how the argument was provided: C# ``` ValueKind kind = context.ArgumentKind("filter"); ``` The `ValueKind` enum tells you which kind of literal represents the argument: C# ``` public enum ValueKind { String, Integer, Float, Boolean, Enum, Object, Null } ``` > An integer literal can contain a long value, and a float literal can be a decimal or a float. #### Returning dictionaries and arbitrary .NET types By default, `Any` expects a `JsonElement`. To return common .NET types such as `Dictionary` or `ExpandoObject`, register the JSON type converter: C# ``` builder .AddGraphQL() .AddJsonTypeConverter(); ``` With the converter registered, resolvers can return dictionaries or any JSON-serializable object: C# ``` [GraphQLType] public object GetData() => new Dictionary { { "name", "John" }, { "age", 30 } }; ``` #### Custom type serialization For custom reference types, register a dedicated converter to control serialization. For example, to serialize `TimeZoneInfo` as its string ID instead of a full JSON object: C# ``` builder .AddGraphQL() .AddTypeConverter( value => JsonSerializer.SerializeToElement(value.Id)); ``` The resolver can then return the type directly: C# ``` [GraphQLType] public TimeZoneInfo GetTimezone() => TimeZoneInfo.Utc; // serializes as "UTC" ``` ## Additional Scalars Package For more specific use cases, install the `HotChocolate.Types.Scalars` package: Bash ``` dotnet add package HotChocolate.Types.Scalars ``` Warning All `HotChocolate.*` packages need to have the same version. | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | EmailAddress | Email address as defined in [RFC 5322](https://tools.ietf.org/html/rfc5322) | | HexColor | HEX color code | | Hsl | CSS HSL color as defined [here](https://developer.mozilla.org/docs/Web/CSS/color%5Fvalue#hsl%5Fcolors) | | Hsla | CSS HSLA color as defined [here](https://developer.mozilla.org/docs/Web/CSS/color%5Fvalue#hsl%5Fcolors) | | IPv4 | IPv4 address as defined [here](https://en.wikipedia.org/wiki/IPv4) | | IPv6 | IPv6 address as defined in [RFC 8064](https://tools.ietf.org/html/rfc8064) | | Isbn | ISBN-10 or ISBN-13 number as defined [here](https://en.wikipedia.org/wiki/International%5FStandard%5FBook%5FNumber) | | Latitude | Decimal degrees latitude number | | Longitude | Decimal degrees longitude number | | MacAddress | IEEE 802 48-bit (MAC-48/EUI-48) and 64-bit (EUI-64) Mac addresses as defined in [RFC 7042](https://tools.ietf.org/html/rfc7042#page-19) and [RFC 7043](https://tools.ietf.org/html/rfc7043) | | PhoneNumber | E.164 format phone number as defined [here](https://en.wikipedia.org/wiki/E.164) | | Rgb | CSS RGB color as defined [here](https://developer.mozilla.org/docs/Web/CSS/color%5Fvalue#rgb%5Fcolors) | | Rgba | CSS RGBA color as defined [here](https://developer.mozilla.org/docs/Web/CSS/color%5Fvalue#rgb%5Fcolors) | | UtcOffset | A value of format ±hh:mm | Many of these scalars are built on native .NET types. An email address, for example, is represented as a `string`, but returning a `string` from your resolver causes Hot Chocolate to interpret it as a `StringType`. You need to specify the scalar type explicitly: C# ``` [GraphQLType] public string GetEmail() => "test@example.com"; ``` [Learn more about explicit types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types#explicit-types) ## NodaTime Scalars For [NodaTime](https://github.com/nodatime/nodatime) types, install the dedicated package: Bash ``` dotnet add package HotChocolate.Types.NodaTime ``` Warning All `HotChocolate.*` packages need to have the same version. `HotChocolate.Types.NodaTime` provides alternative implementations of the same five built-in date and time scalars defined by the specifications on [scalars.graphql.org](https://scalars.graphql.org/): | GraphQL Scalar | NodaTime Runtime Type | Replaces Built-in Mapping | | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------- | | [DateTime](https://scalars.graphql.org/chillicream/date-time.html) | [OffsetDateTime](https://nodatime.org/3.2.x/api/NodaTime.OffsetDateTime.html) | DateTimeOffset | | [Duration](https://scalars.graphql.org/chillicream/duration.html) | [Duration](https://nodatime.org/3.2.x/api/NodaTime.Duration.html) | (see note below for TimeSpan) | | [LocalDate](https://scalars.graphql.org/chillicream/local-date.html) | [LocalDate](https://nodatime.org/3.2.x/api/NodaTime.LocalDate.html) | DateOnly | | [LocalDateTime](https://scalars.graphql.org/chillicream/local-date-time.html) | [LocalDateTime](https://nodatime.org/3.2.x/api/NodaTime.LocalDateTime.html) | DateTime | | [LocalTime](https://scalars.graphql.org/chillicream/local-time.html) | [LocalTime](https://nodatime.org/3.2.x/api/NodaTime.LocalTime.html) | TimeOnly | Note The `Duration` scalar uses `NodaTime.Duration` as its runtime type. Calling `AddNodaTime()` does **not** automatically bind `System.TimeSpan` to `DurationType` or register `TimeSpan`↔`NodaTime.Duration` converters, as the runtime types are not compatible. These NodaTime scalars expose the same `@specifiedBy` URLs and implement the same GraphQL scalar specifications as the built-in versions, but they use NodaTime runtime types and may differ subtly in behavior. For example, the NodaTime implementations support up to 9 fractional second digits (nanosecond precision), whereas the equivalent BCL types only support up to 7 fractional second digits (100-nanosecond precision). Register them with `AddNodaTime()`: C# ``` builder .AddGraphQL() .AddNodaTime(); ``` `AddNodaTime()` registers the five scalar types above and configures the related CLR bindings and converters automatically. If you prefer, you can still register individual scalar types explicitly. For example: C# ``` using NodaTimeDurationType = HotChocolate.Types.NodaTime.DurationType; builder .AddGraphQL() .AddType(); ``` ### NodaTime scalar options `HotChocolate.Types.NodaTime.DateTimeOptions` configures the NodaTime-backed `DateTime`, `LocalDateTime`, and `LocalTime` scalars: - `InputPrecision` controls how many fractional second digits are accepted during parsing, up to `9`. - `OutputPrecision` controls how many fractional second digits are written during serialization, up to `9`. - `AlwaysOutputFractionalSeconds` always emits fractional seconds in serialized output when `OutputPrecision > 0`, padded with trailing zeros up to `OutputPrecision`. By default, trailing zeros are stripped and the fractional component is omitted entirely when zero. The option has no effect when `OutputPrecision` is `0`. Unlike the built-in BCL-backed scalars, the NodaTime implementations preserve up to 9 fractional second digits (nanosecond precision). If you need non-default NodaTime precision settings, register those scalar types individually instead of using `AddNodaTime()`: C# ``` using NodaTimeDateTimeOptions = HotChocolate.Types.NodaTime.DateTimeOptions; using NodaTimeDateTimeType = HotChocolate.Types.NodaTime.DateTimeType; using NodaTimeLocalDateTimeType = HotChocolate.Types.NodaTime.LocalDateTimeType; using NodaTimeLocalTimeType = HotChocolate.Types.NodaTime.LocalTimeType; builder .AddGraphQL() .AddType(new NodaTimeDateTimeType(new NodaTimeDateTimeOptions { OutputPrecision = 3, AlwaysOutputFractionalSeconds = true })) .AddType(new NodaTimeLocalDateTimeType(new NodaTimeDateTimeOptions { OutputPrecision = 3, AlwaysOutputFractionalSeconds = true })) .AddType(new NodaTimeLocalTimeType(new NodaTimeDateTimeOptions { OutputPrecision = 3, AlwaysOutputFractionalSeconds = true })); ``` ## Binding Behavior You can override the default .NET-to-scalar mappings by specifying type bindings explicitly: C# ``` builder .AddGraphQL() .BindRuntimeType(); ``` You can also bind scalars to arrays or complex types: C# ``` builder .AddGraphQL() .BindRuntimeType(); ``` ## Custom Converters You can reuse existing scalar types with different runtime types by registering converters. For example, to map NodaTime's `OffsetDateTime` to the existing `DateTimeType`: C# ``` public sealed class ScheduleQueries { public OffsetDateTime GetDateTime(OffsetDateTime offsetDateTime) { return offsetDateTime; } } ``` C# ``` builder .AddGraphQL() .AddQueryType() .BindRuntimeType() .AddTypeConverter( x => x.ToDateTimeOffset()) .AddTypeConverter( x => OffsetDateTime.FromDateTimeOffset(x)); ``` ## Custom Scalars A custom scalar converts values between the GraphQL wire format and a .NET runtime type. Each custom scalar handles four conversion scenarios: | Method | Direction | Purpose | | -------------------- | ----------------------- | --------------------------------------------------------------- | | OnCoerceInputLiteral | GraphQL literal to .NET | Parses values embedded in a query, e.g. { field(arg: "value") } | | OnCoerceInputValue | JSON to .NET | Parses values provided as variables in the request | | OnCoerceOutputValue | .NET to JSON | Writes resolver results to the response | | OnValueToLiteral | .NET to GraphQL literal | Converts default values for schema introspection | Extend `ScalarType` to create a custom scalar: C# ``` public sealed class CreditCardNumberType : ScalarType { private readonly ICreditCardValidator _validator; // You can inject services registered with the DI container public CreditCardNumberType(ICreditCardValidator validator) : base("CreditCardNumber") { _validator = validator; Description = "Represents a credit card number"; } protected override string OnCoerceInputLiteral(StringValueNode valueLiteral) { AssertCreditCardNumberFormat(valueLiteral.Value); return valueLiteral.Value; } protected override string OnCoerceInputValue( JsonElement inputValue, IFeatureProvider context) { var value = inputValue.GetString()!; AssertCreditCardNumberFormat(value); return value; } protected override void OnCoerceOutputValue( string runtimeValue, ResultElement resultValue) { AssertCreditCardNumberFormat(runtimeValue); resultValue.SetStringValue(runtimeValue); } protected override StringValueNode OnValueToLiteral(string runtimeValue) { AssertCreditCardNumberFormat(runtimeValue); return new StringValueNode(runtimeValue); } private void AssertCreditCardNumberFormat(string value) { if (!_validator.ValidateCreditCard(value)) { throw new LeafCoercionException( "The specified value is not a valid credit card number.", this); } } } ``` ### Specialized Base Classes Hot Chocolate provides specialized base classes for common scalar patterns. #### Integer scalars Use `IntegerTypeBase` for numeric scalars with min/max constraints. The base class handles parsing, validation, and range checking automatically. C# ``` public sealed class TcpPortType : IntegerTypeBase { public TcpPortType() : base("TcpPort", min: 1, max: 65535) { Description = "A valid TCP port number (1-65535)"; } protected override int OnCoerceInputLiteral(IntValueNode valueLiteral) => valueLiteral.ToInt32(); protected override int OnCoerceInputValue(JsonElement inputValue) => inputValue.GetInt32(); protected override void OnCoerceOutputValue(int runtimeValue, ResultElement resultValue) => resultValue.SetNumberValue(runtimeValue); protected override IValueNode OnValueToLiteral(int runtimeValue) => new IntValueNode(runtimeValue); } ``` `IntegerTypeBase` validates that values fall within the specified range and throws a `LeafCoercionException` if they do not. To customize the error message, override `FormatError`: C# ``` protected override LeafCoercionException FormatError(int runtimeValue) => new LeafCoercionException( $"The value '{runtimeValue}' is not a valid TCP port. Must be between 1 and 65535.", this); ``` Hot Chocolate also provides `FloatTypeBase` for floating-point scalars (`float`, `double`, `decimal`) that need min/max range validation. #### Regex-based scalars Use `RegexType` for string scalars that must match a specific pattern. This works well for formats like phone numbers, postal codes, or identifiers. C# ``` public sealed class HexColorType : RegexType { public HexColorType() : base( "HexColor", "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", "A hex color code, e.g. #FF5733 or #F53") { } } ``` You can also instantiate `RegexType` directly when registering scalars: C# ``` builder .AddGraphQL() .AddType(new RegexType( "PostalCode", @"^\d{5}(-\d{4})?$", "US postal code in format 12345 or 12345-6789")); ``` To customize the error message for pattern validation failures, override `FormatException`: C# ``` protected override LeafCoercionException FormatException(string runtimeValue) => new LeafCoercionException( $"'{runtimeValue}' is not a valid hex color. Expected format: #RGB or #RRGGBB.", this); ``` ## Next Steps - **Need to define object types?** See [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). - **Need to accept complex inputs?** See [Input Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types). - **Need to define enums?** See [Enums](https://chillicream.com/docs/hotchocolate/defining-a-schema/enums). - **Need to add custom validation logic?** See [Directives](https://chillicream.com/docs/hotchocolate/defining-a-schema/directives). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/scalars.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Subscriptions - Hot Chocolate > Add real-time GraphQL subscriptions in Hot Chocolate with [SubscriptionType] and [Subscribe], backed by in-memory, Redis, NATS, or Postgres providers. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/subscriptions GraphQL subscriptions allow clients to receive real-time updates from the server. A client opens a persistent connection (over WebSocket or SSE) and asks for specific events. When those events occur, the server pushes the data to the client immediately. Subscriptions differ from queries and mutations in one key way: the client receives a stream of results rather than a single response. Each result has the same shape as a query response. **GraphQL schema** GraphQL ``` type Subscription { orderStatusChanged(orderId: ID!): Order! bookAdded: Book! } ``` **Client subscription** GraphQL ``` subscription { bookAdded { title author } } ``` The client stays connected and receives a new `bookAdded` result each time the server publishes that event. ## Defining a Subscription Type Mark a class with `[SubscriptionType]` and the source generator registers it as part of the Subscription type. The class must be `partial` so the source generator can add code at build time. Each subscription field uses two attributes: - `[Subscribe]` tells Hot Chocolate this field represents a subscription and should be backed by a topic from the pub/sub system. - `[EventMessage]` marks the parameter that receives the event payload when a message arrives on the topic. C# ``` [SubscriptionType] public static partial class BookSubscriptions { [Subscribe] public static Book OnBookAdded([EventMessage] Book book) => book; } ``` The source generator wires up the Subscription type automatically. No additional registration call is needed beyond the source generator's `AddTypes`. The method body returns the event payload. Hot Chocolate calls this method each time a message arrives, so you can transform or filter the payload before it reaches the client. ## Publishing Events with ITopicEventSender To trigger a subscription, you publish an event using `ITopicEventSender`. This abstraction works with any configured subscription provider (in-memory, Redis, NATS, or Postgres), so you can switch providers without changing your publishing code. You typically publish events from mutations after a successful write. Inject `ITopicEventSender` as a method parameter, the same way you inject any other service. C# ``` [MutationType] public static partial class BookMutations { public static async Task AddBookAsync( string title, string author, CatalogContext db, ITopicEventSender sender, CancellationToken ct) { var book = new Book { Title = title, Author = author }; db.Books.Add(book); await db.SaveChangesAsync(ct); await sender.SendAsync(nameof(BookSubscriptions.OnBookAdded), book, ct); return book; } } ``` The first argument to `SendAsync` is the topic name. By default, Hot Chocolate maps the topic to the subscription field by method name. Using `nameof` keeps the topic and the subscription field in sync at compile time. You can also publish events from anywhere you have access to `ITopicEventSender` through dependency injection, not only from mutations. ## Topic Filtering with Dynamic Topics By default, every subscriber to a field receives every event published to that topic. When you need subscribers to receive events for a specific resource, use the `[Topic]` attribute with argument placeholders to create dynamic topics. C# ``` [SubscriptionType] public static partial class OrderSubscriptions { [Subscribe] [Topic($"{{{nameof(orderId)}}}")] public static Order OnOrderStatusChanged( [ID] string orderId, [EventMessage] Order order) => order; } ``` The `{orderId}` placeholder is replaced with the actual argument value at subscription time. A client subscribing with `orderId: "order-42"` only receives events published to the topic `"order-42"`. Publish to the matching topic from your mutation: C# ``` [MutationType] public static partial class OrderMutations { public static async Task UpdateOrderStatusAsync( [ID] string orderId, OrderStatus newStatus, OrderService orders, ITopicEventSender sender, CancellationToken ct) { var order = await orders.UpdateStatusAsync(orderId, newStatus, ct); await sender.SendAsync(orderId, order, ct); return order; } } ``` You can combine multiple arguments in a single topic pattern. Each placeholder uses the format `{argumentName}`: C# ``` [Subscribe] [Topic("OnMessage_{arg1}_{arg2}")] public static string OnMessage(string arg1, string arg2, [EventMessage] string message) => message; ``` ## Static Topics If you want to decouple the topic name from the method name, use `[Topic]` with a fixed string. C# ``` [SubscriptionType] public static partial class BookSubscriptions { [Subscribe] [Topic("NewBookAvailable")] public static Book OnBookAdded([EventMessage] Book book) => book; } ``` Publish to the same static topic string: C# ``` await sender.SendAsync("NewBookAvailable", book, ct); ``` ## Custom Subscribe Resolvers If you need more control over how a subscription connects to the pub/sub system, use `[Subscribe(With = ...)]` to point to a custom subscribe resolver method. C# ``` [SubscriptionType] public static partial class BookSubscriptions { public static ValueTask> SubscribeToBooks( ITopicEventReceiver receiver) => receiver.SubscribeAsync("CustomBookTopic"); [Subscribe(With = nameof(SubscribeToBooks))] public static Book OnBookAdded([EventMessage] Book book) => book; } ``` The `With` parameter names a method on the same class that returns a `ValueTask>`. Hot Chocolate calls this method when a client subscribes. This is useful when the topic name depends on runtime logic that goes beyond argument placeholders. ## Transport Mechanisms Subscriptions require a persistent connection between the client and server. Hot Chocolate supports two transport mechanisms. ### WebSocket (graphql-ws protocol) WebSocket provides a full-duplex channel over a single TCP connection. Both the client and server can send messages at any time. This is the most widely supported option for GraphQL subscriptions. Hot Chocolate supports both the modern [graphql-ws](https://github.com/enisdenjo/graphql-ws) protocol and the legacy [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) protocol. Use graphql-ws for new projects. Add the WebSocket middleware to your request pipeline: C# ``` app.UseRouting(); app.UseWebSockets(); app.MapGraphQL(); ``` ### Server-Sent Events (graphql-sse) Server-Sent Events (SSE) is a one-way channel where the server pushes updates to the client over HTTP. SSE works well with HTTP/2 and has better firewall compatibility than WebSocket. The trade-off is that SSE only supports server-to-client communication. Hot Chocolate supports the [graphql-sse](https://github.com/enisdenjo/graphql-sse) protocol. SSE works out of the box when you map the GraphQL endpoint. No additional middleware is needed. Choose WebSocket when you need bidirectional communication or broad client library support. Choose SSE when you want to leverage HTTP/2 multiplexing and avoid WebSocket-related firewall issues. ## Subscription Providers A subscription provider is the pub/sub backend that delivers events between your mutation (the publisher) and the subscription (the subscriber). You must register exactly one provider. ### In-Memory (default) The in-memory provider works without any external infrastructure. It is suitable for single-server deployments and local development. C# ``` builder .AddGraphQL() .AddInMemorySubscriptions(); ``` Events are lost if the server restarts, and they are not shared across multiple server instances. ### Redis The Redis provider supports multi-instance deployments. Events published on one server instance are delivered to subscribers connected to any instance. Install the package: Bash ``` dotnet add package HotChocolate.Subscriptions.Redis ``` Warning All `HotChocolate.*` packages need to have the same version. C# ``` builder .AddGraphQL() .AddRedisSubscriptions( _ => ConnectionMultiplexer.Connect("localhost:6379")); ``` The Redis provider uses [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis) under the hood. ### NATS The NATS provider supports multi-instance deployments, similar to Redis. NATS uses core publish/subscribe. JetStream is not required. Install the packages: Bash ``` dotnet add package HotChocolate.Subscriptions.Nats ``` Warning All `HotChocolate.*` packages need to have the same version. Bash ``` dotnet add package NATS.Extensions.Microsoft.DependencyInjection ``` C# ``` using NATS.Extensions.Microsoft.DependencyInjection; builder.Services .AddNatsClient( nats => nats.ConfigureOptions( options => options.Configure( opts => opts.Opts = opts.Opts with { Url = "nats://localhost:4222" }))); builder .AddGraphQL() .AddSubscriptionType() .AddNatsSubscriptions(); ``` If multiple GraphQL servers share the same NATS broker, set a `TopicPrefix` to isolate their topics: C# ``` using HotChocolate.Subscriptions; builder .AddGraphQL() .AddSubscriptionType() .AddNatsSubscriptions( new SubscriptionOptions { TopicPrefix = "orders-service-dev" }); ``` ### Postgres The Postgres provider uses PostgreSQL's native `LISTEN/NOTIFY` mechanism. This is a good choice when you already run PostgreSQL and want to avoid adding a separate pub/sub service. Install the package: Bash ``` dotnet add package HotChocolate.Subscriptions.Postgres ``` Warning All `HotChocolate.*` packages need to have the same version. C# ``` builder .AddGraphQL() .AddSubscriptionType() .AddPostgresSubscriptions(options => options.ConnectionFactory = ct => /* create your NpgsqlConnection */); ``` For the connection factory, configure a long-lived connection with pooling disabled: C# ``` var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); dataSourceBuilder.ConnectionStringBuilder.Pooling = false; dataSourceBuilder.ConnectionStringBuilder.KeepAlive = 30; dataSourceBuilder.ConnectionStringBuilder.Enlist = false; var dataSource = dataSourceBuilder.Build(); ``` ## Splitting Across Multiple Classes GraphQL allows only one Subscription type per schema, but you can split your subscription fields across multiple classes. With the source generator, annotate each class with `[SubscriptionType]`. The source generator merges them into one Subscription type. C# ``` [SubscriptionType] public static partial class BookSubscriptions { [Subscribe] public static Book OnBookAdded([EventMessage] Book book) => book; } ``` C# ``` [SubscriptionType] public static partial class OrderSubscriptions { [Subscribe] [Topic($"{{{nameof(orderId)}}}")] public static Order OnOrderStatusChanged( [ID] string orderId, [EventMessage] Order order) => order; } ``` This produces a schema with both fields on the Subscription type: GraphQL ``` type Subscription { onBookAdded: Book! onOrderStatusChanged(orderId: ID!): Order! } ``` Group your subscription classes by domain area, the same way you would split queries and mutations. ## Next Steps - **Need to read data?** See [Queries](https://chillicream.com/docs/hotchocolate/defining-a-schema/queries). - **Need to write data?** See [Mutations](https://chillicream.com/docs/hotchocolate/defining-a-schema/mutations). - **Need to understand how types map to the schema?** See [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). - **Need to authenticate WebSocket connections?** See the [WebSocket authentication example](https://github.com/ChilliCream/hotchocolate-examples/tree/master/misc/WebsocketAuthentication). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/subscriptions.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Unions - Hot Chocolate > Define GraphQL union types in Hot Chocolate with the [UnionType] attribute or marker interfaces, returning one of several object types from a field. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/unions A GraphQL union represents a set of object types that share no required common fields. Unlike [interfaces](https://chillicream.com/docs/hotchocolate/defining-a-schema/interfaces), union members do not need to declare the same fields. Clients use inline fragments to select fields from each possible type. **GraphQL schema** GraphQL ``` type TextContent { text: String! } type ImageContent { imageUrl: String! height: Int! } union PostContent = TextContent | ImageContent ``` **Client query** GraphQL ``` { content { ... on TextContent { text } ... on ImageContent { imageUrl } } } ``` ## Defining a Union Type Use a marker interface (an interface with no members) or an abstract class to group the types that belong to the union. C# ``` [UnionType("PostContent")] public interface IPostContent { } public class TextContent : IPostContent { public string Text { get; set; } } public class ImageContent : IPostContent { public string ImageUrl { get; set; } public int Height { get; set; } } [QueryType] public static partial class ContentQueries { public static IPostContent GetContent() { // ... } } ``` C# ``` builder .AddGraphQL() .AddType() .AddType(); ``` Each type that implements the marker interface must be registered so Hot Chocolate includes it in the union. ## Union vs Interface Both unions and interfaces are abstract types that let a field return one of several object types. They differ in how much structure they enforce and how clients query them. **Use a union** when the member types are genuinely different entities with no meaningful shared fields, for example a search that can return a `User`, a `Post`, or a `Comment`. Clients must use inline fragments for every field because the union guarantees no common structure. GraphQL ``` union SearchResult = User | Post | Comment # Client must fragment into each type query { search(term: "graphql") { ... on User { name } ... on Post { title } ... on Comment { body } } } ``` **Use an interface** when the types share common fields that clients regularly query together. The interface enforces a contract: every implementing type must include the interface fields. Clients can query those fields directly without fragments. GraphQL ``` interface Event { id: ID! timestamp: DateTime! } # Shared fields are queryable directly query { events { id timestamp ... on UserEvent { user { name } } ... on SystemEvent { severity } } } ``` **Use both together** when you need the flexibility of a union with some guaranteed fields across members. The errors-as-data pattern is a common example: a union separates success from failure, while an interface guarantees a `message` field on all error types. GraphQL ``` interface CheckoutError { message: String! } type InsufficientStockError implements CheckoutError { message: String! availableStock: Int! } type InvalidPaymentError implements CheckoutError { message: String! } union CheckoutResult = Order | InsufficientStockError | InvalidPaymentError ``` ## Next Steps - **Need shared fields across types?** See [Interfaces](https://chillicream.com/docs/hotchocolate/defining-a-schema/interfaces). - **Need to define output types?** See [Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). - **Need input polymorphism?** See [OneOf Input Objects](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types#oneof-input-objects). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/unions.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Versioning - Hot Chocolate > Version a GraphQL schema in Hot Chocolate without URL versioning: deprecate fields with @deprecated and gate new features behind @requiresOptIn. Canonical source: https://chillicream.com/docs/hotchocolate/defining-a-schema/versioning Unlike REST APIs, GraphQL schemas do not use URL-based versioning (like `/graphql/v2`). Most schema changes are additive and non-breaking: adding new types and new fields does not affect existing queries. Removing a field or changing its nullability, however, is a breaking change. GraphQL provides two directives to manage the lifecycle of schema elements: - `@deprecated` signals that a field is being phased out and consumers should migrate away. - `@requiresOptIn` signals that a field is not yet stable and requires explicit consumer consent. GraphQL ``` type Query { users: [User] @deprecated(reason: "Use the `authors` field instead") authors: [User] recommendations: [Book] @requiresOptIn(feature: "experimentalRecommendations") } ``` ## Deprecation You can deprecate output fields, input fields, arguments, and enum values without any extra configuration. Object types can be deprecated too, but only once you enable a separate opt-in option (see [Deprecating Object Types](#deprecating-object-types)). Deprecated elements remain functional but are flagged in introspection, warning consumers to migrate. C# ``` [QueryType] public static partial class BookQueries { [GraphQLDeprecated("Use the `authors` field instead")] public static User[] GetUsers() { // ... } public static User[] GetAuthors() { // ... } } ``` The .NET `[Obsolete("reason")]` attribute works the same way as `[GraphQLDeprecated("reason")]` for fields, arguments, input fields, and enum values. Deprecating an object type itself is different: see [Deprecating Object Types](#deprecating-object-types). 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. ### Deprecating Object Types Deprecating an object type is not yet part of the released GraphQL specification. It tracks [graphql-spec RFC #997](https://github.com/graphql/graphql-spec/pull/997), which is still open, so its final shape could still change. Enable it deliberately, and treat it as subject to change until the RFC is merged. Enable it in your schema options: C# ``` builder .AddGraphQL() .ModifyOptions(o => o.EnableObjectDeprecation = true); ``` Once enabled, `@deprecated` becomes valid on object types: C# ``` [GraphQLDeprecated("No longer known to exist.")] public class Baiji { public string? Name { get; set; } } ``` Note The .NET `[Obsolete]` attribute does not deprecate an object type; only `[GraphQLDeprecated]` on the class does. Every other deprecatable member honors both attributes identically, but honoring `[Obsolete]` on a class would silently deprecate types across existing codebases the moment this option was enabled, which is a schema-build failure for a field that returns such a type without itself being deprecated. Unifying the two attributes for object types is deferred to a future major version. A field that is not itself deprecated cannot return a deprecated object type. Reaching a deprecated type indirectly is fine, so the following schema is valid even though `Baiji` is deprecated: GraphQL ``` type Query { animals: [Animal] } interface Animal { name: String } type Dog implements Animal { name: String } type Baiji implements Animal @deprecated(reason: "No longer known to exist.") { name: String } ``` `animals` returns the interface `Animal`, not `Baiji` directly, so no field needs deprecating. Contrast a field that returns `Baiji` directly: GraphQL ``` type Query { baiji: Baiji } type Baiji @deprecated(reason: "No longer known to exist.") { name: String } ``` Warning `Query.baiji` is not deprecated but returns the deprecated `Baiji` type, so building the schema fails. Deprecate the field, or change its return type. A deprecated object type remains a valid union member and interface implementation. Only the field returning it is checked. Introspection exposes object type deprecation through `__Type.isDeprecated` and `__Type.deprecationReason`. Deprecated object types are hidden from `__schema.types` and `__Type.possibleTypes` by default: GraphQL ``` { __schema { types(includeDeprecated: true) { name isDeprecated deprecationReason } } } ``` ## Opt-In Features While `@deprecated` marks schema elements that are going away, `@requiresOptIn` marks schema elements that are not yet stable. This is useful for rolling out experimental features, expensive operations, or anything where consumers should make a deliberate choice to use it. Schema elements marked with `@requiresOptIn` are hidden from introspection by default. Consumers opt in by specifying the feature name. ### Enabling Opt-In Features Opt-in feature support is disabled by default. Enable it in your schema options: C# ``` builder .AddGraphQL() .ModifyOptions(o => o.EnableOptInFeatures = true); ``` ### Marking Schema Elements as Opt-In Apply `@requiresOptIn` to output fields, input fields, arguments, enum values, and directive definitions. The directive is repeatable, so a single element can require multiple features. C# ``` public class Session { public string Id { get; set; } public string Title { get; set; } [RequiresOptIn("experimentalInstantApi")] public Instant? StartInstant { get; set; } [RequiresOptIn("experimentalInstantApi")] public Instant? EndInstant { get; set; } } ``` Warning Like `@deprecated`, you cannot apply `@requiresOptIn` to non-null arguments or input fields without a default value. Hiding a required field would break queries. ### Introspection Consumers discover opt-in fields by passing the `includeOptIn` argument: GraphQL ``` { __type(name: "Session") { fields(includeOptIn: ["experimentalInstantApi"]) { name requiresOptIn } } } ``` The `includeOptIn` argument is available on `fields`, `args`, `inputFields`, `enumValues`, and `directives` in introspection queries. A directive definition exposes its own required features via `__Directive.requiresOptIn`, mirroring the `requiresOptIn` field on other introspection types. To discover all opt-in features in the schema: GraphQL ``` { __schema { optInFeatures } } ``` ### Feature Stability You can declare the stability level of each opt-in feature. This helps consumers understand whether a feature is experimental, preview, or has some other status. C# ``` builder .AddGraphQL() .ModifyOptions(o => o.EnableOptInFeatures = true) .OptInFeatureStability("experimentalInstantApi", "experimental"); ``` Consumers query feature stability through introspection: GraphQL ``` { __schema { optInFeatureStability { feature stability } } } ``` ## Next Steps - **Need to add descriptions?** See [Documentation](https://chillicream.com/docs/hotchocolate/defining-a-schema/documentation). - **Need to create custom directives?** See [Directives](https://chillicream.com/docs/hotchocolate/defining-a-schema/directives). - **Need to understand schema evolution?** See [Extending Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/defining-a-schema/versioning.md) Maintained by ChilliCream. Last updated on **August 03, 2026** by **Glen** --- # Fetching Data - Hot Chocolate > Overview of data middleware in Hot Chocolate: pagination, filtering, sorting, projections, and DataLoader batching applied to IQueryable data sources. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data Hot Chocolate provides data middleware that applies common operations directly to your `IQueryable` or `IExecutable` data sources. Instead of implementing pagination, filtering, sorting, and projections by hand, you declare them on your fields and Hot Chocolate generates the corresponding GraphQL types and applies the operations at execution time. ## Pagination 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](https://chillicream.com/docs/hotchocolate/fetching-data/pagination) ## Filtering When you return a list of entities, clients often need to filter them by operations like `equals`, `contains`, or `startsWith`. Hot Chocolate generates the necessary filter input types from your .NET models and translates applied filters into native database queries. [Learn more about filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering) ## Sorting Hot Chocolate generates sort input types from your .NET models, allowing clients to specify which fields to sort by and in which direction. Like filtering, sort operations translate to native database queries when backed by `IQueryable`. [Learn more about sorting](https://chillicream.com/docs/hotchocolate/fetching-data/sorting) ## Projections Projections optimize database queries by selecting only the columns that match the fields requested in the GraphQL query. If a client requests `name` and `id`, Hot Chocolate queries only those columns from the database. [Learn more about projections](https://chillicream.com/docs/hotchocolate/fetching-data/projections) ## Batching DataLoaders and batch resolvers solve the N+1 problem in GraphQL. When the execution engine resolves a list of objects and each needs related data, a DataLoader collects all individual requests and sends a single query for all keys at once. - [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader) for key-based batching with deduplication and caching. - [Batch Resolvers](https://chillicream.com/docs/hotchocolate/fetching-data/batching/batch-resolver) for simpler cases where caching is not needed. ## Integrations Hot Chocolate is not bound to a specific database. The data middleware works with any `IQueryable` provider. We provide specific guidance for the most common data sources: - [Entity Framework](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework) for EF Core DbContext patterns and pooling. - [MongoDB](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/mongodb) for the MongoDB driver integration. - [Marten](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/marten) for Marten document database support. - [Extending Filtering](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/extending-filtering) for building custom filter providers. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/index.md) Maintained by ChilliCream. Last updated on **July 01, 2026** by **Tobias Tengler** --- # GraphQL Batching in Hot Chocolate: Solve the N+1 Problem > Learn how batching solves the N+1 problem in GraphQL. Hot Chocolate offers DataLoader and batch resolvers to group data fetches into single calls. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/batching Batching groups many small data fetches into one call to your data source and is the standard answer to the N+1 problem in GraphQL. Because GraphQL resolves data field by field, you should plan for batching from the start: it determines whether your API issues one query per request or hundreds. This chapter explains why the N+1 problem appears in GraphQL and introduces the two batching tools Hot Chocolate provides: [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader) and [batch resolvers](https://chillicream.com/docs/hotchocolate/fetching-data/batching/batch-resolver). Note This chapter is about batching data fetches inside a single GraphQL operation. If you are looking for a way to send multiple GraphQL operations in one HTTP request, see [Server Batching](https://chillicream.com/docs/hotchocolate/server/batching). ## Why the N+1 Problem Happens in Every API The N+1 problem exists with any API technology. A REST client that fetches a list of products and then calls `/brands/{id}` for each product produces the exact same access pattern: one request for the list, plus N requests for related data. The same happens inside a REST backend that loads a list and then lazily loads a relation per item. GraphQL does not make this problem worse. What GraphQL changes is where the problem lives. With REST, each client decides how to stitch resources together, so the N+1 pattern is distributed across every client and cannot be fixed centrally. With GraphQL, clients declare what they need in one query, and the server resolves it. The deliberate decision is to handle data fetching centrally in the backend, and the backend is exactly where it can be optimized well: it is one place, it is close to the data, and one fix helps every client. ## Why Per-Field Resolvers Make N+1 Visible Every field in a GraphQL schema is backed by a [resolver](https://chillicream.com/docs/hotchocolate/resolvers), and the execution engine walks the query as a tree. Consider this query: **Client query** GraphQL ``` { products(first: 5) { nodes { name brand { name } } } } ``` The `products` resolver runs once and returns five products. Then the `brand` resolver runs once per product. A naive `brand` resolver that queries the database directly issues five queries for five products, and fifty queries for fifty products: ``` 1 query: products(first: 5) 5 queries: brand for product 1..5 ← N+1 ``` The per-field resolver model is what makes the N+1 pattern visible and measurable in GraphQL. That visibility is a feature: because all data fetching flows through resolvers, you can intercept it in one place and batch it. ## DataLoader A DataLoader batches by key. Resolvers ask the DataLoader for a value by key, the DataLoader collects all requested keys while the engine executes resolvers, and then fetches all of them in one call (for example a single `WHERE id IN (...)` query). DataLoaders also cache and deduplicate within a request: the same key requested from anywhere in the query tree is fetched once, and every resolver sees the same result. DataLoaders are the default choice for batching in Hot Chocolate. Use them whenever data is loaded by key and may be requested from more than one place in a query. [Learn more about DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader) ## Batch Resolvers A batch resolver batches by field. Instead of running a resolver once per parent object, the execution engine collects all parent objects that reach a specific field and calls your resolver once with the full list. You do not define a DataLoader class or a key: you receive the parents and return one result per parent. Batch resolvers have no cache and no cross-field deduplication. They fit computed values, aggregations, and external services with native batch endpoints, where the result is specific to one field. [Learn more about Batch Resolvers](https://chillicream.com/docs/hotchocolate/fetching-data/batching/batch-resolver) ## Next Steps - **Loading data by key?** Start with [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader). - **Resolving one field for many parents?** See [Batch Resolvers](https://chillicream.com/docs/hotchocolate/fetching-data/batching/batch-resolver). - **New to resolvers?** See [Resolvers](https://chillicream.com/docs/hotchocolate/resolvers) for the resolver tree mental model. - **Using Entity Framework?** See [Entity Framework](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework) for integration patterns. - **Looking for HTTP request batching?** See [Server Batching](https://chillicream.com/docs/hotchocolate/server/batching). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/batching/index.md) Maintained by ChilliCream. Last updated on **July 08, 2026** by **Michael Staib** --- # GraphQL Batch Resolvers: DataLoader Alternative > Use a Hot Chocolate batch resolver to resolve a GraphQL field for many parents in one call: a lighter alternative to DataLoader without caching. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/batching/batch-resolver Batch resolvers resolve a GraphQL field for many parent objects in a single call. Instead of running a resolver once per parent and batching through a [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader), the execution engine collects all parent objects that reach the field and calls your method once with the full list. You do not define a DataLoader class or manage keys. ## When to Use Batch Resolvers vs DataLoaders **Use a DataLoader** when data is loaded by key and may be requested from more than one place in a query. DataLoaders cache and deduplicate by key, so the same entity fetched in different parts of the query tree is only loaded once. **Use a batch resolver** when the resolved value is specific to one field and does not benefit from cross-field caching. Common examples: computed values, aggregations over the parent set, or calling an external service that supports batch requests natively. The batching models differ: a DataLoader batches by key and can merge lookups across fields, types, and depths, while a batch resolver batches by field and is invoked exactly once per field selection in an operation, with no cache involved. ## Defining a Batch Resolver Mark a method with `[BatchResolver]`. The `[Parent]` parameter must be a list of the parent type, and the return type must be a list with one element per parent, in the same order. **C# resolver** C# ``` [ObjectType] public static partial class UserNode { [BatchResolver] public static List GetDisplayName([Parent] List users) { return users.Select(u => $"{u.FirstName} {u.LastName}").ToList(); } } ``` The execution engine collects all `User` parent objects being resolved for this field and calls `GetDisplayName` once with the full list. The field's GraphQL type is derived from the list's element type, so this field is a `String`. Supported list shapes for the `[Parent]` parameter, argument parameters, and the return type are `T[]`, `List`, `IList`, `IReadOnlyList`, and `ImmutableArray`. Other collection types like `IEnumerable` are rejected when the schema is built. Warning Returning one result per parent, in parent order, is your responsibility. If an attribute-based batch resolver returns fewer elements than parents, the remaining parents silently receive `null`; extra elements are ignored. Only the code-first `ResolveBatch` API enforces the count and throws on a mismatch. ### A Real-World Example A typical use case is an aggregate over the parent set, computed with one database query: C# ``` [ObjectType] public static partial class BrandNode { [BatchResolver] public static async Task> GetProductCountAsync( [Parent] List brands, [Service] CatalogContext context, CancellationToken cancellationToken) { var brandIds = brands.ConvertAll(b => b.Id); var counts = await context.Products .Where(p => brandIds.Contains(p.BrandId)) .GroupBy(p => p.BrandId) .Select(g => new { BrandId = g.Key, Count = g.Count() }) .ToDictionaryAsync(g => g.BrandId, g => g.Count, cancellationToken); return brands.ConvertAll(b => counts.GetValueOrDefault(b.Id, 0)); } } ``` No matter how many brands the query returns, `productCount` is computed with a single grouped query, and the results are mapped back to the parents positionally. ## Parameter Binding Batch resolver parameters fall into two groups: - **Per parent (list-typed)**: the `[Parent]` parameter and GraphQL field arguments. Both are collected as lists with one entry per parent, in the same order as the parents. An argument parameter declared as `List prefix` produces a GraphQL argument `prefix: String` (the element type, not a list type), and `prefix[i]` carries the coerced argument value for the parent at index `i`. - **Once per batch (singular)**: everything else. Services (`[Service]`), `[GlobalState]`, `[ScopedState]`, and `CancellationToken` are resolved once for the whole batch call, not per parent. C# ``` [ObjectType] public static partial class UserNode { [BatchResolver] public static List GetGreeting( [Parent] List users, List prefix) { var result = new List(); for (var i = 0; i < users.Count; i++) { result.Add($"{prefix[i]}, {users[i].Name}!"); } return result; } } ``` ## Async Batch Resolvers and Services Batch resolvers can be synchronous or return `Task` or `ValueTask`. Services are injected with the `[Service]` attribute: C# ``` [ObjectType] public static partial class UserNode { [BatchResolver] public static async Task> GetGreeting( [Parent] List users, [Service] GreetingService greetingService, CancellationToken ct) { return await greetingService.GetGreetingsAsync( users.Select(u => u.Id).ToList(), ct); } } ``` Warning Annotate custom service parameters with `[Service]`. Without it, the source generator classifies the parameter as a per-parent GraphQL argument and generates broken code. Well-known infrastructure types like `CancellationToken` are recognized without an attribute. ## Handling Errors If a batch resolver throws an unhandled exception, the entire batch fails: every parent in the batch receives the same error and a `null` result. To report an error for individual parents while the rest of the batch resolves normally, use the code-first `ResolveBatch` API with `ResolverResult`. Each element of the returned list is either `ResolverResult.Ok(value)` or `ResolverResult.Fail(error)`: C# ``` public class UserType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor .Field("verificationStatus") .Type() .ResolveBatch(contexts => { var results = new ResolverResult[contexts.Count]; for (var i = 0; i < contexts.Count; i++) { var user = contexts[i].Parent(); results[i] = user.Email is null ? ResolverResult.Fail( ErrorBuilder.New() .SetMessage("User has no email address.") .Build()) : ResolverResult.Ok(user.IsVerified ? "verified" : "pending"); } return new ValueTask>(results); }); } } ``` A failed element becomes a GraphQL error at that specific parent's path, while the other parents keep their data: **Response** JSON ``` { "errors": [ { "message": "User has no email address.", "path": ["users", 1, "verificationStatus"] } ], "data": { "users": [ { "verificationStatus": "verified" }, { "verificationStatus": null } ] } } ``` Warning `ResolverResult` only works with the code-first `ResolveBatch` API. Returning `List` from a `[BatchResolver]`\-attributed method (or through `ResolveBatchWith`) is not unwrapped: every element fails leaf-value coercion with an `EXEC_INVALID_LEAF_VALUE` error. ## Code-First Batch Resolvers In the code-first approach, use `ResolveBatch` on the field descriptor. The delegate receives one `IResolverContext` per parent and must return exactly one `ResolverResult` per context, in the same order. A count mismatch throws an `InvalidOperationException` at execution time. C# ``` public class UserType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor .Field("displayName") .Type() .ResolveBatch(contexts => { var results = new ResolverResult[contexts.Count]; for (var i = 0; i < contexts.Count; i++) { var user = contexts[i].Parent(); results[i] = ResolverResult.Ok($"{user.FirstName} {user.LastName}"); } return new ValueTask>(results); }); } } ``` You can also point a field at an existing batch resolver method with `ResolveBatchWith`: C# ``` descriptor .Field("displayName") .ResolveBatchWith(t => t.GetDisplayName(default!)); ``` `ResolveBatchWith` only supports synchronous methods. Pointing it at a method that returns `Task` or `ValueTask` throws a `SchemaException` when the schema is built. Async batch resolvers must be defined with the `[BatchResolver]` attribute or as a `ResolveBatch` delegate. ## Limitations - Regular field middleware does not run for batch resolver fields. Middleware-based features such as `[UsePaging]`, `[UseProjection]`, `[UseFiltering]`, and `[UseSorting]` are not compatible with `[BatchResolver]` or `ResolveBatch`. - Services, state, and `CancellationToken` are bound once per batch, not per parent. When the field uses a resolver-level dependency injection scope, one scope is shared by the whole batch. - An unhandled exception fails the whole batch. Use `ResolveBatch` with `ResolverResult.Fail` for per-parent errors. ## Next Steps - **Loading data by key with caching?** See [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader). - **New to batching?** See the [Batching overview](https://chillicream.com/docs/hotchocolate/fetching-data/batching) for the N+1 background. - **Need to understand resolver basics?** See [Resolvers](https://chillicream.com/docs/hotchocolate/resolvers). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/batching/batch-resolver.md) Maintained by ChilliCream. Last updated on **July 08, 2026** by **Michael Staib** --- # GraphQL DataLoader: Solve the N+1 Problem > Solve the N+1 problem in Hot Chocolate with DataLoader: source-generated methods that batch, cache, and deduplicate data lookups within a GraphQL request. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader DataLoaders solve the N+1 problem in GraphQL by batching key-based data lookups. 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 the requested keys while resolvers execute and then sends one query for all keys at once, deduplicating and caching results for the rest of the request. This page covers the source-generated DataLoader (the recommended approach) and manual DataLoader classes. If you are new to GraphQL data fetching, start with [Resolvers](https://chillicream.com/docs/hotchocolate/resolvers) first. For the background on why batching matters, see the [Batching overview](https://chillicream.com/docs/hotchocolate/fetching-data/batching). ## The N+1 Problem Consider a schema where each product has a brand, and clients can query a list of products with their brands. **GraphQL schema** GraphQL ``` type Query { products(first: 5): ProductsConnection } type Product { id: ID! name: String! brand: Brand! } type Brand { id: ID! name: String! } ``` **Client query** GraphQL ``` query { products(first: 5) { nodes { name brand { name } } } } ``` Without a DataLoader, the `brand` resolver executes once per product. Five products means five database queries for brands, even if several products share the same brand. With 50 products, that becomes 50 queries. This is the N+1 problem: 1 query for the product list, plus N queries for related data. A DataLoader batches those N brand lookups into a single query. The resolver asks the DataLoader for each brand by key. The DataLoader collects the keys while the engine executes resolvers and then fires one `WHERE id IN (...)` query for all requested brands. ## Source-Generated DataLoader The recommended way to define a DataLoader is with the `[DataLoader]` attribute and the source generator. You write a static method that accepts a list of keys and returns a dictionary of results. The source generator creates the DataLoader class and its interface at build time. ### Batch DataLoader (one-to-one) Use a batch DataLoader when each key maps to at most one result. This is the most common pattern: fetching an entity by ID. **C# DataLoader** C# ``` internal static class BrandDataLoaders { [DataLoader] public static async Task> GetBrandByIdAsync( IReadOnlyList ids, CatalogContext db, CancellationToken ct) => await db.Brands .Where(b => ids.Contains(b.Id)) .ToDictionaryAsync(b => b.Id, ct); } ``` The source generator produces an `IBrandByIdDataLoader` interface and a `BrandByIdDataLoader` class. The name is derived from the method name: the `Get` prefix and the `Async` suffix are stripped, leaving `BrandById`. **C# resolver** C# ``` [ObjectType] public static partial class ProductNode { public static async Task GetBrandAsync( [Parent] Product product, IBrandByIdDataLoader brandById, CancellationToken ct) => await brandById.LoadAsync(product.BrandId, ct); } ``` The resolver requests a brand by key. The DataLoader collects the keys from all concurrently executing resolvers, then calls `GetBrandByIdAsync` once with the full list. Duplicate keys are collapsed: if five products reference only two distinct brands, the fetch method receives exactly two keys in one call. ### Group DataLoader (one-to-many) Use a group DataLoader when each key maps to multiple results. This is common for relationships like "all products for a brand" or "all reviews for a product". **C# DataLoader** C# ``` internal static class ProductDataLoaders { [DataLoader] public static async Task> GetProductsByBrandIdAsync( IReadOnlyList brandIds, CatalogContext db, CancellationToken ct) => await db.Products .Where(p => brandIds.Contains(p.BrandId)) .GroupBy(p => p.BrandId) .Select(g => new { g.Key, Items = g.OrderBy(p => p.Name).ToArray() }) .ToDictionaryAsync(g => g.Key, g => g.Items, ct); } ``` The return type is `Dictionary`. The generated interface is `IProductsByBrandIdDataLoader`. **C# resolver** C# ``` [ObjectType] public static partial class BrandNode { public static async Task GetProductsAsync( [Parent] Brand brand, IProductsByBrandIdDataLoader productsByBrandId, CancellationToken ct) => await productsByBrandId.LoadAsync(brand.Id, ct) ?? []; } ``` When the DataLoader returns `null` for a key (the brand has no products), use the null-coalescing operator to return an empty array. ### How the Generator Classifies `[DataLoader]` Methods The DataLoader kind is derived from the method signature: | Method shape | Kind | Behavior | | ------------------------------------------------------- | ----- | ----------------------------------------------------------------- | | Task> with IReadOnlyList | Batch | One fetch call per batch; missing keys resolve to null / default. | | Task> with IReadOnlyList | Group | One fetch call per batch; missing keys resolve to an empty array. | | Task with a single TKey parameter | Cache | Fetches each key individually, but caches results per request. | `IReadOnlyDictionary` and other `IDictionary` implementations work as batch return types too. A `Dictionary` (like the group example above) is technically a batch loader whose value is an array, which is why a missing key yields `null` there. If you return `ILookup` instead, the generated loader returns an empty array for missing keys. Beyond the key parameter, a DataLoader method can declare additional parameters: a `CancellationToken`, services from dependency injection (like the `CatalogContext` above), `[DataLoaderState]` parameters for passing context data, and data-integration parameters such as `PagingArguments`, `QueryContext`, or `ISelectorBuilder` (see [Entity Framework](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework) and [Pagination](https://chillicream.com/docs/hotchocolate/fetching-data/pagination)). ### Keyed Services Generated DataLoader methods can resolve keyed services with `[Service("key")]` or `[FromKeyedServices(key)]`. `[Service]` has parity with `[FromKeyedServices]` and supports any attribute constant, including strings, enum values, and integers. String keys remain supported unchanged. A non-nullable parameter is required. A nullable parameter is optional and can resolve to `null`. C# ``` internal enum CatalogServiceKey { ReadOnly } [AttributeUsage(AttributeTargets.Parameter)] internal sealed class ReadOnlyCatalogAttribute : ServiceAttribute { public ReadOnlyCatalogAttribute() : base("read-only") { } } internal static class BrandDataLoaders { [DataLoader] public static async Task> GetBrandByIdAsync( IReadOnlyList ids, [Service("primary")] CatalogContext primaryContext, [Service(CatalogServiceKey.ReadOnly)] CatalogContext? readOnlyContext, [ReadOnlyCatalog] CatalogCache cache, CancellationToken ct) => await primaryContext.Brands .Where(b => ids.Contains(b.Id)) .ToDictionaryAsync(b => b.Id, ct); } ``` For attributes derived from `ServiceAttribute`, the constructor chain must be available in source and the key passed to the `ServiceAttribute` base constructor must be statically determinable. If `HC0133` remains, apply the key directly with `[Service("key")]` or `[FromKeyedServices(key)]`. Keys can be any constant, including strings, enum values, and integers. `KeyedService.AnyKey` is not supported. Hand-written DataLoader constructors use `[FromKeyedServices]`: C# ``` public BrandByIdDataLoader( [FromKeyedServices("primary")] CatalogContext context, IBatchScheduler batchScheduler, DataLoaderOptions options) : base(batchScheduler, options) { _context = context; } ``` `[Service("primary")]` on a hand-written DataLoader constructor parameter is resolved as an unkeyed service and produces `HC0132`. | Code | Meaning | | ------ | ----------------------------------------------------------------------------------------------- | | HC0130 | A generated DataLoader service parameter uses both \[Service(key)\] and \[FromKeyedServices\]. | | HC0131 | A keyed-service attribute is applied to a parameter that is not a generated DataLoader service. | | HC0132 | A keyed \[Service\] attribute is applied to a hand-written DataLoader constructor parameter. | | HC0133 | A ServiceAttribute constructor chain or base-constructor key cannot be determined. | ### Handling Missing Keys A batch DataLoader returns `null` for keys that are absent from the returned dictionary. If that `null` flows into a non-nullable GraphQL field, the execution engine reports a standard non-null violation at that field's path (error code `HC0018`) and applies regular null propagation: **Response** JSON ``` { "errors": [ { "message": "Cannot return null for non-nullable field.", "path": ["products", 0, "brand"], "extensions": { "code": "HC0018" } } ], "data": { "products": [null] } } ``` If a missing key is a valid state, make the field nullable or handle the `null` in the resolver. If a missing key indicates broken data, use `LoadRequiredAsync` instead of `LoadAsync`: it throws a `KeyNotFoundException` naming the missing key, which surfaces as a clearer error than a generic non-null violation. ### Explicit DataLoader Contracts Use `[DataLoader]` when the DataLoader contract is part of your application API. `T` can be a custom interface deriving from one closed `IBatchDataLoader` or `ICacheDataLoader` contract. C# ``` public interface IUserByIdDataLoader : IBatchDataLoader; internal static class UserDataLoaders { [DataLoader] public static async Task> GetUserByIdAsync( IReadOnlyList ids, CatalogContext db, CancellationToken ct) => await db.Users .Where(t => ids.Contains(t.Id)) .ToDictionaryAsync(t => t.Id, ct); } ``` The type argument supplies the DataLoader kind, key type, and value type. The source generator creates a sealed partial `UserByIdDataLoader` class that implements `IUserByIdDataLoader`. You can also use a closed kind interface directly when no custom interface is needed: C# ``` [DataLoader>] public static async Task> GetUserByIdAsync( IReadOnlyList ids, CatalogContext db, CancellationToken ct) => await db.Users .Where(t => ids.Contains(t.Id)) .ToDictionaryAsync(t => t.Id, ct); ``` The method signature must match the selected contract: | Contract | First parameter | Return type | | ------------------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | IBatchDataLoader | IReadOnlyList | Task>, ValueTask>, Task>, or ValueTask> | | ICacheDataLoader | TKey | Task or ValueTask | Group loading uses the batch contract with an array value. For example, `IBatchDataLoader` requires a method returning `Task>`. `[DataLoader]` batch methods do not accept `ILookup` return types. When a closed `T` is used by one `[DataLoader]` method in an assembly, it is registered for dependency injection and can be injected by interface. When multiple methods use the same closed `T`, each generated DataLoader is registered only by its concrete class. Inject the generated class in that case. Custom interfaces can declare members in addition to the DataLoader contract. Implement those members in a partial declaration of the generated class: C# ``` public interface IUserByIdDataLoader : IBatchDataLoader { Task RefreshAsync(CancellationToken ct); } public sealed partial class UserByIdDataLoader { public Task RefreshAsync(CancellationToken ct) => Task.CompletedTask; } ``` The generated class name is derived from the method name or the `Name` override. `[DataLoader("CatalogUsers")]` generates `CatalogUsersDataLoader`, so the partial declaration must use that name. ### Generic DataLoader Diagnostics The `HotChocolate.Types.Analyzers` package validates `[DataLoader]` methods. | Code | Severity | Meaning | | ------ | -------- | ------------------------------------------------------------------------------------------------------------------ | | HC0122 | Error | T is neither a direct closed batch/cache interface nor a custom interface deriving from exactly one such contract. | | HC0123 | Error | The key parameter does not match the selected contract. | | HC0124 | Error | The return type does not match the selected contract. | | HC0125 | Error | The method has more than one \[DataLoader\] attribute. | | HC0126 | Info | A closed T is used by multiple methods and cannot be injected by interface. | | HC0127 | Warning | Members declared by T are not implemented by the generated partial class. | | HC0128 | Warning | DataLoaderAccessModifier.PublicInterface is treated as Public. | | HC0129 | Error | A method parameter uses ref, in, out, or ref readonly. | ### Registration Declare a DataLoader module for your assembly. The source generator then emits a registration extension method named after the module: C# ``` [assembly: DataLoaderModule("CatalogDataLoaders")] ``` C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services.AddCatalogDataLoaders(); builder.Services .AddGraphQLServer() .AddTypes(); ``` `AddCatalogDataLoaders()` registers every generated DataLoader with the dependency injection container. Resolvers do not need any further wiring: declaring the generated interface as a resolver parameter (as in the examples above) is enough, and Hot Chocolate injects the request's DataLoader instance automatically. ### DataLoader Options `[DataLoader]` and `[DataLoader]` accept the same configuration options. `[DataLoader]` uses `T` as its interface and does not generate another interface. | Option | Type | Default | Description | | -------------- | ------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Name | string | Derived from the method name | First constructor argument: \[DataLoader("...")\]. Overrides the generated DataLoader name. Used verbatim, with DataLoader appended, so \[DataLoader("BrandLookup")\] generates BrandLookupDataLoader. | | MaxBatchSize | int | 1024 | Maximum number of keys per fetch call. When more keys are pending, they are split into multiple batches. 0 disables splitting. | | ServiceScope | DataLoaderServiceScope | Default | Controls how injected services are resolved. DataLoaderScope creates a dedicated scope per fetch. OriginalScope resolves services from the request scope. Default defers to the assembly-level \[DataLoaderDefaults\] attribute; without one, a dedicated scope is created. | | AccessModifier | DataLoaderAccessModifier | Default | Controls generated code visibility. For \[DataLoader\], Public and Internal affect only the generated class and T keeps its declared accessibility. An explicit method-level PublicInterface acts as Public and produces HC0128; a \[DataLoaderDefaults\] PublicInterface default makes the generated class internal and produces no HC0128. For \[DataLoader\], Public and Internal make both class and interface public or internal; PublicInterface makes the interface public and the class internal. Default defers to \[DataLoaderDefaults\]; without one, both are public. | | Lookups | string\[\] | None | Names of methods on the same class that derive additional cache keys from loaded values, so an entity fetched by one key can be resolved from the cache by another. | Note `Name` is a positional constructor argument, not a settable property. `[DataLoader(Name = "X")]` does not compile; use `[DataLoader("X")]`. For `[DataLoader]`, assembly-level `DataLoaderDefaults` values for `ServiceScope`, `AccessModifier`, and registration generation still apply. `GenerateInterfaces` is ignored because the interface is supplied by `T`. #### MaxBatchSize Use `MaxBatchSize` to protect downstream systems from unbounded batch sizes (SQL parameter limits, HTTP request size limits): C# ``` [DataLoader(MaxBatchSize = 2)] public static Task> GetValueByKeyAsync( IReadOnlyList keys, CancellationToken cancellationToken) { IReadOnlyDictionary result = keys.ToDictionary(k => k, k => k.ToString()); return Task.FromResult(result); } ``` Loading five keys through this DataLoader produces three fetch calls with 2, 2, and 1 keys, and the results are still assembled transparently for all callers. Split batches are dispatched concurrently, not one after another. #### ServiceScope C# ``` [DataLoader(ServiceScope = DataLoaderServiceScope.DataLoaderScope)] public static async Task> GetBrandByIdAsync( IReadOnlyList ids, CatalogContext db, CancellationToken ct) => await db.Brands .Where(b => ids.Contains(b.Id)) .ToDictionaryAsync(b => b.Id, ct); ``` Use `DataLoaderServiceScope.DataLoaderScope` when your data access service (like a `DbContext`) is registered as scoped and you want the DataLoader to have its own scope, separate from the request scope. This prevents lifetime conflicts when the DataLoader outlives a single resolver. When `ServiceScope` is not set and no `[DataLoaderDefaults]` assembly attribute overrides it, the generator already creates a dedicated scope, so setting `DataLoaderScope` makes that choice explicit. Use `OriginalScope` to resolve services from the request scope instead. ### How Execution Works When a resolver calls `LoadAsync`, no data source call happens yet: the key is added to the DataLoader's current batch and the resolver receives a task. The execution engine keeps running other resolvers, and once no more resolver work is immediately ready, each pending batch is dispatched as a single call to its fetch method with all collected keys. ``` Resolve products(first: 5) → [Product1 .. Product5] brand resolvers call LoadAsync → keys accumulate: [1, 2, 1, 3, 2] no more resolver work ready → dispatch: one fetch call with keys [1, 2, 3] brand resolvers resume → each receives its Brand from the result ``` Sibling resolvers that use the same DataLoader within one execution pass are combined into a single batch. The dispatch trigger is "no more ready work", not a fixed schedule and not one dispatch per level of the query. DataLoaders also deduplicate keys. If two products share the same brand ID, the DataLoader sends that ID once and returns the same result to both resolvers. In the example above, five products referencing three distinct brands produce exactly one fetch call with three keys. ### Data Consistency A DataLoader caches results for the duration of a single GraphQL request. If the same key is requested multiple times within one request, the DataLoader returns the cached result without hitting the data source again, even across different fields and depths of the query. This guarantees that all resolvers within a request see the same data for the same key. Both the DataLoader instance and its cache live for exactly one request (or one subscription event). Nothing is cached across requests, and no data can leak between requests or users. ## Manual DataLoader Classes You can write DataLoader classes by hand when you need full control over the batching logic. This is rarely needed since the source generator covers most cases. C# ``` public class BrandByIdDataLoader : BatchDataLoader { private readonly IServiceProvider _services; public BrandByIdDataLoader( IServiceProvider services, IBatchScheduler batchScheduler, DataLoaderOptions options) : base(batchScheduler, options) { _services = services; } protected override async Task> LoadBatchAsync( IReadOnlyList keys, CancellationToken ct) { await using var scope = _services.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); return await db.Brands .Where(b => keys.Contains(b.Id)) .ToDictionaryAsync(b => b.Id, ct); } } ``` Warning The `IReadOnlyList` passed to `LoadBatchAsync` and to source-generated methods is only valid for the duration of the fetch call. Do not store a reference to it or use it after the method completes; copy the keys if you need them longer. ## Next Steps - **Need to batch a single field for many parents?** See [Batch Resolvers](https://chillicream.com/docs/hotchocolate/fetching-data/batching/batch-resolver). - **Need to understand resolver basics?** See [Resolvers](https://chillicream.com/docs/hotchocolate/resolvers). - **Need pagination?** See [Pagination](https://chillicream.com/docs/hotchocolate/fetching-data/pagination) for cursor-based connections. - **Using Entity Framework?** See [Entity Framework](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework) for integration patterns with DataLoaders. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/batching/dataloader.md) Maintained by ChilliCream. Last updated on **August 30, 2026** by **Michael Staib** --- # GraphQL Filtering in Hot Chocolate > Generate filter input types from .NET models with the [UseFiltering] attribute in Hot Chocolate, translating client filters into native database queries. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/filtering Hot Chocolate generates filter input types from your .NET models and translates client-supplied filter arguments into native database queries. The default implementation builds expression trees that apply to `IQueryable`, but you can customize filters for other data sources. Given a model like `User` with a `Name` property, Hot Chocolate generates a `UserFilterInput` with string operations such as `eq`, `contains`, `startsWith`, and more. Clients use the `where` argument to filter results. ## Getting Started Filtering is part of the `HotChocolate.Data` package. Bash ``` dotnet add package HotChocolate.Data ``` Warning All `HotChocolate.*` packages need to have the same version. Register filtering on the schema: C# ``` builder .AddGraphQL() .AddFiltering(); ``` Apply the `[UseFiltering]` attribute to a resolver that returns `IQueryable` or `IEnumerable`: C# ``` [QueryType] public static partial class UserQueries { [UseFiltering] public static IQueryable GetUsers(CatalogContext db) => db.Users; } ``` Clients can now filter users: GraphQL ``` query { users(where: { name: { contains: "Alice" } }) { name email } } ``` Warning **Middleware order matters.** When combining multiple middleware, apply them in this order: `UsePaging` \> `UseProjection` \> `UseFiltering` \> `UseSorting`. ## Filter Types Hot Chocolate generates filter operations based on the .NET type of each property. ### String Filters Operations: `eq`, `neq`, `contains`, `ncontains`, `in`, `nin`, `startsWith`, `nstartsWith`, `endsWith`, `nendsWith`. ### Boolean Filters Operations: `eq`, `neq`. ### Comparable Filters For numeric types (`int`, `long`, `float`, `double`, `decimal`), `Guid`, `DateTime`, `DateTimeOffset`, and `TimeSpan`. Operations: `eq`, `neq`, `in`, `nin`, `gt`, `ngt`, `gte`, `ngte`, `lt`, `nlt`, `lte`, `nlte`. ### Enum Filters Operations: `eq`, `neq`, `in`, `nin`. ### Object Filters Filters are generated for nested objects, supporting filtering across database relationships: C# ``` public class User { public string Name { get; set; } public Address Address { get; set; } } public class Address { public string City { get; set; } } ``` GraphQL ``` query { users(where: { address: { city: { eq: "Berlin" } } }) { name } } ``` ### List Filters For collection properties, Hot Chocolate generates `all`, `none`, `some`, and `any` operations: GraphQL ``` query { users(where: { orders: { some: { total: { gt: 100 } } } }) { name } } ``` ## Combining Filters with "and" / "or" Every filter input type includes `and` and `or` fields for composing multiple conditions: GraphQL ``` query { users( where: { or: [{ name: { contains: "Alice" } }, { name: { contains: "Bob" } }] } ) { name } } ``` The `or` field must be used at the top level of the filter. Placing it inside a field operation results in `and` semantics instead. ### Removing "and" / "or" Disable these combinators in a custom filter type: C# ``` public class UserFilterType : FilterInputType { protected override void Configure(IFilterInputTypeDescriptor descriptor) { descriptor.AllowAnd(false).AllowOr(false); } } ``` ## Custom Filter Types Customize which fields are filterable and which operations are available by extending `FilterInputType`: C# ``` public class UserFilterType : FilterInputType { protected override void Configure(IFilterInputTypeDescriptor descriptor) { descriptor.BindFieldsExplicitly(); descriptor.Field(f => f.Name); descriptor.Field(f => f.Email).Type(); } } ``` Restrict operations on a field by defining a custom operation type: C# ``` public class CustomStringOperationFilterInputType : StringOperationFilterInputType { protected override void Configure(IFilterInputTypeDescriptor descriptor) { descriptor.Operation(DefaultFilterOperations.Equals).Type(); descriptor.Operation(DefaultFilterOperations.Contains).Type(); } } ``` Apply the custom filter type: C# ``` [QueryType] public static partial class UserQueries { [UseFiltering(typeof(UserFilterType))] public static IQueryable GetUsers(CatalogContext db) => db.Users; } ``` ## Filter Conventions Filter conventions let you change filtering behavior globally across your schema. ### Setting Up a Convention Extend `FilterConvention` and override `Configure`, or use `FilterConventionExtension` to build on top of the defaults: C# ``` public class CustomFilterConvention : FilterConvention { protected override void Configure(IFilterConventionDescriptor descriptor) { descriptor.AddDefaults(); descriptor.ArgumentName("filter"); } } ``` C# ``` builder .AddGraphQL() .AddConvention(); ``` ### Binding Filter Types Globally Bind custom filter types to .NET types through the convention: C# ``` public class CustomFilterConvention : FilterConvention { protected override void Configure(IFilterConventionDescriptor descriptor) { descriptor.AddDefaults(); descriptor.BindRuntimeType(); } } ``` ### Custom Scalar Filters When you use custom scalars (including those from `HotChocolate.Types.Scalars`), you must create and bind a filter type for each scalar: C# ``` public class EmailAddressOperationFilterInputType : FilterInputType { protected override void Configure(IFilterInputTypeDescriptor descriptor) { descriptor.Operation(DefaultFilterOperations.Equals).Type(); descriptor.Operation(DefaultFilterOperations.NotEquals).Type(); descriptor.Operation(DefaultFilterOperations.Contains).Type(); } } ``` C# ``` builder .AddGraphQL() .AddFiltering(x => x .AddDefaults() .BindRuntimeType()); ``` ## Next Steps - **Need to sort results?** See [Sorting](https://chillicream.com/docs/hotchocolate/fetching-data/sorting). - **Need to page through results?** See [Pagination](https://chillicream.com/docs/hotchocolate/fetching-data/pagination). - **Need to optimize database queries?** See [Projections](https://chillicream.com/docs/hotchocolate/fetching-data/projections). - **Need to protect against expensive filter queries?** See [Cost Analysis](https://chillicream.com/docs/hotchocolate/security/cost-analysis). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/filtering.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Integrations - Hot Chocolate > Learn how to use the IExecutable interface to abstract data sources in Hot Chocolate. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/integrations The `IExecutable` and `IExecutable` interfaces abstract data sources in Hot Chocolate. Your data or domain layer can wrap a data source in an executable and pass it to the GraphQL layer. A resolver that returns `IExecutable` is recognized as a list. C# ``` public class User { public string Name { get; } } public interface IUserRepository { public IExecutable FindAll(); } public class Query { public IExecutable GetUsers(IUserRepository repo) => repo.FindAll(); } ``` SDL ``` type Query { users: [User!]! } ``` This abstraction completely decouples the GraphQL layer from database-specific knowledge. Filtering, sorting, and projections can pick up the executable and apply logic to it. A database-specific provider is still needed for these features, but it is opaque to the GraphQL layer. The execution engine calls `ToListAsync`, `FirstOrDefaultAsync`, or `SingleOrDefaultAsync` on the executable. The executable runs these operations in the most efficient way for the database. ## API ### Source C# ``` object Source { get; } ``` The `Source` property holds the current state of the executable. For Entity Framework, this holds the `IQueryable`. For MongoDB, it is the `DbSet` or `IAggregateFluent`. `Source` is read-only. If you have a custom `IExecutable` implementation and need to change the source, create a method that returns a new executable with the new source. ### ToListAsync C# ``` ValueTask ToListAsync(CancellationToken cancellationToken); ``` Returns a list of items. ### FirstOrDefaultAsync C# ``` ValueTask FirstOrDefaultAsync(CancellationToken cancellationToken); ``` Returns the first element of the sequence, or a default value if the sequence contains no elements. ### SingleOrDefaultAsync C# ``` ValueTask SingleOrDefaultAsync(CancellationToken cancellationToken); ``` Returns the only element of the sequence, or a default value if no element exists. Throws an exception if more than one element satisfies the condition. ### Print C# ``` string Print(); ``` Prints the executable in its current state. This is useful for debugging and logging the generated query. ## Example The following shows the Entity Framework implementation: C# ``` public class EntityFrameworkExecutable : QueryableExecutable { public IQueryable Source { get; } object IExecutable.Source => Source; public EntityFrameworkExecutable(IQueryable queryable) : base(queryable) { } public QueryableExecutable WithSource(IQueryable source) { return new QueryableExecutable(source); } public override async ValueTask ToListAsync( CancellationToken cancellationToken) => await Source.ToListAsync(cancellationToken).ConfigureAwait(false); public override async ValueTask FirstOrDefaultAsync( CancellationToken cancellationToken) => await Source.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); public override async ValueTask SingleOrDefaultAsync( CancellationToken cancellationToken) => await Source.SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); public override string Print() => Source.ToQueryString(); } ``` ## Next Steps - [Entity Framework integration](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework) for EF Core executables - [MongoDB integration](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/mongodb) for MongoDB executables - [Filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering) for applying filters to executables [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/integrations/index.md) Maintained by ChilliCream. Last updated on **July 01, 2026** by **Tobias Tengler** --- # Entity Framework Core - Hot Chocolate > Learn how to integrate Entity Framework Core with Hot Chocolate, including DbContext injection and factory patterns. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework [Entity Framework Core](https://docs.microsoft.com/ef/core/) is a powerful object-relational mapping framework that has become a staple when working with SQL-based databases in .NET applications. ## Resolver Injection of a DbContext When using the [default scope](https://chillicream.com/docs/hotchocolate/resolvers/dependency-injection#default-scope) for queries, each resolver that accepts a scoped `DbContext` receives a **separate** instance. This avoids [threading issues](https://learn.microsoft.com/en-gb/ef/core/dbcontext-configuration/#avoiding-dbcontext-threading-issues). C# ``` public static async Task GetBookByIdAsync( ApplicationDbContext dbContext) => // ... ``` When using the [default scope](https://chillicream.com/docs/hotchocolate/resolvers/dependency-injection#default-scope) for mutations, each mutation resolver that accepts a scoped `DbContext` receives the **same** request-scoped instance, as mutations execute sequentially. C# ``` public static async Task AddBookAsync( AddBookInput input, AppDbContext dbContext) => // ... ``` See the [Dependency Injection](https://chillicream.com/docs/hotchocolate/resolvers/dependency-injection) documentation for more details. Warning Changing the default scope for queries will likely result in the error "A second operation started on this context before a previous operation completed", because Entity Framework Core does not support multiple parallel operations on the same `DbContext` instance. ## Using a DbContext Factory To use a `DbContext` factory, register your `DbContext` with Hot Chocolate. Install the additional package: Bash ``` dotnet add package HotChocolate.Data.EntityFramework ``` Warning All `HotChocolate.*` packages need to have the same version. Call the `RegisterDbContextFactory` method on the `IRequestExecutorBuilder`. The Hot Chocolate resolver compiler then takes care of injecting your `DbContext` instance into resolvers. C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddDbContextFactory( options => options.UseSqlServer("YOUR_CONNECTION_STRING")); // ... or AddPooledDbContextFactory. builder .AddGraphQL() .RegisterDbContextFactory() .AddTypes(); ``` C# ``` [QueryType] public static class Query { public static async Task GetBookByIdAsync( Guid id, ApplicationDbContext dbContext) { return await dbContext.Books.FindAsync(id); } } ``` Warning You still need to add your `DbContextFactory` to the dependency injection container by calling `AddDbContextFactory` or `AddPooledDbContextFactory`. `RegisterDbContextFactory` on its own is not enough. ## Working with a DbContext Factory When you use a `DbContext` factory, you need to access the `DbContext` differently outside of direct resolver injection. ### DataLoaders When creating DataLoaders that need access to your `DbContext`, inject the `IDbContextFactory` through the constructor. Create and dispose the `DbContext` within the `LoadBatchAsync` method. C# ``` public sealed class BookByIdDataLoader : BatchDataLoader { private readonly IDbContextFactory _dbContextFactory; public BookByIdDataLoader( IDbContextFactory dbContextFactory, IBatchScheduler batchScheduler, DataLoaderOptions options) : base(batchScheduler, options) { _dbContextFactory = dbContextFactory; } protected override async Task> LoadBatchAsync( IReadOnlyList keys, CancellationToken cancellationToken) { using AppDbContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.Books .Where(b => keys.Contains(b.Id)) .ToDictionaryAsync(b => b.Id, cancellationToken); } } ``` Warning Dispose the `DbContext` after use. The example above uses the `using` statement for this purpose. ### Services Services that need a `DbContext` should inject `IDbContextFactory` instead of the `DbContext` directly. C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContextFactory( options => options.UseSqlServer("YOUR_CONNECTION_STRING")); builder.Services.AddScoped(); builder .AddGraphQL() .AddTypes(); ``` C# ``` public sealed class BookService : IAsyncDisposable { private readonly ApplicationDbContext _dbContext; public BookService( IDbContextFactory dbContextFactory) { _dbContext = dbContextFactory.CreateDbContext(); } public async Task GetBookAsync(Guid id) { return await _dbContext.Books.FindAsync(id); } public ValueTask DisposeAsync() { return _dbContext.DisposeAsync(); } } ``` C# ``` [QueryType] public static class Query { public static async Task GetBookByIdAsync( Guid id, BookService bookService) { return await bookService.GetBookAsync(id); } } ``` Warning Dispose the `DbContext` when the service is disposed. The example above implements `IAsyncDisposable` and disposes the `DbContext` in `DisposeAsync`. ## Next Steps - [Dependency Injection](https://chillicream.com/docs/hotchocolate/resolvers/dependency-injection) for DI scope configuration - [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader) for batching patterns - [Filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering) for applying filters to EF Core queries [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/integrations/entity-framework.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Extending Filtering - Hot Chocolate > Learn how to extend the filtering system in Hot Chocolate with custom conventions, providers, and field handlers. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/integrations/extending-filtering The `HotChocolate.Data` package works with all databases that support `IQueryable`. The default settings include all filter operations that work over `IQueryable` on all databases. In some cases, this is not enough. Some databases might not support `IQueryable`. Others may have technology-specific operations (e.g., SQL `LIKE`). The filtering system is designed with extensibility in mind. Filtering can be broken down into two parts: schema building and execution. During schema building, filter input types are created. During execution, user-provided data is analyzed and translated to a database query. Both parts are configured through a convention. You are free to design the structure of filters as it suits you best. Typically, you divide the structure into two parts: the _field_ and the _operation_. The query below returns all movies where the franchise equals "Star Wars". The _field_ is `franchise` and the _operation_ is equals (`eq`): GraphQL ``` { movies(where: { franchise: { eq: "Star Wars" } }) { name } } ``` Fields can form paths. The following query has two _fields_ (`genre` and `totalMovieCount`) and one operation (`eq`): GraphQL ``` { movies(where: { genre: { totalMovieCount: { eq: 100 } } }) { name } } ``` A field is always context-specific. Even when two fields share the same name (like `description` on a movie and `description` on a genre), they have different meanings. An operation, on the other hand, always has the same meaning. The equals operation (`eq`) always means the field value should equal the provided value. Operations can be applied in different contexts, but the operation itself stays the same. There should be only one operation that checks for equality, and it should always have the same name. ## How Everything Fits Together At the core of the configuration API sits a convention. The convention holds the entire configuration that filtering needs to create filter types and translate them to the database. During schema creation, the schema builder asks the convention how the schema should look. The convention defines the names, descriptions, and types used for properties. The convention also defines which provider should translate a GraphQL query to a database query. The provider is the only component used after the schema is built. Every field or operation in a filter type has a handler annotated. During schema initialization, these handlers are bound to the GraphQL fields. The provider specifies which handler should be bound to which field. During execution, the provider visits the incoming value node and executes the handler on the fields. This loose coupling allows defining the provider independently of the convention. ## Filter Convention A filter convention is a .NET class that implements `IFilterConvention`. Instead of writing a convention from scratch, extend the `FilterConvention` base class. This convention is configurable through a fluent interface, so in most cases you can use the descriptor API. ### Descriptor Most descriptor capabilities are documented under [Filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering). Read the parts about `FilterConventions` there first. Two features on the descriptor are specific to extensibility: #### Operation C# ``` IFilterOperationConventionDescriptor Operation(int operationId); ``` Operations are configured globally. Each operation has a unique identifier. You can find the built-in identifiers in `DefaultFilterOperations`. This identifier is used in `FilterInputType` to bind operations on a type. Filter operations are configurable through a fluent interface where you specify the name and description. This configuration applies to all operation fields across all `FilterInputType` definitions. C# ``` conventionDescriptor .Operation(DefaultFilterOperations.Equals) .Name("equals") .Description("Compares the value of the input to the value of the field"); ``` With this configuration, all equals operations are now named `equals` (instead of `eq`) and have a description. To create your own operations, choose an identifier higher than 1024 to avoid collisions with the framework. Store the identifier on a class for reference: C# ``` public static class CustomOperations { public const int Like = 1025; } public static class CustomerFilterConventionExtensions { public static IFilterConventionDescriptor AddInvariantComparison( this IFilterConventionDescriptor conventionDescriptor) => conventionDescriptor .Operation(CustomOperations.Like) .Name("like"); } ``` To apply this configuration to operation types, use the `Configure` method: C# ``` conventionDescriptor.Configure( x => x.Operation(CustomOperations.Like)) ``` #### Provider C# ``` IFilterConventionDescriptor Provider() where TProvider : class, IFilterProvider; IFilterConventionDescriptor Provider(TProvider provider) where TProvider : class, IFilterProvider; IFilterConventionDescriptor Provider(Type provider); ``` You configure the provider on the convention. More details on providers appear later in this page. C# ``` conventionDescriptor.Provider(); ``` ### Custom Conventions Most of the time the descriptor API should satisfy your needs. Building extensions based on the descriptor API is recommended over creating a custom convention. However, if you need full control over naming and type creation, override the methods on `FilterConvention`: C# ``` public class CustomConvention : FilterConvention { protected override void Configure(IFilterConventionDescriptor descriptor) { descriptor.AddDefaults(); } public override NameString GetTypeName(Type runtimeType) => base.GetTypeName(runtimeType) + "Suffix"; } ``` ## Providers Like the convention, a provider is configured through a fluent interface. Every filter field or operation has a specific handler. The handler translates the operation to the database. Handlers are stored on the provider. After schema initialization, an interceptor visits the filter types and requests a handler from the provider, which is then annotated on the field. The provider translates an incoming query into a database query by traversing an input object and executing the handlers on the fields. The output is always some kind of _filter definition_. For `IQueryable`, this is an expression. For MongoDB, this is a `FilterDefinition`. To inspect and analyze the input object, the provider uses a visitor. ### Provider Descriptor The provider descriptor has one method: C# ``` IFilterProviderDescriptor AddFieldHandler() where TFieldHandler : IFilterFieldHandler; ``` Use this method to register field handlers on the provider. ### Field Handler Every field or operation is annotated with an instance of `FilterFieldHandler`. When the provider looks for a handler for a field, it iterates through the list of registered handlers and calls `CanHandle`. The first handler that can handle the field is annotated on it. During visitation, the visitor calls `TryHandleEnter` when it enters an input field and `TryHandleLeave` when it leaves. > A field handler supports constructor injection and is a singleton. Do not store state on the field handler. Use the visitor context for state management. #### CanHandle C# ``` bool CanHandle( ITypeCompletionContext context, IFilterInputTypeDefinition typeDefinition, IFilterFieldDefinition fieldDefinition); ``` Tests whether this handler can handle a field. If it can, it is attached to the field. #### TryHandleEnter C# ``` bool TryHandleEnter( TContext context, IFilterField field, ObjectFieldNode node, [NotNullWhen(true)] out ISyntaxVisitorAction? action); ``` Called when the visitor encounters a field. The parameters are: - `context` \-- the visitor context - `field` \-- the field instance being visited - `node` \-- the field node from the input object (`node.Value` contains the value) - `action` \-- if the method returns `true`, this action controls further visitor processing #### TryHandleLeave C# ``` bool TryHandleLeave( TContext context, IFilterField field, ObjectFieldNode node, [NotNullWhen(true)] out ISyntaxVisitorAction? action); ``` Called when the visitor leaves the field it previously entered. ### Filter Operation Handlers `FilterOperationHandler` is a more specific abstraction for handling operations. Override `TryHandleOperation` to handle operations. ### The Context The visitor and field handlers are singletons, so a context object is passed along during traversal. Handlers can push data onto this context for other handlers further down the tree. The context contains `Types`, `Operations`, `Errors`, and `Scopes`. What data you store is provider-specific. The `IQueryable` provider also contains `RuntimeTypes` and knows whether the source is `InMemory` or a database call. `Scopes` allow adding multiple logical layers to a context. For `IQueryable`, this is needed whenever a new closure starts: C# ``` // /------------------------ SCOPE 1 -----------------------------\ // /----------- SCOPE 2 -------------\ users.Where(x => x.Company.Addresses.Any(y => y.Street == "221B Baker Street")) ``` ## Extending IQueryable The default filtering implementation uses `IQueryable` under the hood. You can customize query translation by registering handlers on the `QueryableFilterProvider`. The following example creates a `StringOperationHandler` that supports case-insensitive filtering: C# ``` public class QueryableStringInvariantEqualsHandler : QueryableStringOperationHandler { public QueryableStringInvariantEqualsHandler(InputParser inputParser) : base(inputParser) { } private static readonly MethodInfo s_toLower = typeof(string) .GetMethods() .Single( x => x.Name == nameof(string.ToLower) && x.GetParameters().Length == 0); protected override int Operation => DefaultFilterOperations.Equals; public override Expression HandleOperation( QueryableFilterContext context, IFilterOperationField field, IValueNode value, object parsedValue) { Expression property = context.GetInstance(); if (parsedValue is string str) { return Expression.Equal( Expression.Call(property, s_toLower), Expression.Constant(str.ToLower())); } throw new InvalidOperationException(); } } ``` Register this handler on a custom convention: C# ``` public class CustomFilteringConvention : FilterConvention { protected override void Configure(IFilterConventionDescriptor descriptor) { descriptor.AddDefaults(); descriptor.Provider( new QueryableFilterProvider( x => x .AddFieldHandler(ctx => new QueryableStringInvariantEqualsHandler(ctx.InputParser)) .AddDefaultFieldHandlers())); } } builder .AddGraphQL() .AddFiltering(); ``` Warning Register custom handlers **before** `AddDefaultFieldHandlers()`. If a default handler covers the same operation (for example `eq` on strings), the one registered first wins, and your custom handler will be silently ignored. You can also use convention and provider extensions instead of creating a custom `FilterConvention`: C# ``` builder .AddGraphQL() .AddFiltering() .AddConvention( new FilterConventionExtension( x => x.AddProviderExtension( new QueryableFilterProviderExtension( y => y.AddFieldHandler(ctx => new QueryableStringInvariantEqualsHandler(ctx.InputParser)))))); ``` ## Next Steps - [Filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering) for using built-in filtering - [MongoDB integration](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/mongodb) for MongoDB-specific filtering [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/integrations/extending-filtering.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Marten - Hot Chocolate > Learn how to integrate Marten with Hot Chocolate for filtering, sorting, projections, and pagination. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/integrations/marten The `HotChocolate.Data` package generally works with any LINQ provider that provides an `IQueryable`. However, Marten requires special handling. Pagination and projections work out of the box, but filtering and sorting need LINQ expressions translated into a format that the Marten LINQ provider can process. This integration provides custom configurations for that purpose. You can find a sample project in [Hot Chocolate Examples](https://github.com/ChilliCream/hotchocolate-examples/tree/master/misc/MartenDB). ## Get Started Install the `HotChocolate.Data.Marten` package: Bash ``` dotnet add package HotChocolate.Data.Marten ``` Warning All `HotChocolate.*` packages need to have the same version. ## Filtering Register the Marten filtering convention on the schema builder: C# ``` builder .AddGraphQL() .AddQueryType() .AddMartenFiltering(); ``` [Learn more about filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering). ## Sorting Register the Marten sorting convention on the schema builder: C# ``` builder .AddGraphQL() .AddQueryType() .AddMartenSorting(); ``` [Learn more about sorting](https://chillicream.com/docs/hotchocolate/fetching-data/sorting). ## Projections Projections work out of the box with Marten. No custom configuration is needed. [Learn more about projections](https://chillicream.com/docs/hotchocolate/fetching-data/projections). ## Paging Pagination works out of the box with Marten. No custom configuration is needed. [Learn more about pagination](https://chillicream.com/docs/hotchocolate/fetching-data/pagination). ## Next Steps - [Filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering) for filtering concepts - [Sorting](https://chillicream.com/docs/hotchocolate/fetching-data/sorting) for sorting concepts - [Pagination](https://chillicream.com/docs/hotchocolate/fetching-data/pagination) for pagination setup [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/integrations/marten.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # MongoDB - Hot Chocolate > Learn how to integrate MongoDB with Hot Chocolate, including filtering, sorting, projections, and pagination. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/integrations/mongodb Hot Chocolate has a data integration for MongoDB. With this integration, you can translate paging, filtering, sorting, and projections directly into native MongoDB queries. You can find an example project in [Hot Chocolate Examples](https://github.com/ChilliCream/hotchocolate-examples/tree/master/misc/MongoDB). ## Get Started Install the `HotChocolate.Data.MongoDb` package: Bash ``` dotnet add package HotChocolate.Data.MongoDb ``` Warning All `HotChocolate.*` packages need to have the same version. ## MongoExecutable The integration builds around `IExecutable`. The `AsExecutable` extension method is available on `IMongoCollection`, `IAggregateFluent`, and `IFindFluent`. The execution engine picks up the `IExecutable` and executes it efficiently. You can use any aggregation or find pipeline before calling `AsExecutable`. C# ``` [UsePaging] [UseProjection] [UseSorting] [UseFiltering] public IExecutable GetPersons(IMongoCollection collection) { return collection.AsExecutable(); } [UseFirstOrDefault] public IExecutable GetPersonById( IMongoCollection collection, Guid id) { return collection.Find(x => x.Id == id).AsExecutable(); } ``` ## Filtering Register the MongoDB filtering convention on the schema builder: C# ``` builder .AddGraphQL() .AddQueryType() .AddMongoDbFiltering(); ``` > To use MongoDB filtering alongside `IQueryable`/`IEnumerable`, register the MongoDB convention under a different scope: `AddMongoDbFiltering("yourScope")`. Then specify the scope on each resolver: `[UseFiltering(Scope = "yourScope")]`. Filters are converted to `BsonDocument`s and applied to the executable. _GraphQL Query:_ GraphQL ``` query GetPersons { persons( where: { name: { eq: "Yorker Shorton" } addresses: { some: { street: { eq: "04 Leroy Trail" } } } } ) { name addresses { street city } } } ``` _Mongo Query:_ JSON ``` { "find": "person", "filter": { "Name": { "$eq": "Yorker Shorton" }, "Addresses": { "$elemMatch": { "Street": { "$eq": "04 Leroy Trail" } } } } } ``` ## Sorting Register the MongoDB sorting convention on the schema builder: C# ``` builder .AddGraphQL() .AddQueryType() .AddMongoDbSorting(); ``` > To use MongoDB sorting alongside `IQueryable`/`IEnumerable`, register the MongoDB convention under a different scope: `AddMongoDbSorting("yourScope")`. Then specify the scope on each resolver: `[UseSorting(Scope = "yourScope")]`. Sorting is converted to `BsonDocument`s and applied to the executable. _GraphQL Query:_ GraphQL ``` query GetPersons { persons(order: [{ name: ASC }, { mainAddress: { city: DESC } }]) { name addresses { street city } } } ``` _Mongo Query:_ JSON ``` { "find": "person", "filter": {}, "sort": { "Name": 1, "MainAddress.City": -1 } } ``` ## Projections Register the MongoDB projection convention on the schema builder: C# ``` builder .AddGraphQL() .AddQueryType() .AddMongoDbProjections(); ``` > To use MongoDB projections alongside `IQueryable`/`IEnumerable`, register the MongoDB convention under a different scope: `AddMongoDbProjections("yourScope")`. Then specify the scope on each resolver: `[UseProjection(Scope = "yourScope")]`. Projections do not always improve performance. Even though MongoDB processes and transfers less data, projections can sometimes harm query performance. See [this article by Tek Loon](https://betterprogramming.pub/improve-mongodb-performance-using-projection-c08c38334269) for guidance on when to use them. _GraphQL Query:_ GraphQL ``` query GetPersons { persons { name addresses { city } } } ``` _Mongo Query:_ JSON ``` { "find": "person", "filter": {}, "projection": { "Addresses.City": 1, "Name": 1 } } ``` ## Paging Register the MongoDB-specific pagination providers: C# ``` builder .AddGraphQL() .AddMongoDbPagingProviders(); ``` [Learn more about pagination providers](https://chillicream.com/docs/hotchocolate/fetching-data/pagination#providers) ### Cursor Pagination Annotate your resolver with `[UsePaging]` or `.UsePaging()` to use cursor-based pagination: C# ``` [UsePaging] public IExecutable GetPersons(IMongoCollection collection) { return collection.AsExecutable(); } ``` Example query: GraphQL ``` query GetPersons { persons(first: 50, after: "OTk=") { nodes { name addresses { city } } pageInfo { endCursor hasNextPage hasPreviousPage startCursor } } } ``` ## FirstOrDefault / SingleOrDefault To return a single object from a collection, use the `UseFirstOrDefault` or `UseSingleOrDefault` middleware. Hot Chocolate rewrites the field type from a list to an object type. C# ``` [UseFirstOrDefault] public IExecutable GetPersonById( IMongoCollection collection, Guid id) { return collection.Find(x => x.Id == id).AsExecutable(); } ``` ## Next Steps - [Pagination](https://chillicream.com/docs/hotchocolate/fetching-data/pagination) for pagination setup - [Filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering) for filtering concepts - [Executable](https://chillicream.com/docs/hotchocolate/fetching-data/integrations) for the `IExecutable` abstraction [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/integrations/mongodb.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # GraphQL Cursor Pagination in Hot Chocolate > Implement cursor-based connection pagination in Hot Chocolate with [UseConnection] and PagingArguments, following the GraphQL Cursor Connections spec. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/pagination When a dataset is too large to return in a single response, you need pagination. Hot Chocolate implements cursor-based connection pagination following the [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). Connections give clients a standardized way to traverse pages using opaque cursors. GraphQL models data as a graph of related entities. When one entity relates to a list of other entities, that relationship is called a _connection_. A `UsersConnection` for instance represents the connection between `Query` and `User`. Each _edge_ in that connection links one `User` to the parent, and carries a cursor that marks the user's position in the list. This is more than just naming. Traditional offset pagination (`skip: 20, take: 10`) breaks when data changes between pages: inserts and deletes shift items, causing duplicates or gaps. Cursors avoid this because they point to a stable position rather than a numeric offset. The database can seek directly to the cursor position, which also means pagination performance stays constant regardless of how deep into the list the client navigates. ## How Connections Work Instead of returning a flat list, a paginated field returns a Connection. The connection wraps the data with page metadata, cursors for navigation and optionally aggregations. GraphQL ``` type Query { users(first: Int, after: String, last: Int, before: String): UsersConnection } type UsersConnection { pageInfo: PageInfo! edges: [UsersEdge!] nodes: [User!] } type UsersEdge { cursor: String! node: User! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } ``` Clients use `first`/`after` to page forward and `last`/`before` to page backward. Each edge carries a cursor that points to its position in the dataset. ## Adding Pagination To use pagination register the paging arguments with the GraphQL builder. C# ``` builder .AddGraphQL() .AddPagingArguments(); ``` Hot Chocolate by default builds on top of the `Page` which describes a single page in a dataset. A page can be used to construct a `PageConnection`. C# ``` [QueryType] public static partial class UserQueries { public static async Task> GetUsersAsync( PagingArguments pagingArgs, CatalogContext db, CancellationToken cancellationToken) => await db.Users.OrderBy(u => u.Id).ToPageAsync(pagingArgs, cancellationToken); } ``` The `ToPageAsync` extension method is located in one of the following packages: - GreenDonut.Data.EntityFramework - GreenDonut.Data.Raven - GreenDonut.Data.Marten - GreenDonut.Data.Mongo ## Pagination Options You can configure pagination behavior per field or globally. ### Per-Field Options C# ``` [QueryType] public static partial class UserQueries { [UseConnection(MaxPageSize = 100, DefaultPageSize = 25, IncludeTotalCount = true)] public static async Task> GetUsersAsync( PagingArguments pagingArgs, CatalogContext db, CancellationToken cancellationToken) => await db.Users.OrderBy(u => u.Id).ToPageAsync(pagingArgs, cancellationToken); } ``` ### Global Defaults Apply consistent pagination settings across your entire schema: C# ``` builder .AddGraphQL() .ModifyPagingOptions(opt => { opt.MaxPageSize = 100; opt.DefaultPageSize = 25; opt.IncludeTotalCount = true; }); ``` ### All PagingOptions | Property | Default | Description | | ---------------------------- | ----------- | ----------------------------------------------------------------------------------- | | MaxPageSize | 50 | Maximum number of items a client can request via first or last. | | DefaultPageSize | 10 | Number of items returned if the client does not specify first or last. | | IncludeTotalCount | false | Adds a totalCount field to the Connection. | | AllowBackwardPagination | true | Includes before and last arguments on the Connection. | | RequirePagingBoundaries | false | Requires the client to specify first or last. | | InferConnectionNameFromField | true | Infers the Connection name from the field name instead of the return type. | | ProviderName | null | Name of the pagination provider to use. | | NullOrdering | Unspecified | Controls how null values are ordered when a nullable field is used as a cursor key. | ## MaxPageSize and Cost Analysis The `MaxPageSize` setting works together with [cost analysis](https://chillicream.com/docs/hotchocolate/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. For public APIs, keep `MaxPageSize` conservative and use `RequirePagingBoundaries = true` to force clients to declare how many items they want. ## Connection Naming The Connection and Edge type names are inferred from the field name by default. A field called `users` generates `UsersConnection` and `UsersEdge`. Override the name with `ConnectionName`: C# ``` [QueryType] public static partial class UserQueries { [UseConnection(ConnectionName = "TeamMembers")] public static async Task> GetUsersAsync( PagingArguments pagingArgs, CatalogContext db, CancellationToken cancellationToken) => await db.Users.OrderBy(u => u.Id).ToPageAsync(pagingArgs, cancellationToken); } ``` This produces `TeamMembersConnection` and `TeamMembersEdge`. ## Total Count Enable the `totalCount` field to let clients request the total number of items in the dataset: C# ``` [QueryType] public static partial class UserQueries { [UseConnection(IncludeTotalCount = true)] public static async Task> GetUsersAsync( PagingArguments pagingArgs, CatalogContext db, CancellationToken cancellationToken) => await db.Users.OrderBy(u => u.Id).ToPageAsync(pagingArgs, cancellationToken); } ``` ## Relative Cursors Cursor-based pagination is great for infinite scrolling, but many applications need a traditional page bar that lets users jump to a specific page (e.g. "1 2 3 ... 10"). Relative cursors bridge this gap. They let you request cursors for surrounding pages so the frontend can render a page bar while still using cursor-based navigation under the hood. ``` [1] 2 3 4 5 ... 10 ↑ ↑ ↑ ↑ forward cursors ``` When a client requests `forwardCursors` or `backwardCursors` inside `pageInfo`, Hot Chocolate returns a list of `PageCursor` objects, each containing a `page` number and the opaque `cursor` to navigate there. The frontend can render these directly as page links. Enable relative cursors on a field with `EnableRelativeCursors`: C# ``` [QueryType] public static partial class UserQueries { [UseConnection(EnableRelativeCursors = true)] public static async Task> GetUsersAsync( PagingArguments pagingArgs, CatalogContext db, CancellationToken cancellationToken) => await db.Users.OrderBy(u => u.Id).ToPageAsync(pagingArgs, cancellationToken); } ``` Clients can then query the relative cursors: GraphQL ``` query { users(first: 10) { nodes { id name } pageInfo { hasNextPage hasPreviousPage forwardCursors { page cursor } backwardCursors { page cursor } } } } ``` The response includes cursors for surrounding pages: JSON ``` { "data": { "users": { "nodes": [ ... ], "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "forwardCursors": [ { "page": 2, "cursor": "ezB8MXw2fTIz" }, { "page": 3, "cursor": "ezF8MXw2fTIz" }, { "page": 4, "cursor": "ezJ8MXw2fTIz" } ], "backwardCursors": [] } } } } ``` To navigate to page 3, the client sends `users(first: 10, after: "ezF8MXw2fTIz")`. By default, up to 5 cursors are returned per direction. You can also enable relative cursors globally: C# ``` builder .AddGraphQL() .ModifyPagingOptions(opt => { opt.EnableRelativeCursors = true; }); ``` > Relative cursors are only available with the implementation-first approach. ## Custom Connection Types ### Extending PageConnection The simplest way to add fields to a connection is to inherit from `PageConnection`. Any public property or method you add becomes a GraphQL field on the connection type. C# ``` public class ProductConnection : PageConnection { private readonly Page _page; public ProductConnection(Page page) : base(page) { _page = page; } public decimal AveragePrice => _page.Average(p => p.Price); } ``` Return the custom connection from your resolver instead of `PageConnection`: C# ``` [QueryType] public static partial class ProductQueries { [UseConnection(IncludeTotalCount = true)] public static async Task GetProductsAsync( PagingArguments pagingArgs, CatalogContext db, CancellationToken cancellationToken) { var page = await db.Products .OrderBy(p => p.Id) .ToPageAsync(pagingArgs, cancellationToken); return new ProductConnection(page); } } ``` ### ConnectionBase for Full Control When you need custom edge types or want to control how edges and page info are constructed, inherit from `ConnectionBase` directly. Start by defining a custom edge. An edge implements `IEdge` and pairs a node with its cursor. C# ``` public class ProductsEdge(Page page, PageEntry entry) : IEdge { public Product Node => entry.Item; object? IEdge.Node => Node; public string Cursor => page.CreateCursor(entry); } ``` Then build the connection around it: C# ``` public class ProductConnection : ConnectionBase { private readonly Page _page; private ConnectionPageInfo? _pageInfo; private ProductsEdge[]? _edges; public ProductConnection(Page page) { _page = page; } public override IReadOnlyList? Edges { get { if (_edges is null) { var entries = _page.Entries; var edges = new ProductsEdge[entries.Length]; for (var i = 0; i < entries.Length; i++) { edges[i] = new ProductsEdge(_page, entries[i]); } _edges = edges; } return _edges; } } public IReadOnlyList? Nodes => _page; public override ConnectionPageInfo PageInfo { get { if (_pageInfo is null) { var startCursor = _page.CreateStartCursor(); var endCursor = _page.CreateEndCursor(); _pageInfo = new ConnectionPageInfo( _page.HasNextPage, _page.HasPreviousPage, startCursor, endCursor); } return _pageInfo; } } public int TotalCount => _page.TotalCount ?? 0; } ``` ### Reusable Generic Connection If multiple entities share the same connection structure, define a generic connection and edge. Use the `[GraphQLName("{0}Connection")]` attribute so Hot Chocolate replaces `{0}` with the entity name (e.g. `CatalogConnection` becomes `BrandConnection`). C# ``` [GraphQLName("{0}Edge")] public class CatalogEdge( Page page, PageEntry entry) : IEdge { public TEntity Node => entry.Item; object? IEdge.Node => Node; public string Cursor => page.CreateCursor(entry); } ``` C# ``` [GraphQLName("{0}Connection")] public class CatalogConnection : ConnectionBase, ConnectionPageInfo> { private readonly Page _page; private ConnectionPageInfo? _pageInfo; private CatalogEdge[]? _edges; public CatalogConnection(Page page) { _page = page; } public override IReadOnlyList> Edges { get { if (_edges is null) { var entries = _page.Entries; var edges = new CatalogEdge[entries.Length]; for (var i = 0; i < entries.Length; i++) { edges[i] = new CatalogEdge(_page, entries[i]); } _edges = edges; } return _edges; } } public IReadOnlyList Nodes => _page; public override ConnectionPageInfo PageInfo { get { if (_pageInfo is null) { var startCursor = _page.CreateStartCursor(); var endCursor = _page.CreateEndCursor(); _pageInfo = new ConnectionPageInfo( _page.HasNextPage, _page.HasPreviousPage, startCursor, endCursor); } return _pageInfo; } } public int TotalCount => _page.TotalCount ?? 0; } ``` ## Nullable Cursor Keys When your cursor key field can be `null`, you must tell Hot Chocolate how the database orders null values so that cursor-based pagination produces correct results across pages. Set `NullOrdering` on `PagingOptions` to match your database: | Value | When to use | | ---------------- | ------------------------------------------------------------------------------ | | Unspecified | Default. The EF Core paging handler auto-detects ordering for known providers. | | NativeNullsFirst | Nulls sort before non-null values (SQL Server, SQLite, in-memory LINQ). | | NativeNullsLast | Nulls sort after non-null values (PostgreSQL default). | C# ``` builder .AddGraphQL() .ModifyPagingOptions(opt => opt.NullOrdering = NullOrdering.NativeNullsLast); ``` When `NullOrdering` is `Unspecified` and the EF Core paging handler is used, ordering is detected automatically for PostgreSQL (`NativeNullsLast`) and SQL Server, SQLite, and in-memory (`NativeNullsFirst`). For unrecognized providers, an error is thrown when nullable cursor keys are present. Set `NullOrdering` explicitly to resolve it. [Learn more about database integrations](https://chillicream.com/docs/hotchocolate/fetching-data/integrations) ## Next Steps - **Need to filter results?** See [Filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering). - **Need to sort results?** See [Sorting](https://chillicream.com/docs/hotchocolate/fetching-data/sorting). - **Need to optimize database queries?** See [Projections](https://chillicream.com/docs/hotchocolate/fetching-data/projections). - **Need to protect against expensive queries?** See [Cost Analysis](https://chillicream.com/docs/hotchocolate/security/cost-analysis). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/pagination.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Projections - Hot Chocolate > Translate GraphQL field selections into optimized database queries with the [UseProjection] attribute in Hot Chocolate, combined with filtering and sorting. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/projections GraphQL clients specify which fields they need. Projections take advantage of this by translating the requested fields directly into optimized database queries. If a client requests only `name` and `email`, Hot Chocolate queries only those columns from the database. GraphQL ``` { users { email address { street } } } ``` SQL ``` SELECT "u"."Email", "a"."Id" IS NOT NULL, "a"."Street" FROM "Users" AS "u" LEFT JOIN "Address" AS "a" ON "u"."AddressId" = "a"."Id" ``` In Hot Chocolate v16, `QueryContext` is the recommended way to apply projections. It combines projection, filtering, and sorting into a single parameter that Hot Chocolate injects into your resolver automatically. You apply it to your `IQueryable` with the `.With()` extension method, giving you full control over your data pipeline. ## Getting Started Projections are part of the `HotChocolate.Data` package. Bash ``` dotnet add package HotChocolate.Data ``` Warning All `HotChocolate.*` packages need to have the same version. Register filtering and sorting on the schema. This also registers `QueryContext` support automatically: C# ``` builder .AddGraphQL() .AddFiltering() .AddSorting(); ``` Add a `QueryContext` parameter to your resolver. Hot Chocolate constructs it at runtime from the GraphQL selection set, filter arguments, and sort arguments: C# ``` [QueryType] public static partial class ProductQueries { [UseFiltering] [UseSorting] public static async Task> GetProductsAsync( PagingArguments pagingArgs, QueryContext query, CatalogContext db, CancellationToken cancellationToken) => await db.Products .With(query) .ToPageAsync(pagingArgs, cancellationToken); } ``` The `[UseFiltering]` and `[UseSorting]` attributes generate the `where` and `order` arguments in the schema. The `QueryContext` parameter receives the projection selector (from the GraphQL selection set), the filter predicate, and the sort definition. Calling `.With(query)` applies all three to the `IQueryable` in the correct order: filter, sort, then project. ## How QueryContext Works `QueryContext` is a simple record with three properties: | Property | Type | Source | | --------- | -------------------------- | ------------------------------------- | | Selector | Expression>? | Built from the GraphQL selection set | | Predicate | Expression>? | Built from \[UseFiltering\] arguments | | Sorting | SortDefinition? | Built from \[UseSorting\] arguments | When you call `.With(queryContext)` on an `IQueryable`, it applies these in order: 1. **Filter** the data with `Where(predicate)` 2. **Sort** the results with `OrderBy(sorting)` 3. **Project** only the requested columns with `Select(selector)` This order is important for query efficiency. Filtering first reduces the dataset, sorting arranges the filtered results, and projecting last ensures only the needed columns are selected. ## Default Sort Order When users don't provide explicit sort arguments, you often want a stable default order (for example, for pagination). The `.With()` method accepts an optional sort modifier: C# ``` public async Task> GetProductsAsync( PagingArguments pagingArgs, QueryContext? query = null, CancellationToken cancellationToken = default) => await context.Products .With(query, DefaultOrder) .ToPageAsync(pagingArgs, cancellationToken); private static SortDefinition DefaultOrder(SortDefinition sort) => sort.IfEmpty(o => o.AddDescending(t => t.Name)).AddAscending(t => t.Id); ``` The `IfEmpty` method applies the default sort only when the client did not provide a sort argument. The `AddAscending(t => t.Id)` call always appends a tiebreaker to ensure stable cursor-based pagination. ## Using QueryContext with Services A common pattern is to pass `QueryContext` from your resolver into a service layer. This keeps your resolvers thin and your data access logic reusable: C# ``` [QueryType] public static partial class ProductQueries { [UseConnection(IncludeTotalCount = true, EnableRelativeCursors = true)] [UseFiltering] [UseSorting] public static async Task GetProductsAsync( PagingArguments pagingArgs, QueryContext query, ProductService productService, CancellationToken cancellationToken) { var page = await productService.GetProductsAsync( pagingArgs, query, cancellationToken); return new ProductConnection(page); } } ``` The service applies `QueryContext` to the EF Core `DbSet`: C# ``` public class ProductService(CatalogContext context) { public async Task> GetProductsAsync( PagingArguments pagingArgs, QueryContext? query = null, CancellationToken cancellationToken = default) => await context.Products .With(query, DefaultOrder) .ToPageAsync(pagingArgs, cancellationToken); private static SortDefinition DefaultOrder( SortDefinition sort) => sort .IfEmpty(o => o.AddDescending(t => t.Name)) .AddAscending(t => t.Id); } ``` Making the `QueryContext` parameter nullable with a default of `null` allows you to call the service from places that don't have a GraphQL context, such as background jobs or unit tests. ## Using QueryContext with DataLoaders `QueryContext` integrates with GreenDonut DataLoaders to enable batched data fetching with projections. Use `.With(query)` on a DataLoader to branch it with the current query context: C# ``` public class ProductService( CatalogContext context, IProductBatchingContext batchingContext) { public async Task GetProductByIdAsync( int id, QueryContext? query = null, CancellationToken cancellationToken = default) => await batchingContext.ProductById .With(query) .LoadAsync(id, cancellationToken); } ``` The DataLoader itself receives `QueryContext` and applies it to the batch query: C# ``` [DataLoaderGroup("ProductBatchingContext")] internal static class ProductDataLoader { [DataLoader] public static async Task> GetProductByIdAsync( IReadOnlyList ids, QueryContext query, CatalogContext context, CancellationToken cancellationToken) { ids = ids.EnsureOrdered(); return await context.Products .Where(t => ids.Contains(t.Id)) .With(query) .ToDictionaryAsync(t => t.Id, cancellationToken); } } ``` For batched collection DataLoaders (for example, loading products by brand), you can combine `QueryContext` with pagination: C# ``` [DataLoader] public static async Task>> GetProductsByBrandAsync( IReadOnlyList brandIds, PagingArguments pagingArgs, QueryContext query, CatalogContext context, CancellationToken cancellationToken) { brandIds = brandIds.EnsureOrdered(); return await context.Products .Where(t => brandIds.Contains(t.BrandId)) .With(query, s => s.AddAscending(t => t.Id)) .ToBatchPageAsync( t => t.BrandId, pagingArgs, cancellationToken); } ``` ## Nested Resolvers In object type resolvers, `QueryContext` is typed to the entity being resolved, not the parent. This means each resolver gets the projection, filter, and sort context for its own return type: C# ``` [ObjectType] public static partial class ProductNode { [BindMember(nameof(Product.BrandId))] public static async Task GetBrandAsync( [Parent(requires: nameof(Product.BrandId))] Product product, QueryContext query, BrandService brandService, CancellationToken cancellationToken) => await brandService.GetBrandByIdAsync( product.BrandId, query, cancellationToken); } ``` For nested connection fields, combine `QueryContext` with `[UseConnection]`, `[UseFiltering]`, and `[UseSorting]`: C# ``` [ObjectType] public static partial class BrandNode { [UseConnection(EnableRelativeCursors = true)] [UseFiltering] [UseSorting] public static async Task> GetProductsAsync( [Parent(requires: nameof(Brand.Id))] Brand brand, PagingArguments pagingArgs, QueryContext query, ProductService productService, CancellationToken cancellationToken) { var page = await productService.GetProductsByBrandAsync( brand.Id, pagingArgs, query, cancellationToken); return new PageConnection(page); } } ``` ## Including Additional Fields Sometimes a DataLoader or batch resolver needs a field that the client didn't request (for example, the `Id` for dictionary keying). Use `.Include()` to ensure specific properties are always projected: C# ``` [BatchResolver] public static async Task> GetSupplierAsync( [Parent(requires: nameof(Brand.SupplierId))] List brands, QueryContext query, CatalogContext context, CancellationToken cancellationToken) { var supplierIds = brands .Select(b => b.SupplierId) .Distinct() .ToList(); var suppliers = await context.Suppliers .Where(s => supplierIds.Contains(s.Id)) .With(query.Include(s => s.Id)) .ToDictionaryAsync(s => s.Id, cancellationToken); return brands .Select(b => suppliers.GetValueOrDefault(b.SupplierId)) .ToList(); } ``` The `.Include(s => s.Id)` call adds the `Id` property to the projection selector so it is always available for the dictionary key, even if the client did not request it. ## Always and Never Projected Fields By default, the projection selector contains the properties the client selects and any you add with `.Include()`. Two attributes change this per field at the schema level. `[IsProjected]` marks a field whose property is always part of the projection, even when the client does not select it. Use it for properties that server-side logic reads regardless of the selection, such as an owner identifier checked by authorization or a discriminator read by an abstract type resolver. Only leaf fields (scalars and enums) backed by a property are included this way; navigation and collection properties are not. `[IsProjected(false)]` marks a field whose property is never part of the projection, even when the client selects it. Unlike `[IsProjected]`, this applies to any field, including navigation and collection fields. The field then resolves to the property's default value. Use it for large columns you load another way. C# ``` public class Product { public int Id { get; set; } public string Name { get; set; } [IsProjected] public int OwnerId { get; set; } [IsProjected(false)] public string? Description { get; set; } } ``` For the following query, the generated SQL selects `Name` and `OwnerId`, and `description` resolves to `null`: GraphQL ``` { products { name description } } ``` SQL ``` SELECT "p"."Name", "p"."OwnerId" FROM "Products" AS "p" ``` Both attributes apply wherever the projection is built from the selection set: `QueryContext` and DataLoaders that call `.Select(selection)`. In code-first types, use the `.IsProjected()` and `.IsProjected(false)` descriptor extensions instead of the attributes. ## Migrating from UseProjection If you are migrating from the `[UseProjection]` attribute approach, the key changes are: **Before (attribute-based):** C# ``` [QueryType] public static partial class ProductQueries { [UsePaging] [UseProjection] [UseFiltering] [UseSorting] public static IQueryable GetProducts(CatalogContext db) => db.Products; } ``` **After (QueryContext):** C# ``` [QueryType] public static partial class ProductQueries { [UseConnection] [UseFiltering] [UseSorting] public static async Task GetProductsAsync( PagingArguments pagingArgs, QueryContext query, CatalogContext db, CancellationToken cancellationToken) { var page = await db.Products .With(query) .ToPageAsync(pagingArgs, cancellationToken); return new ProductConnection(page); } } ``` The main differences: - **No `[UseProjection]` attribute.** `QueryContext` handles projections via the `Selector` it receives from the GraphQL selection set. - **Explicit data pipeline.** You control when and how filtering, sorting, and projection are applied to your query through the `.With()` call. - **Service layer friendly.** You can pass `QueryContext` into services and DataLoaders, making your data access logic reusable and testable. - **No middleware ordering concerns.** With `[UseProjection]`, you had to maintain a strict attribute order (`UsePaging` \> `UseProjection` \> `UseFiltering` \> `UseSorting`). With `QueryContext`, the `.With()` method applies operations in the correct order automatically. > Do not combine `QueryContext` with `[UseProjection]` on the same field. Each applies its own `Select` expression, leading to unexpected behavior. The HC0099 analyzer warns when both are present. ## Next Steps - **Need to filter results?** See [Filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering). - **Need to sort results?** See [Sorting](https://chillicream.com/docs/hotchocolate/fetching-data/sorting). - **Need to page through results?** See [Pagination](https://chillicream.com/docs/hotchocolate/fetching-data/pagination). - **Need to integrate with Entity Framework?** See [Entity Framework Integration](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/projections.md) Maintained by ChilliCream. Last updated on **August 28, 2026** by **Glen** --- # GraphQL Sorting in Hot Chocolate > Generate sort input types from .NET models with the [UseSorting] attribute in Hot Chocolate, translating client sort arguments into native database ordering. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/sorting Hot Chocolate generates sort input types from your .NET models, allowing clients to order results by one or more fields. The default implementation translates sort operations to expression trees applied to `IQueryable`, producing native database queries. For models with nested objects, sorting extends across relationships. ## Getting Started Sorting is part of the `HotChocolate.Data` package. Bash ``` dotnet add package HotChocolate.Data ``` Warning All `HotChocolate.*` packages need to have the same version. Register sorting on the schema: C# ``` builder .AddGraphQL() .AddSorting(); ``` Apply the `[UseSorting]` attribute to a resolver that returns `IQueryable` or `IEnumerable`: C# ``` [QueryType] public static partial class UserQueries { [UseSorting] public static IQueryable GetUsers(CatalogContext db) => db.Users; } ``` Clients use the `order` argument to sort results: GraphQL ``` query { users(order: [{ name: ASC }]) { name email } } ``` Warning **Middleware order matters.** When combining multiple middleware, apply them in this order: `UsePaging` \> `UseProjection` \> `UseFiltering` \> `UseSorting`. ## Sorting on Nested Fields Sorting extends to properties of nested objects: GraphQL ``` query { users(order: [{ address: { city: ASC } }]) { name address { city } } } ``` ## Multi-Field Sorting Pass multiple sort conditions as an array. The database applies them in order: GraphQL ``` query { users(order: [{ name: ASC }, { address: { city: DESC } }]) { name address { city } } } ``` ## NullOrdering Enum The `NullOrdering` enum controls how `null` values sort relative to non-null values. This is relevant when sorting on nullable fields. Set this through `PagingOptions` at the global level: C# ``` builder .AddGraphQL() .ModifyPagingOptions(opt => opt.NullOrdering = NullOrdering.NativeNullsLast); ``` | Value | When to use | | ---------------- | ----------------------------------------------------------------------- | | Unspecified | Default. Auto-detected for known EF Core providers. | | NativeNullsFirst | Nulls sort before non-null values (SQL Server, SQLite, in-memory LINQ). | | NativeNullsLast | Nulls sort after non-null values (PostgreSQL default). | ## Custom Sort Types Customize which fields are sortable by extending `SortInputType`: C# ``` public class UserSortType : SortInputType { protected override void Configure(ISortInputTypeDescriptor descriptor) { descriptor.BindFieldsExplicitly(); descriptor.Field(f => f.Name); descriptor.Field(f => f.CreatedAt); } } ``` Restrict sort directions on a field by providing a custom enum type: C# ``` public class AscOnlySortEnumType : DefaultSortEnumType { protected override void Configure(ISortEnumTypeDescriptor descriptor) { descriptor.Operation(DefaultSortOperations.Ascending); } } ``` C# ``` public class UserSortType : SortInputType { protected override void Configure(ISortInputTypeDescriptor descriptor) { descriptor.BindFieldsExplicitly(); descriptor.Field(f => f.Name).Type(); } } ``` Apply the custom sort type: C# ``` [QueryType] public static partial class UserQueries { [UseSorting(typeof(UserSortType))] public static IQueryable GetUsers(CatalogContext db) => db.Users; } ``` ## Sort Conventions Sort conventions let you change sorting behavior globally across your schema. ### Setting Up a Convention Extend `SortConvention` and override `Configure`: C# ``` public class CustomSortConvention : SortConvention { protected override void Configure(ISortConventionDescriptor descriptor) { descriptor.AddDefaults(); descriptor.ArgumentName("sortBy"); } } ``` C# ``` builder .AddGraphQL() .AddConvention(); ``` To extend the default behavior without replacing it, use `SortConventionExtension`: C# ``` public class CustomSortConventionExtension : SortConventionExtension { protected override void Configure(ISortConventionDescriptor descriptor) { descriptor.Configure( x => x.Operation(DefaultSortOperations.Ascending).Description("Sort ascending")); } } ``` ### Binding Sort Types Globally Bind custom sort types to .NET types through the convention: C# ``` public class CustomSortConvention : SortConvention { protected override void Configure(ISortConventionDescriptor descriptor) { descriptor.AddDefaults(); descriptor.BindRuntimeType(); } } ``` ### Default Binding For fields where no explicit binding exists, `DefaultSortEnumType` (with `ASC` and `DESC`) is used. Override this with `DefaultBinding`: C# ``` descriptor.AddDefaults().DefaultBinding(); ``` ## Next Steps - **Need to filter results?** See [Filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering). - **Need to page through results?** See [Pagination](https://chillicream.com/docs/hotchocolate/fetching-data/pagination). - **Need to optimize database queries?** See [Projections](https://chillicream.com/docs/hotchocolate/fetching-data/projections). - **Need to protect against expensive queries?** See [Cost Analysis](https://chillicream.com/docs/hotchocolate/security/cost-analysis). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/sorting.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Spatial Data - Hot Chocolate > Learn how to expose NetTopologySuite spatial types as GeoJSON in Hot Chocolate. Canonical source: https://chillicream.com/docs/hotchocolate/fetching-data/spatial-data Experimental This feature is community-driven and not yet finalized. The core team has limited experience with spatial data and welcomes your feedback to guide next steps. While we try not to introduce breaking changes, we reserve the possibility to adjust the API in future releases. Spatial data describes locations or shapes as objects. Many database providers support storing this type of data. APIs often use GeoJSON to send spatial data over the network. The most common library for spatial data in .NET is [NetTopologySuite](https://github.com/NetTopologySuite/NetTopologySuite). Entity Framework supports [Spatial Data](https://docs.microsoft.com/en-gb/ef/core/modeling/spatial) and uses NetTopologySuite as its data representation. The `HotChocolate.Spatial` package integrates NetTopologySuite into Hot Chocolate. Your resolvers can return NetTopologySuite shapes, and they are transformed into GeoJSON. ## Getting Started Install the `HotChocolate.Spatial` package: Bash ``` dotnet add package HotChocolate.Spatial ``` Warning All `HotChocolate.*` packages need to have the same version. Register the spatial types on the schema builder: C# ``` builder .AddGraphQL() .AddSpatialTypes(); ``` If you use data extensions to project data from a database, also install `HotChocolate.Data.Spatial`: Bash ``` dotnet add package HotChocolate.Data.Spatial ``` Warning All `HotChocolate.*` packages need to have the same version. Register the data extensions: C# ``` builder .AddGraphQL() .AddSpatialTypes() .AddFiltering() .AddProjections() .AddSpatialFiltering() .AddSpatialProjections(); ``` All NetTopologySuite runtime types are now bound to the corresponding GeoJSON type: C# ``` public class Pub { public int Id { get; set; } public string Name { get; set; } public Point Location { get; set; } } public class Query { public IQueryable GetPubs(SomeDbContext someDbContext) { return someDbContext.Pubs; } } ``` SDL ``` type Pub { id: Int! name: String! location: GeoJSONPointType! } type Query { pubs: [Pub!]! } ``` GraphQL ``` { pubs { id location { __typename bbox coordinates crs type } name } } ``` JSON ``` { "data": { "pubs": [ { "id": 1, "location": { "__typename": "GeoJSONPointType", "bbox": [12, 12, 12, 12], "coordinates": [[12, 12]], "crs": 4326, "type": "Point" }, "name": "The Winchester" } ] } } ``` ## Spatial Types Hot Chocolate supports GeoJSON input and output types, along with a GeoJSON scalar for generic inputs. ### Output Types | NetTopologySuite | GraphQL | | ---------------- | -------------------------- | | Point | GeoJSONPointType | | MultiPoint | GeoJSONMultiPointType | | LineString | GeoJSONLineStringType | | MultiLineString | GeoJSONMultiLineStringType | | Polygon | GeoJSONPolygonType | | MultiPolygon | GeoJSONMultiPolygonType | | Geometry | GeoJSONInterface | All GeoJSON output types implement: SDL ``` interface GeoJSONInterface { "The geometry type of the GeoJson object" type: GeoJSONGeometryType! "The minimum bounding box around the geometry object" bbox: [Float] "The coordinate reference system integer identifier" crs: Int } ``` ### Input Types | NetTopologySuite | GraphQL | | ---------------- | --------------------------- | | Point | GeoJSONPointInput | | MultiPoint | GeoJSONMultiPointInput | | LineString | GeoJSONLineStringInput | | MultiLineString | GeoJSONMultiLineStringInput | | Polygon | GeoJSONPolygonInput | | MultiPolygon | GeoJSONMultiPolygonInput | ### Scalar The `Geometry` scalar accepts any geometry type as input. This is useful when a resolver expects any `Geometry` type. Use this scalar with caution, as input and output types are more expressive. SDL ``` scalar Geometry ``` ## Projections Register the spatial projection handler with `.AddSpatialProjections()`: C# ``` builder .AddGraphQL() .AddProjections() .AddSpatialTypes() .AddSpatialProjections() ``` The projection middleware uses this handler to project spatial data directly to the database: C# ``` [UseProjection] public IQueryable GetPubs(SomeDbContext someDbContext) { return someDbContext.Pubs; } ``` ## Filtering Entity Framework supports filtering on NetTopologySuite objects. `HotChocolate.Spatial` provides handlers for filtering spatial types on `IQueryable`. Register them with `.AddSpatialFiltering()`: C# ``` builder .AddGraphQL() .AddFiltering() .AddSpatialTypes() .AddSpatialFiltering() ``` After registration, `UseFiltering()` infers the possible filter types for all `Geometry`\-based types. C# ``` [UseFiltering] public IQueryable GetPubs(SomeDbContext someDbContext) { return someDbContext.Pubs; } ``` ### Distance The `distance` filter requires an input geometry. You can optionally buffer the geometry. All comparable filter operations are available. GraphQL ``` { pubs( where: { location: { distance: { geometry: { type: Point, coordinates: [1, 1] }, lt: 120 } } } ) { id name } } ``` ### Contains The `contains` filter is an implementation of `Geometry.Contains`. It requires an input geometry with an optional buffer. GraphQL ``` { counties( where: { area: { contains: { geometry: { type: Point, coordinates: [1, 1] } } } } ) { id name } } ``` The negation is `ncontains`. ### Touches The `touches` filter is an implementation of `Geometry.Touches`. GraphQL ``` { counties( where: { area: { touches: { geometry: { type: Polygon coordinates: [[1, 1], ...] } } } } ) { id name } } ``` The negation is `ntouches`. ### Intersects The `intersects` filter is an implementation of `Geometry.Intersects`. GraphQL ``` { roads( where: { road: { intersects: { geometry: { type: LineString coordinates: [[1, 1], ...] } } } } ) { id name } } ``` The negation is `nintersects`. ### Overlaps The `overlaps` filter is an implementation of `Geometry.Overlaps`. The negation is `noverlaps`. ### Within The `within` filter is an implementation of `Geometry.Within`. GraphQL ``` { pubs( where: { location: { within: { geometry: { type: Point, coordinates: [1, 1] }, buffer: 200 } } } ) { id name } } ``` The negation is `nwithin`. ## Next Steps - [Filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering) for general filtering concepts - [Projections](https://chillicream.com/docs/hotchocolate/fetching-data/projections) for projection setup - [Entity Framework integration](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework) for EF Core setup [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/fetching-data/spatial-data.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # GraphQL .NET Tutorial with Hot Chocolate > In this tutorial, you will walk through the basics of creating a GraphQL server with Hot Chocolate. Canonical source: https://chillicream.com/docs/hotchocolate/get-started-with-graphql-in-net-core By the end of this guide, you will have a running GraphQL server that responds to queries. You will use the Hot Chocolate project template, explore the generated code, and execute your first query in the Nitro GraphQL IDE. **Prerequisites:** [.NET 8 SDK](https://dotnet.microsoft.com/download) or later. ## Create the Project Install the Hot Chocolate templates. Bash ``` dotnet new install HotChocolate.Templates ``` Create a new project from the template. Bash ``` dotnet new graphql --name GettingStarted ``` This creates a `GettingStarted` directory with the project files. Open it in your editor. ## Explore the Generated Code ### Domain Types The `Types` directory contains two record types that represent the domain model. C# ``` public record Author(string Name); ``` C# ``` public record Book(string Title, Author Author); ``` These are regular C# types. Hot Chocolate infers the GraphQL schema from them. ### Query Type The `Query` class defines the root type for read operations. Each public method becomes a field that clients can query. C# ``` [QueryType] public static partial class Query { public static Book GetBook() => new Book("C# in depth.", new Author("Jon Skeet")); } ``` The `[QueryType]` attribute tells the source generator to register this class as part of the GraphQL Query type. The class must be `partial` so the source generator can add the registration code at build time. The method `GetBook` becomes a field named `book` in the schema. Hot Chocolate strips the `Get` prefix by convention. ### Program.cs The generated `Program.cs` sets up the server. C# ``` builder.AddGraphQL() ``` `AddGraphQL` returns an `IRequestExecutorBuilder` for configuring the GraphQL server. The template also calls a source-generated `AddTypes` method that registers all types decorated with attributes like `[QueryType]` in the current assembly. C# ``` app.MapGraphQL() ``` `MapGraphQL` exposes the GraphQL endpoint at `/graphql`. This is where clients send queries and where Nitro (the built-in GraphQL IDE) is served. C# ``` app.RunWithGraphQLCommands(args) ``` `RunWithGraphQLCommands` works like `Run()` but adds developer commands. For example, you can export the schema as SDL. Bash ``` dotnet run -- schema export ``` This writes a `schema.graphqls` file to your project directory. ## Run the Server Bash ``` dotnet run ``` If everything worked, the terminal output includes a line like this: ``` Now listening on: http://localhost:5095 ``` Open in your browser. You should see the Nitro GraphQL IDE. ![GraphQL IDE](https://chillicream.com/images/hotchocolate-docs/getting-started-nitro.webp) Click **Create Document**, verify the HTTP Endpoint matches your server URL, and click **Apply**. ![GraphQL IDE: Setup](https://chillicream.com/images/hotchocolate-docs/getting-started-nitro-setup.webp) You should see the editor with **Schema available** at the bottom right. ![GraphQL IDE: Editor](https://chillicream.com/images/hotchocolate-docs/getting-started-nitro-editor.webp) ## Execute a Query Paste the following query into the **Request** pane. GraphQL ``` { book { title author { name } } } ``` Click **Run**. The **Response** pane should show: JSON ``` { "data": { "book": { "title": "C# in depth.", "author": { "name": "Jon Skeet" } } } } ``` ![GraphQL IDE: Executing a query](https://chillicream.com/images/hotchocolate-docs/getting-started-nitro-query.webp) You can browse the schema by clicking the **Schema** tab next to **Operation**. The **Schema Definition** tab shows the raw SDL. ![GraphQL IDE: Schema](https://chillicream.com/images/hotchocolate-docs/getting-started-nitro-schema.webp) Your GraphQL server is running and responding to queries. ## Next Steps - **"I want to learn about the type system."** See [Defining a Schema](https://chillicream.com/docs/hotchocolate/defining-a-schema) for queries, mutations, subscriptions, and all the GraphQL types. - **"I want to fetch data from a database."** See [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader) for batched data fetching, or [Entity Framework](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework) for EF Core integration. - **"I want a deeper tutorial."** Check out the [GraphQL Workshop](https://github.com/ChilliCream/graphql-workshop) for a hands-on walkthrough covering types, resolvers, DataLoaders, filtering, and more. - **"I'm new to GraphQL."** Read the [official GraphQL introduction](https://graphql.org/learn/) to understand the concepts before diving deeper into Hot Chocolate. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/get-started-with-graphql-in-net-core.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Migrate Hot Chocolate from 10 to 11 - Hot Chocolate > Step-by-step guide for migrating a Hot Chocolate GraphQL server from version 10 to 11, including the move to the consolidated HotChocolate.AspNetCore package. Canonical source: https://chillicream.com/docs/hotchocolate/migrating/migrate-from-10-to-11 This guide will walk you through the manual migration steps to get you Hot Chocolate GraphQL server to version 11. As a general preparation, we recommend removing all HotChocolate.\* package references from your project. Then start by adding the `HotChocolate.AspNetCore` package. The server package now contains most of the needed packages. When do I need to add other Hot Chocolate packages explicitly? We have now added the most common packages to the Hot Chocolate core. But there are certain areas where we still need to add some additional packages. | Package | Topic | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | HotChocolate.AspNetCore.Authorization | The authorization package adds the authorization directive and integrates with Microsoft Authorization Policies | | HotChocolate.Data | The new data package represents our integration with all kinds of data sources. This package provides the fundamentals for filtering, sorting, and projection logic. | | HotChocolate.Types.Spatial | This package provides GeoJson spatial types. | | HotChocolate.Data.Spatial | The package integrates the spatial types with the data package to allow for spatial filtering, sorting, and projections. | | HotChocolate.Subscriptions.Redis | The in-memory subscription provider, is now integrated by default. To have an integration with Redis, you need to add this package. | | HotChocolate.PersistedQueries.FileSystem | This package provides a persisted query storage for the file system. | | HotChocolate.PersistedQueries.Redis | This package provides a persisted query storage for Redis. | ## ASP.NET Core One of the main focuses of version 11 was to create a new configuration API that brings all our builders together into one unified API. This also means that we had to introduce breaking changes to the way we configure schemas. After you have cleaned up your packages, head over to the `Startup.cs` to start with the new configuration API migration. ### ConfigureServices In your `Startup.cs` head over to the `ConfigureServices` methods. The configuration of a schema has slightly changed, and the new configuration API has replaced the `SchemaBuilder`. We now start with `AddGraphQLServer` to define a new GraphQL server, `AddGraphQLServer`, returns the new `IRequestExecutorBuilder` that lets us apply all the configuration methods that used to be on the `SchemaBuilder`, `StitchingBuilder` and the `QueryExecutionBuilder`. **Old:** C# ``` services.AddGraphQL(sp => SchemaBuilder.New() .AddServices(sp) .AddQueryType() .AddMutationType() ... .Create()); ``` **New:** C# ``` services .AddGraphQLServer() .AddQueryType() .AddMutationType() ... ``` If you were using the `OperationRequestBuilder` to configure request options or change the request pipeline, you need to add those things to the configuration chain of the \`\`\`IRequestExecutorBuilder\`. C# ``` services .AddGraphQLServer() .AddQueryType() .AddMutationType() ... .ModifyRequestOptions(o => o.ExecutionTimeout = TimeSpan.FromSeconds(180)); ``` ### Configure After migrating the schema configuration, the next area that has fundamentally changed is the schema middleware. Hot Chocolate server now embraces the new endpoint routing API from ASP.NET core and with that brings a lot of new features. Head over [here](https://chillicream.com/docs/hotchocolate/server) to read more about the ASP.NET Core integration. **Old:** C# ``` app.UseGraphQL(); ``` **New:** C# ``` app.UseRouting(); // routing area app.UseEndpoints(x => x.MapGraphQL()); ``` ### Request Interceptor The query request interceptor was reworked and we renamed it to `IHttpRequestInterceptor`. C# ``` public interface IHttpRequestInterceptor { ValueTask OnCreateAsync( HttpContext context, IRequestExecutor requestExecutor, OperationRequestBuilder requestBuilder, CancellationToken cancellationToken); } ``` **Old:** C# ``` services.AddQueryRequestInterceptor( (context, builder, ct) => { // your code }); ``` **New:** C# ``` services.AddGraphQLServer() ... .AddHttpRequestInterceptor( (context, executor, builder, ct) => { // your code }); ``` You can also extend `DefaultHttpRequestInterceptor` and inject it like the following. C# ``` services.AddGraphQLServer() ... .AddHttpRequestInterceptor(); ``` > A request interceptor is a service that is used by all hosted schemas. ### Entity Framework Serial Execution The serial execution for Entity Framework compatibility is gone. If you use Entity Framework Core we recommend using version 5 and the new context factory in combination with context pooling. This allows the execution engine to execute in parallel and still be memory efficient since context objects are pooled. Another variant here is to use our scoped service feature that scopes services for the resolver pipeline. This is explained in our GraphQL Workshop project. ## Schema / Resolvers ### Field ordering Hot Chocolate 11 follows the spec and returns the fields in the order they were defined. This feature makes migrations harder because the schema snapshot looks different compared to version 11\. You can change this behavior with the following setting. C# ``` builder.ModifyOptions(x => x.SortFieldsByName = true) ``` ### DataLoaders With Hot Chocolate server 11, we have embraced the new DataLoader spec version 2\. With that, we have decoupled the scheduler from the DataLoader itself, meaning you now have to pass on the `IBatchScheduler` to the base implementation of the DataLoader. Apart from that, DataLoader now uses `ValueTask` instead of `Task` when doing async work. If you were adding the `DataLoaderRegistry` to the services, remove that code since `service.AddDataLoaderRegistry` is no longer needed. **Old:** C# ``` public class FooDataLoader : DataLoaderBase { private readonly IFooRepository _fooRepository; public FooDataLoader(IFooRepository fooRepository) { _fooRepository = fooRepository; } protected override async Task>> FetchAsync( IReadOnlyList keys, CancellationToken cancellationToken) { .... } } ``` **New:** C# ``` public class FooDataLoader : DataLoaderBase { private readonly IFooRepository _fooRepository; public FooDataLoader( // ▼ IBatchScheduler scheduler, IFooRepository fooRepository) : base(scheduler) { _fooRepository = fooRepository; } // ▼ protected override async ValueTask>> FetchAsync( IReadOnlyList keys, CancellationToken cancellationToken) { .... } ``` ### Node Resolver With version 11, we have reworked how Relay node types are defined. Furthermore, we added pure code-first (annotation-based) support. **Old:** C# ``` descriptor .AsNode() .IdField(d => d.Id) .NodeResolver(async (ctx, id) => await ctx .DataLoader() .LoadAsync(id, ctx.RequestAborted)) ``` **New:** The following example essentially aligns very closely to the old variant. C# ``` descriptor .ImplementsNode() .IdField(d => d.Id) .ResolveNode(async (ctx, id) => await ctx .DataLoader() .LoadAsync(id, ctx.RequestAborted)) ``` But, we can now also use an external resolver like with standard resolvers. This allows us to write better testable code that takes advantage of the method parameter injection we use in everyday resolvers. C# ``` descriptor .ImplementsNode() .IdField(d => d.Id) .ResolveNodeWith(t => t.GetNodeAsync(default, default)); ``` But we can go even further now with pure code-first (annotation-based) support. By just annotating the entity with the `NodeAttribute`, we essentially told the schema builder that this is a node. The type initialization can then try to infer the node resolver directly from the type. C# ``` [Node] public class MyEntity { public string Id { get; set; } public async Task GetAsync(....) { .... } } ``` Often, however, we want the repository logic decoupled from our domain object/entity. In this case, we can specify the entity resolver type. C# ``` [Node(NodeResolverType = typeof(MyEntityResolver))] public class MyEntity { public string Id { get; set; } } public class MyEntityResolver { public async Task GetAsync(....) { .... } } ``` There are more variants possible, but to give an impression of the new convenience and flexibility around nodes. As a side note, if you do not want the node attribute on the domain objects, you can also now add your very own attribute or interface to mark this and rewrite that in the schema building process to the `NodeAttribute`. ### Pagination The first thing to note around pagination is that we listened to a lot of feedback and have removed the `PaginationAmountType`. Moreover, we have introduced new PagingOptions, which can be set with the new configuration API on the schema level. With the new options, you can configure the `MaxPageSize`, `DefaultPageSize` and whether the total count shall be included `IncludeTotalCount`. C# ``` builder.SetPagingOptions( new PagingOptions() { MaxPageSize = searchOptions.PaginationAmount, DefaultPageSize = searchOptions.PaginationAmount, IncludeTotalCount = true }); ``` Further, you can override the paging option on the resolver level. C# ``` [UsePaging(MaxPageSize = 100)] ``` C# ``` descriptor.Field(...).UsePaging(maxPageSize = 100)... ``` ### Projections The selection middleware, that was available in `HotChocolate.Types.Selections` was replaced by the projection middleware from `HotChocolate.Data`. **Old:** C# ``` descriptor.Field(...).UseSelection()... ``` **New:** C# ``` descriptor.Field(...).UseProjection()... ``` Similarly, the attribute `[UseSelection]` was replaced by `[UseProjection]`. To use projections with your GraphQL endpoint you have to register it on the schema: C# ``` services.AddGraphQLServer() // Your schema configuration .AddProjections(); ``` ### Enum Type Hot Chocolate server 11 now follows the spec recommendation with the new enum name conventions and formats the enum values by default as UPPER\_SNAIL\_CASE. To avoid breaking changes to your schema, you will have to override the naming convention: **Configuration:** C# ``` builder .AddConvention(new CompatibilityNamingConvention()) ``` **Convention:** C# ``` public class CompatibilityNamingConvention : DefaultNamingConventions { public override NameString GetEnumValueName(object value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } return value.ToString().ToUpperInvariant(); } } ``` ### IResolverContext.Source The source result stack was removed from the resolver context for performance reasons. If you need such a functionality, you can write a middleware that aggregates the resulting path on the scoped context. **Old:** C# ``` public class FooType : ObjectType { private static readonly object _empty = new object(); protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor .Field("bar") .Type>() .Resolver(_empty); } } public class BarType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor .Field("baz") .Type() .Resolve(ctx => { Foo foo = (Foo)ctx.Source.Pop().Peek(); return foo.Baz; }); } } ``` **New:** C# ``` public class FooType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor .Field("bar") .Type>() .Resolve( ctx => { ctx.ScopedContextData = ctx.ScopedContextData.SetItem(nameof(Foo), ctx.Parent()); return new object(); }); } } public class BarType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor .Field("baz") .Type() .Resolve( ctx => { if (ctx.ScopedContextData.TryGetValue(nameof(Foo), out object? potentialFoo) && potentialFoo is Foo foo) { return foo.Baz; } throw new GraphQLException( ErrorBuilder.New() .AddLocation(ctx.Field.SyntaxNode) .SetMessage("Foo was not pushed down.") .SetPath(ctx.Path) .Build()); }); } } ``` ### Authorization If you use authorization, you need to add a package reference to `HotChocolate.AspNetCore.Authorization`. **Old:** C# ``` builder.AddAuthorizeDirectiveType() ``` **New:** C# ``` builder.AddAuthorization() ``` ### TypeBinding We have renamed the binding method from `BindClrType` to `BindRuntimeType` to make it more clear what it does. **Old:** C# ``` builder.BindClrType() ``` **New:** C# ``` builder.BindRuntimeType() ``` ### FieldMiddleware Since all configuration APIs were integrated into one, we needed to make it more specific for what a middleware is defined. `UseField` defines a middleware that is applied to the resolver pipeline / field pipeline whereas `UseRequest` defines a middleware that is defined for the request processing. **Old:** C# ``` builder.Use() ``` **New:** C# ``` builder.UseField() ``` ## Stitching The schema stitching configuration API has been completely integrated into the new configuration API. This means that a Gateway is nothing more than a GraphQL schema, which will make it easier for new users. However, you will need to completely rewire your stitching configuration. ### Configuration The stitching builder no longer exists in version 11 and you need to use the new configuration API to configure your gateway. **Old:** C# ``` services.AddStitchedSchema(x => ....); ``` **New:** C# ``` services.AddGraphQLServer().... ``` #### AddSchemaFromHttp Registering a remote schema has slightly changed in version 11 to make it more clear that we are adding a remote schema into the local gateway schema. Removing, root types and importing a remote schema can be done in one go now. **Old:** C# ``` builder.AddSchemaFromHttp("SomeSchema").IgnoreRootTypes("SomeSchema"); ``` **New:** C# ``` builder.AddRemoteSchema("SomeSchema", ignoreRootTypes: true); ``` ### AddSchemaConfiguration In version 11 it is now much easier to configure the gateway schema. **Old:** C# ``` services.AddStitchedSchema(x => x.AddSchemaConfiguration(y => y.RegisterType())); ``` **New:** C# ``` services .AddGraphQLServer() .AddType(); ``` ### IgnoreField The order of the parameters in ignore field and ignore type has changed since we moved optional parameters to the end. **Old:** C# ``` services.AddStitchedSchema(x => x.IgnoreField("SchemaName", "TypeName, "FieldName")); ``` **New:** C# ``` services .AddGraphQLServer() .IgnoreField("TypeName, "FieldName", "SchemaName") ``` ### SetExecutionOptions Execution options can now be configured on the root schema directly like for any other schema: **Old:** C# ``` services.AddStitchedSchema( x => x.SetExecutionOptions( new QueryExecutionOptions { TracingPreference = TracingPreference.OnDemand })); ``` **New:** C# ``` services .AddGraphQLServer() .ModifyRequestOptions(x => x.TracingPreference = TracingPreference.OnDemand); ``` ### Configuring a downstream schema In case you want to configure a downstream schema, you can now just use the new configuration API since all downstream schemas have an in-memory representation. C# ``` services .AddGraphQLServer() .AddRemoteSchema("SomeSchema"); services .AddGraphQL("SomeSchema") .AddType(new IntType("SpecialIntegerType")); ``` ### PaginationAmount The `PaginationAmount` scalar was removed since it caused a lot of issues with clients and only provided limited benefit. The arguments `first` and `last` use now `Int` as a type. To avoid breaking schemas on a stitched schema, you can add a rewriter that rewrites all `first: Int` and `last: Int` on a connection to `first: PaginationAmount` and `last: PaginationAmount`. You also have to make sure that you register a new `IntType` on the root schema and rewrite all downstream schemas. **Configuration:** C# ``` services .AddGraphQLServer() .AddRemoteSchema("SomeSchema") .ConfigureSchema(x => x.AddType(new IntType()) .AddType(new IntType("PaginationAmount"))) .AddMergedDocumentRewriter( d => (DocumentNode)new PagingAmountRewriter().Rewrite(d, null)); services .AddGraphQL("SomeSchema") .ConfigureSchema(x => x.AddType(new IntType()) .AddType(new IntType("PaginationAmount"))); ``` **PagingAmountRewriter:** C# ``` internal class PagingAmountRewriter : SchemaSyntaxRewriter { protected override FieldDefinitionNode RewriteFieldDefinition( FieldDefinitionNode node, object? context) { if (node.Type.NamedType().Name.Value.EndsWith("Connection") && (node.Arguments.Any( t => t.Name.Value.EqualsOrdinal("first") && t.Type.NamedType().Name.Value.EqualsOrdinal("Int")) || node.Arguments.Any( t => t.Name.Value.EqualsOrdinal("last") && t.Type.NamedType().Name.Value.EqualsOrdinal("Int")) )) { var arguments = node.Arguments.ToList(); InputValueDefinitionNode first = arguments.FirstOrDefault(t => t.Name.Value.EqualsOrdinal("first")); InputValueDefinitionNode last = arguments.FirstOrDefault(t => t.Name.Value.EqualsOrdinal("last")); if (first != null) arguments[arguments.IndexOf(first)] = first.WithType(RewriteType(first.Type, "PaginationAmount")); if (last != null) arguments[arguments.IndexOf(last)] = last.WithType(RewriteType(last.Type, "PaginationAmount")); node = node.WithArguments(arguments); } return base.RewriteFieldDefinition(node, context); } private static ITypeNode RewriteType(ITypeNode type, NameString name) { if (type is NonNullTypeNode nonNullType) { return new NonNullTypeNode( (INullableTypeNode)RewriteType(nonNullType.Type, name)); } if (type is ListTypeNode listType) { return new ListTypeNode(RewriteType(listType.Type, name)); } return new NamedTypeNode(name); } } internal static class StringExtensions { public static bool EqualsOrdinal(this string value, string other) => string.Equals(value, other, StringComparison.Ordinal); } ``` ### Batch responses In v10, responses to batched operations were returned as a JsonArray. In v11 the default is to return MultiPartChunked responses. To switch back to JsonArray, configure the HttpResult serializer as follows: C# ``` services.AddHttpResultSerializer( batchSerialization: HttpResultSerialization.JsonArray ); ``` ## Testing We have added a couple of test helpers to make the transition to the new configuration API easier. ### Schema Snapshot Tests **Old:** C# ``` SchemaBuilder.New() .AddQueryType() .Create() .ToString() .MatchSnapshot(); ``` **New:** C# ``` ISchema schema = await new ServiceCollection() .AddGraphQL() .AddQueryType() .BuildSchemaAsync(); schema.Print().MatchSnapshot(); ``` ### Request Tests **Old:** C# ``` IQueryExecutor executor = SchemaBuilder.New() .AddQueryType() .Create() .MakeExecutable(); ``` **New:** C# ``` IRequestExecutor executor = await new ServiceCollection() .AddGraphQL() .AddQueryType() .BuildRequestExecutorAsync(); IExecutionResult result = await executor.ExecuteAsync("{ __typename }"); result.ToJson().MatchSnapshot(); ``` Or you can directly build and execute: C# ``` IExecutionResult result = await new ServiceCollection() .AddGraphQL() .AddQueryType() .ExecuteRequestAsync("{ __typename }"); result.ToJson().MatchSnapshot(); ``` ### DataLoader Testing Due to the changed constructor you now need to also create a scheduler for the dataloaders Old C# ``` FooDataLoader dataLoader = new FooDataLoader( fooRepoMock.Object); ``` New C# ``` var scheduler = new BatchScheduler(); FooDataLoader dataLoader = new FooDataLoader( scheduler, fooRepoMock.Object); ``` // TODO : Type Converter [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/migrating/migrate-from-10-to-11.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Migrate Hot Chocolate from 11 to 12 - Hot Chocolate > Covers the manual steps to upgrade your Hot Chocolate GraphQL server from version 11 to 12, including resolver changes and other breaking API updates. Canonical source: https://chillicream.com/docs/hotchocolate/migrating/migrate-from-11-to-12 This guide will walk you through the manual migration steps to get your Hot Chocolate GraphQL server to version 12. ## Resolvers We have reworked the resolver compiler and are now demanding that the `ParentAttribute` is used when an argument is referring to the parent object. This is done since in some cases people want to get the parent object which is the same runtime type as an argument value. **v11** C# ``` public string MyResolver(Person parent, string additionalInput) { // Code omitted for brevity } ``` **v12** C# ``` public string MyResolver([Parent] Person parent, string additionalInput) { // Code omitted for brevity } ``` ## Scalars We changed some defaults around scalars. These new defaults can break your existing schema but are, in general, better for newcomers and align better with the overall GraphQL ecosystem. Of course, you can naturally opt out of these new defaults to preserve your current schema's integrity. ### UUID We changed the name of the UUID scalar from `Uuid` to `UUID`. To maintain the old name, register the type manually like the following: C# ``` services .AddGraphQLServer() .AddType(() => new UuidType("Uuid")); ``` Further, we changed the default serialization of UUID values from format `N` (`nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn`) to format `D` (`nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn`). While the format `N` saved a few payload characters, new users, in general, often had issues with that format and some other tooling. New users will now, by default, have a better experience when using non-ChilliCream tooling. To preserve the old format, you can directly provide the format in the scalar. C# ``` services .AddGraphQLServer() .AddType(() => new UuidType(defaultFormat: 'N')); ``` In order to fully preserve version 11 behavior do: C# ``` services .AddGraphQLServer() .AddType(() => new UuidType("Uuid", defaultFormat: 'N')); ``` ### URL We changed the name of the URL scalar from `Url` to `URL`. To maintain the old name, register the type manually like the following: C# ``` services .AddGraphQLServer() .AddType(() => new UrlType("Url")); ``` ## Pagination ### ConnectionType In v12 we have removed the `ConnectionType` and `ConnectionType`. **v11** C# ``` descriptor .Field("users") .UsePaging() .Type>() .Resolver(context => { // Omitted code for brevity }); ``` **v12** C# ``` descriptor .Field("users") .UsePaging() .Resolver(context => { // Omitted code for brevity }); ``` ### Connection naming We have changed the way we infer the name for the connection type when using cursor-based pagination. By default, the connection name is now inferred from the field name instead of the type name. SDL ``` type Person { friends: [Person] } ``` In version 11, we would have created a connection named `PersonConnection`. SDL ``` type Person { friends(first: Int, last: Int, after: String, before: String): PersonConnection } ``` In version 12, we now will infer the connection name as `FriendsConnection`. SDL ``` type Person { friends(first: Int, last: Int, after: String, before: String): FriendsConnection } ``` To keep your schema stable when you migrate, you can switch the behavior back to how you did in version 11. C# ``` services .AddGraphQLServer() .SetPagingOptions(new PagingOptions{ InferConnectionNameFromField = false }) ... ``` Moreover, you now can explicitly define the connection name per field. C# ``` public class Person { [UsePaging(ConnectionName = "Persons")] public IQueryable GetFriends() => ... } ``` [Reference](https://chillicream.com/docs/hotchocolate/fetching-data/pagination#connection-naming) ### MongoDB Paging In version 11 we had the `UseMongoDbPagingAttribute` and the `UseMongoDbOffsetPagingAttribute`, which we removed with version 11\. In version 12 you now can use the standard attributes `UsePagingAttribute` and `UseOffsetPagingAttribute`. To use these attributes with mongo, you need to register the mongo paging provider with your GraphQL configuration: C# ``` services .AddGraphQLServer() .AddMongoDbPagingProviders() ... ``` [Reference](https://chillicream.com/docs/hotchocolate/fetching-data/pagination) ## Records With version 11, we added support for records and added the ability to infer attributes from parameters. This, in the end, leads to more errors than benefits. With version 12, we removed this feature. Use the official' property' keyword to write records in C# short-hand syntax when annotating properties. C# ``` public record Foo([property: ID] string Id); ``` ## Instrumentation We added more instrumentation events and generalized more how one can tap into our internal events. The class `DiagnosticEventListener` is now obsolete and replaced with `ExecutionDiagnosticEventListener`. This is due to new event listener classes like `DataLoaderDiagnosticEventListener`. Most virtual methods previously returning IActivityScope now return IDisposable. [Learn more about instrumentation](https://chillicream.com/docs/hotchocolate/server/instrumentation) ## Relay Previously the configuration of the Relay integration was focused around the `EnableRelaySupport()` method. It allowed you to enable Global Object Identification and automatically adding a query field to mutation payloads. The problem is that `EnableRelaySupport()` always enabled the Global Object Identification feature. This is not obviously implied by the name and also prevents you from using the other feature in isolation. Therefore we introduced two separate APIs to give you more explicit control over which parts of the Relay integration you want to enable. ### Global Object Identification **v11** C# ``` services .AddGraphQLServer() .EnableRelaySupport(); ``` **v12** C# ``` services .AddGraphQLServer() .AddGlobalObjectIdentification(); ``` [Learn more about Global Object Identification](https://chillicream.com/docs/hotchocolate/defining-a-schema/relay#global-object-identification) ### Query field in Mutation payloads **v11** C# ``` services .AddGraphQLServer() .EnableRelaySupport(new RelayOptions { AddQueryFieldToMutationPayloads = true, QueryFieldName = "rootQuery", MutationPayloadPredicate = type => type.Name.Value.EndsWith("Result") }); ``` **v12** C# ``` services .AddGraphQL() .AddQueryFieldToMutationPayloads(options => { options.QueryFieldName = "rootQuery"; options.MutationPayloadPredicate = type => type.Name.Value.EndsWith("Result"); }); ``` If you just want to enable the feature without further configuration, you can omit the `options =>` action. Warning Since `EnableRelaySupport()` previously always implied the usage of Global Object Identification, you might have to enable Global Object Identification separately as well. [Learn more about Query field in Mutation payloads](https://chillicream.com/docs/hotchocolate/defining-a-schema/relay#query-field-in-mutation-payloads) ## DataLoader We have consolidated the DataLoader base classes into the GreenDonut package which has no dependency on any HotChocolate packages. This allows for people using DataLoader in their business layer without having to reference GraphQL related packages. In your DataLoader classes the namespace `HotChocolate.Fetching` and `HotChocolate.DataLOader` are no longer needed. Second, we optimized memory usage of DataLoader and it is now best practice to let the DI inject the DataLoaderOptions into the DataLoader. **v11** C# ``` public class CustomBatchDataLoader : BatchDataLoader { public CustomBatchDataLoader(IBatchScheduler batchScheduler) : base(batchScheduler) { } // code omitted for brevity. } ``` **v12** C# ``` public class CustomBatchDataLoader : BatchDataLoader { public CustomBatchDataLoader(IBatchScheduler batchScheduler, DataLoaderOptions options) : base(batchScheduler, options) { } // code omitted for brevity. } ``` Allowing the DI to inject the options will allow the DataLoader to use the new shared pooled cache objects. ## Custom naming conventions If you're using a custom naming convention and have xml documentation enabled, you'll need to modify the way the naming convention is hooked up else your comments will disappear from your schema. **v11** C# ``` public class CustomNamingConventions : DefaultNamingConventions { public CustomNamingConventions() : base() { } } services .AddGraphQLServer() .AddConvention(sp => new CustomNamingConventions()) // or .AddConvention(); ``` **v12** C# ``` public class CustomNamingConventions : DefaultNamingConventions { public CustomNamingConventions(IDocumentationProvider documentationProvider) : base(documentationProvider) { } } IReadOnlySchemaOptions capturedSchemaOptions; services .AddGraphQLServer() .ModifyOptions(opt => capturedSchemaOptions = opt) .AddConvention(sp => new CustomNamingConventions( new XmlDocumentationProvider( new XmlDocumentationFileResolver( capturedSchemaOptions.ResolveXmlDocumentationFileName), sp.GetApplicationService>() ?? new NoOpStringBuilderPool()))); ``` ## Miscellaneous - `IObjectField` - If you were using `IObjectField.Member`, you'll likely want to move to `IObjectField.ResolverMember` (as `.Member` can be `null` in some cases now where it previously wasn't; and `.ResolverMember` will fall back to `.Member`). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/migrating/migrate-from-11-to-12.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Migrate Hot Chocolate from 12 to 13 - Hot Chocolate > Upgrade your Hot Chocolate GraphQL server from version 12 to 13 with this migration guide covering package updates and the breaking changes you need to address. Canonical source: https://chillicream.com/docs/hotchocolate/migrating/migrate-from-12-to-13 This guide will walk you through the manual migration steps to update your Hot Chocolate GraphQL server to version 13. Start by installing the latest `13.x.x` version of **all** of the `HotChocolate.*` packages referenced by your project. ## Breaking changes Things that have been removed or had a change in behavior that may cause your code not to compile or lead to unexpected behavior at runtime if not addressed. ### @authorize on types If you previously annotated a type with `@authorize`, either directly in the schema or via `[Authorize]` or `descriptor.Authorize()`, the authorization rule was copied to each field of this type. This meant the authorization rule would be evaluated for each selected field beneath the annotated type in a request. This is inefficient, so we switched to evaluating the authorization rule **once** on the field that returns the "authorized" type instead. Let's imagine you currently have the following GraphQL schema: GraphQL ``` type Query { user: User } type User @authorize { field1: String field2: Int } ``` This is how the authorization rule would be evaluated previously and now: **Before** GraphQL ``` { user { # The authorization rule is evaluated here since this field is beneath # the `User` type, which is annotated with @authorize field1 # The authorization rule is evaluated here since this field is beneath # the `User` type, which is annotated with @authorize field2 } } ``` **After** GraphQL ``` { # The authorization rule is now evaluated here since the `user` field # returns the `User` type, which is annotated with @authorize user { field1 field2 } } ``` We observed a common pattern to put a '@authorize' directive on the root types and secure all their fields. With the new default behavior of authorization, this would now fail since annotating the type will ensure that all fields returning instances of this type will be validated. Since there is no field returning the root types in most cases, these authorization rules will have no effect. With the Authorization overhaul, we also introduced a way to more efficiently implement such a pattern by moving parts of the authorization into the validation. GraphQL ``` type Query @authorize(apply: VALIDATION) { user: User } type User { field1: String field2: Int } ``` The ‘apply‘ argument defines when an authorization rule is applied. In the above case, the validation ensures that the GraphQL request documents authorization rules are fulfilled. We do that by collecting all authorization directives with ‘apply‘ set to ‘Validation‘ and running them before we start the execution. ### RegisterDbContext We changed the default [DbContextKind](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework) from [DbContextKind.Synchronized](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework) to [DbContextKind.Resolver](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework). If the instance of your `DbContext` doesn't need to be the same for each executed resolver during a request, this should lead to a performance improvement. To restore the v12 default behavior, pass the [DbContextKind.Synchronized](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework) to the `RegisterDbContext` call. **Before** C# ``` services.AddGraphQLServer() .RegisterDbContext() ``` **After** C# ``` services.AddGraphQLServer() .RegisterDbContext(DbContextKind.Synchronized) ``` Note Only add this if your application requires it. You're better off with the new default otherwise. ### Batching As an added security measure, batching has been disabled per default in this release. If you require batching, you now need to explicitly enable it. C# ``` app.MapGraphQL().WithOptions(new GraphQLServerOptions { EnableBatching = true }); ``` Previously you might have also configured the ([now replaced](#ihttpresultserializer)) `IHttpResultSerializer` to produce a `JsonArray` for your batches: C# ``` services.AddHttpResultSerializer(batchSerialization: HttpResultSerialization.JsonArray) ``` This option has been removed in this release and batch results are now always being delivered through `multipart/mixed` responses. This allows us to send the batch results back to the client as soon as they are ready, without having to hold on to the result and performing a JSON array aggregation on the server. If you need an aggregated batch result, you should do the aggregation on the client instead. [Learn more about the batching](https://chillicream.com/docs/hotchocolate/server/batching) ### Nodes batch size The number of nodes that can be requested through the `nodes` field is limited to 10 by default. See [Nodes batch size](https://chillicream.com/docs/hotchocolate/security/request-limits#nodes-batch-size) for the details. You can change this default to suite the needs of your application as shown below: C# ``` builder.Services.AddGraphQLServer() .ModifyOptions(o => o.MaxAllowedNodeBatchSize = 1); ``` ### UseOffsetPaging In this release we aligned the naming of types generated by `UseOffsetPaging` with the behavior of `UsePaging`. C# ``` [UseOffsetPaging] public IQueryable GetUserOrders() => ... ``` The above resolver would previously generated a schema like this: GraphQL ``` type Query { userOrders(skip: Int, take: Int): OrderCollectionSegment } type OrderCollectionSegment { items: [Order!] pageInfo: CollectionSegmentInfo! } ``` Notice how the CollectionSegment is named after the type being returned and not the name of the field. If you were to create a second resolver returning the same type also annotated with `UseOffsetPaging`, the previous CollectionSegment would be re-used. This could lead to issues, if you wanted to add additional information to only one of the CollectionSegments. Given the same resolver from above, we now generate the following schema in v13, where the CollectionSegment is named after the field it's being returned from and not the field's return type: GraphQL ``` type Query { userOrders(skip: Int, take: Int): UserOrdersCollectionSegment } type UserOrdersCollectionSegment { items: [Order!] pageInfo: CollectionSegmentInfo! } ``` If you want to retain the old behavior, you can disable the inferring from the field name either on a per-field basis C# ``` [UseOffsetPaging(InferCollectionSegmentNameFromField = false)] ``` or change the global default C# ``` builder.Services .AddGraphQLServer() .SetPagingOptions(new PagingOptions { InferCollectionSegmentNameFromField = false }); ``` ### Field naming Previously only the first character in a property or method name was lowercased in the schema. This worked fine in most cases, but if a name started with multiple uppercase characters or was all uppercase, the resulting field name was pretty weird. In this release we therefore changed how those field names are being inferred. **Before** ``` FooBar --> fooBar IPAddress --> iPAddress PLZ --> pLZ ``` **After** ``` FooBar --> fooBar IPAddress --> ipAddress PLZ --> plz ``` If you need to retain the old naming behavior or the inferred field name doesn't match your expectation, you can still [explicitly override the name of the fields in question](https://chillicream.com/docs/hotchocolate/defining-a-schema/object-types#renaming-fields). ### IHttpResultSerializer In this release we have replaced the `IHttpResultSerializer`, and consequently the `DefaultHttpResultSerializer`, with the `IHttpResponseFormatter` and `DefaultHttpResponseFormatter`. Below you can see how you can port your custom HTTP status code generation logic to the new contract: **Before** C# ``` builder.Services.AddHttpResultSerializer(); // ... public class CustomHttpResultSerializer : DefaultHttpResultSerializer { public override HttpStatusCode GetStatusCode(IExecutionResult result) { if (result is IQueryResult queryResult && queryResult.Errors?.Count > 0 && queryResult.Errors.Any(error => error.Code == "SOME_AUTH_ISSUE")) { return HttpStatusCode.Forbidden; } return base.GetStatusCode(result); } } ``` **After** C# ``` builder.Services.AddHttpResponseFormatter(); // ... public class CustomHttpResponseFormatter : DefaultHttpResponseFormatter { protected override HttpStatusCode OnDetermineStatusCode( IQueryResult result, FormatInfo format, HttpStatusCode? proposedStatusCode) { if (result.Errors?.Count > 0 && result.Errors.Any(error => error.Code == "SOME_AUTH_ISSUE")) { return HttpStatusCode.Forbidden; } return base.OnDetermineStatusCode(result, format, proposedStatusCode); } } ``` ### HTTP transport With this release we adopted the latest [GraphQL over HTTP](https://github.com/graphql/graphql-over-http/blob/a1e6d8ca248c9a19eb59a2eedd988c204909ee3f/spec/GraphQLOverHTTP.md) specification changes. Most notably the server now returns the `Content-Type: application/graphql-response+json;charset=utf-8` response header for requests without an `Accept: application/json` request header. This might break expectations of existing clients. Apollo Federation Gateway (`@apollo/gateway`) for example still expects responses from subgraphs to contain the `Content-Type: application/json` header. If you need to support legacy clients that do not yet support the [GraphQL over HTTP](https://github.com/graphql/graphql-over-http/blob/a1e6d8ca248c9a19eb59a2eedd988c204909ee3f/spec/GraphQLOverHTTP.md) specification, you can 1. Send the `Accept: application/json` header in requests from the legacy client 2. Infer `application/json` as the `Accept` header value for requests with a missing `Accept` header or `Accept: */*`, by setting the `HttpTransportVersion` to `Legacy`: C# ``` builder.Services.AddHttpResponseFormatter(new HttpResponseFormatterOptions { HttpTransportVersion = HttpTransportVersion.Legacy }); ``` An `Accept` header with the value `application/json` will opt you out of the [GraphQL over HTTP](https://github.com/graphql/graphql-over-http/blob/a1e6d8ca248c9a19eb59a2eedd988c204909ee3f/spec/GraphQLOverHTTP.md) specification. The response `Content-Type` will now be `application/json` and a status code of 200 will be returned for every request, even if it had validation errors or a valid response could not be produced. [Learn more about the HTTP transport](https://chillicream.com/docs/hotchocolate/server/http-transport) ### DataLoaderAttribute Previously you might have annotated [DataLoaders](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader) in your resolver method signature with the `[DataLoader]` attribute. This attribute has been removed in v13 and can be safely removed from your code. **Before** C# ``` public async Task GetUserByIdAsync(string id, [DataLoader] UserDataLoader loader) => await loader.LoadAsync(id); ``` **After** C# ``` public async Task GetUserByIdAsync(string id, UserDataLoader loader) => await loader.LoadAsync(id); ``` ### ITopicEventReceiver / ITopicEventSender Previously you could use any type as the topic for an event stream. In this release we are requiring the topic to be a `string`. **Before** C# ``` ITopicEventReceiver.SubscribeAsync(TTopic topic, CancellationToken cancellationToken); ITopicEventSender.SendAsync(TTopic topic, TMessage message, CancellationToken cancellationToken) ``` **After** C# ``` ITopicEventReceiver.SubscribeAsync(string topicName, CancellationToken cancellationToken); ITopicEventSender.SendAsync(string topicName, TMessage message, CancellationToken cancellationToken) ``` ### TopicAttribute Previously you might have annotated the `[Topic]` attribute on a method argument, to designate its runtime value as a dynamic topic. Now we no longer allow the attribute on arguments, but only on the method itself. **Before** C# ``` public class Subscription { [Subscribe] public Book BookPublished([Topic] string author, [EventMessage] Book book) => book; } ``` **After** C# ``` public class Subscription { [Subscribe] // What's in between the curly braces must match an argument name. [Topic("{author}")] public Book BookPublished(string author, [EventMessage] Book book) => book; } ``` ### AddInMemorySubscriptions / AddRedisSubscriptions We moved the extension methods from the `IServiceCollection` to our `IRequestExecutorBuilder`. **Before** C# ``` builder.Services.AddInMemorySubscriptions(); // or builder.Services.AddRedisSubscriptions(); ``` **After** C# ``` builder.Services .AddGraphQLServer() .AddInMemorySubscriptions() // or .AddRedisSubscriptions(); ``` ### @defer / @stream `@defer` and `@stream` have now been disabled per default. If you want to continue using them, you have to opt-in now: C# ``` services.AddGraphQLServer() .ModifyOptions(o => { o.EnableDefer = true; o.EnableStream = true; }); ``` If your client is setting an `Accept` header value that doesn't include `multipart/mixed` or `text/event-stream`, the server will no longer produce a response, because the client is signaling that it can't handle a streamed response. In order for the server to produce a streamed response, you now need to either 1. Omit the `Accept` header 2. Send an `Accept` header with the value `*/*`, signaling that your client can handle any format 3. Send an `Accept` header which includes `multipart/mixed` and/or `text/event-stream` There have also been changes to the response format of streamed responses. You can checkout the currently proposed format [here](https://github.com/graphql/graphql-spec/blob/94363c9d5d8e53e91240ea3eabd32ff522f27a6b/spec/Section%207%20--%20Response.md). Warning The spec of these features is still evolving, so expect more changes on how the incremental payloads are being delivered. ### NameString In this release we have abandoned the `NameString` in favor of simple `string`s. Most commonly you would encounter the `NameString` when defining names for fields or types. Since `string` was already implicitly converted to `NameString`, there shouldn't be any issues unless you were instantiating a `NameString` yourself. ### IResolverContext / IMiddlewareContext Previously you could access properties like `Document` and `RootType` directly on the `IResolverContext` or the `IMiddlewareContext`. In this release we have moved these properties and they can now be accessed through the `Operation` property on the contexts. We have also removed the deprecated properties `Field` and `FieldSelection`. `context.Document` \--> `context.Operation.Document` `context.RootType` \--> `context.Operation.RootType` `context.Field` \--> `context.Selection.Field` `context.FieldSelection` \--> `context.Selection.SyntaxNode` ## Deprecations Things that will continue to function this release, but we encourage you to move away from. ### ScopedServiceAttribute In this release, we are deprecating the `[ScopedService]` attribute and encourage you to use `RegisterDbContext(DbContextKind.Pooled)` instead. Checkout [this part of our Entity Framework documentation](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework) to learn how to register your `DbContext` with `DbContextKind.Pooled`. Afterward you just need to update your resolvers: **Before** C# ``` [UseDbContext] public IQueryable GetUsers([ScopedService] MyDbContext dbContext) => dbContext.Users; ``` **After** C# ``` public IQueryable GetUsers(MyDbContext dbContext) => dbContext.Users; ``` If you've been using `[ScopedService]` without a pooled `DbContext`, you can recreate its behavior by switching it out for `[LocalState("FullName")]` (where `FullName` is the [full name](https://learn.microsoft.com/dotnet/api/system.type.fullname) of the method argument type). ### SubscribeAndResolve **Before** C# ``` public class Subscription { [SubscribeAndResolve] public ValueTask> BookPublished(string author, [Service] ITopicEventReceiver receiver) { var topic = $"{author}_PublishedBook"; return receiver.SubscribeAsync(topic); } } ``` **After** C# ``` public class Subscription { public ValueTask> SubscribeToPublishedBooks( string author, ITopicEventReceiver receiver) { var topic = $"{author}_PublishedBook"; return receiver.SubscribeAsync(topic); } [Subscribe(With = nameof(SubscribeToPublishedBooks))] public Book BookPublished(string author, [EventMessage] Book book) => book; } ``` ### LocalValue / ScopedValue / GlobalValue We aligned the naming of state related APIs: #### IResolverContext - `IResolverContext.GetGlobalValue` \--> `IResolverContext.GetGlobalStateOrDefault` - `IResolverContext.GetOrAddGlobalValue` \--> `IResolverContext.GetOrSetGlobalState` - `IResolverContext.SetGlobalValue` \--> `IResolverContext.SetGlobalState` - `IResolverContext.RemoveGlobalValue` \--> _Removed_ - `IResolverContext.GetScopedValue` \--> `IResolverContext.GetScopedStateOrDefault` - `IResolverContext.GetOrAddScopedValue` \--> `IResolverContext.GetOrSetScopedState` - `IResolverContext.SetScopedValue` \--> `IResolverContext.SetScopedState` - `IResolverContext.RemoveScopedValue` \--> `IResolverContext.RemoveScopedState` - `IResolverContext.GetLocalValue` \--> `IResolverContext.GetLocalStateOrDefault` - `IResolverContext.GetOrAddLocalValue` \--> `IResolverContext.GetOrSetLocalState` - `IResolverContext.SetLocalValue` \--> `IResolverContext.SetLocalState` - `IResolverContext.RemoveLocalValue` \--> `IResolverContext.RemoveLocalState` #### OperationRequestBuilder - `OperationRequestBuilder.SetProperties` \--> `OperationRequestBuilder.InitializeGlobalState` - `OperationRequestBuilder.SetProperty` \--> `OperationRequestBuilder.SetGlobalState` - `OperationRequestBuilder.AddProperty` \--> `OperationRequestBuilder.AddGlobalState` - `OperationRequestBuilder.TryAddProperty` \--> `OperationRequestBuilder.TryAddGlobalState` - `OperationRequestBuilder.TryRemoveProperty` \--> `OperationRequestBuilder.RemoveGlobalState` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/migrating/migrate-from-12-to-13.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Migrate Hot Chocolate from 13 to 14 - Hot Chocolate > Migration guide for moving a Hot Chocolate GraphQL server from version 13 to 14, walking through the breaking changes and manual update steps involved. Canonical source: https://chillicream.com/docs/hotchocolate/migrating/migrate-from-13-to-14 This guide will walk you through the manual migration steps to update your Hot Chocolate GraphQL server to version 14. Start by installing the latest `14.x.x` version of **all** of the `HotChocolate.*` packages referenced by your project. > This guide is still a work in progress with more updates to follow. ## Breaking changes Things that have been removed or had a change in behavior that may cause your code not to compile or lead to unexpected behavior at runtime if not addressed. ### Banana Cake Pop and Barista renamed to Nitro | Old | New | Notes | | --------------------------------------------- | ------------------------------------- | ------------------------------------ | | AddBananaCakePopExporter | AddNitroExporter | | | AddBananaCakePopServices | AddNitro | | | BananaCakePop.Middleware | ChilliCream.Nitro.App | | | BananaCakePop.Services | ChilliCream.Nitro | | | BananaCakePop.Services.Azure | ChilliCream.Nitro.Azure | | | BananaCakePop.Services.Fusion | ChilliCream.Nitro.Fusion | | | barista | nitro | CLI executable | | Barista | ChilliCream.Nitro.CLI | CLI NuGet package | | BARISTA\_API\_ID | NITRO\_API\_ID | | | BARISTA\_API\_KEY | NITRO\_API\_KEY | | | BARISTA\_CLIENT\_ID | NITRO\_CLIENT\_ID | | | BARISTA\_OPERATIONS\_FILE | NITRO\_OPERATIONS\_FILE | | | BARISTA\_OUTPUT\_FILE | NITRO\_OUTPUT\_FILE | | | BARISTA\_SCHEMA\_FILE | NITRO\_SCHEMA\_FILE | | | BARISTA\_STAGE | NITRO\_STAGE | | | BARISTA\_SUBGRAPH\_ID | NITRO\_SUBGRAPH\_ID | | | BARISTA\_SUBGRAPH\_NAME | NITRO\_SUBGRAPH\_NAME | | | BARISTA\_TAG | NITRO\_TAG | | | bcp | nitro | Key in subgraph-config.json | | bcp-config.json | nitro-config.json | | | BCP\_API\_ID | NITRO\_API\_ID | | | BCP\_API\_KEY | NITRO\_API\_KEY | | | BCP\_STAGE | NITRO\_STAGE | | | eat.bananacakepop.com | nitro.chillicream.com | | | MapBananaCakePop | MapNitroApp | | | @chillicream/bananacakepop-express-middleware | @chillicream/nitro-express-middleware | | | @chillicream/bananacakepop-graphql-ide | @chillicream/nitro-embedded | mode: "self" is now mode: "embedded" | ### Dependency injection changes - It is no longer necessary to use the `[Service]` attribute unless you're using keyed services, in which case the attribute is used to specify the key. - Hot Chocolate will identify services automatically. - Support for the `[FromServices]` attribute has been removed. - As with the `[Service]` attribute above, this attribute is no longer necessary. - Since the `RegisterService` method is no longer required, it has been removed, along with the `ServiceKind` enum. - Scoped services injected into query resolvers are now resolver-scoped by default (not request scoped). For mutation resolvers, services are request-scoped by default. - The default scope can be changed in two ways: 1. Globally, using `ModifyOptions`: C# ``` builder.Services .AddGraphQLServer() .ModifyOptions(o => { o.DefaultQueryDependencyInjectionScope = DependencyInjectionScope.Resolver; o.DefaultMutationDependencyInjectionScope = DependencyInjectionScope.Request; }); ``` 2. On a per-resolver basis, with the `[UseRequestScope]` or `[UseResolverScope]` attribute. - Note: The `[UseServiceScope]` attribute has been removed. For more information, see the [Dependency Injection](https://chillicream.com/docs/hotchocolate/resolvers/dependency-injection) documentation. ### Entity framework integration changes - The `RegisterDbContext` method is no longer required, and has therefore been removed, along with the `DbContextKind` enum. - Use `RegisterDbContextFactory` to register a DbContext factory. For more information, see the [Entity Framework integration](https://chillicream.com/docs/hotchocolate/fetching-data/integrations/entity-framework) documentation. ### New GID format This release introduces a more performant GID serializer, which also simplifies the underlying format of globally unique IDs. By default, the new serializer will be able to parse both the old and new ID format, while only emitting the new format. This change is breaking if your consumers depend on the format of the GIDs, by for example parsing them (which they shouldn't). If possible, strive to decouple your consumers from the internal ID format and exposing the underlying ID as a separate field on your type if necessary. If you don't want to switch to the new format yet, you can register the legacy serializer, which only supports parsing and emitting the old ID format: C# ``` builder.Services .AddGraphQLServer() .AddLegacyNodeIdSerializer() .AddGlobalObjectIdentification(); ``` Note `AddLegacyNodeIdSerializer()` needs to be called before `AddGlobalObjectIdentification()`. #### How to adopt incrementally in a distributed system None of your services can start to emit the new ID format, as long as there are services that can't parse the new format. Therefore, you'll first want to make sure that all of your services support parsing both the old and new format, while still emitting the old format. This can be done, by configuring the new default serializer to not yet emit the new format: C# ``` builder.Services .AddGraphQLServer() .AddDefaultNodeIdSerializer(outputNewIdFormat: false) .AddGlobalObjectIdentification(); ``` Note `AddDefaultNodeIdSerializer()` needs to be called before `AddGlobalObjectIdentification()`. Once all of your services have been updated to this, you can start emitting the new format service-by-service, by removing the `AddDefaultNodeIdSerializer()` call and switching to the new default behavior: C# ``` builder.Services .AddGraphQLServer() .AddGlobalObjectIdentification(); ``` ### Node Resolver validation We now enforce that each object type implementing the `Node` interface also defines a resolver, so that the object can be refetched through the `node(id: ID!)` field. You can opt out of this new behavior by setting the `EnsureAllNodesCanBeResolved` option to `false`. C# ``` builder.Services .AddGraphQLServer() .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) ``` ### Builder APIs We have aligned all builder APIs to be more consistent and easier to use. Builders can now be created by using the static method `Builder.New()` and the `Build()` method to create the final object. #### IQueryRequestBuilder replaced by OperationRequestBuilder The interface `IQueryRequestBuilder` and its implementations were replaced with `OperationRequestBuilder` which now supports building standard GraphQL operation requests as well as variable batch requests. The `Build()` method returns now a `IOperationRequest` which is implemented by `OperationRequest` and `VariableBatchRequest`. We have also simplified what the builder does and removed a lot of the convenience methods that allowed to add single variables to it. This has todo with the support of variable batching. Now, you have to provide the variable map directly. #### IQueryResultBuilder replaced by OperationResultBuilder The interface `IQueryResultBuilder` and its implementations were replaced with `OperationResultBuilder` which produces an `OperationResult` on `Build()`. #### IQueryResult replaced by IOperationResult The interface `IQueryResult` was replaced with `IOperationResult`. ### Operation complexity analyzer replaced The Operation Complexity Analyzer in v13 has been replaced by Cost Analysis in v14, based on the draft [IBM Cost Analysis specification](https://ibm.github.io/graphql-specs/cost-spec.html). - The `Complexity` property on `RequestExecutorOptions` (accessed via `ModifyRequestOptions`) has been removed. - Cost analysis is enabled by default. Please see the [documentation](https://chillicream.com/docs/hotchocolate/security/cost-analysis) for further information. ### DateTime scalar enforces a specific format The `DateTime` scalar will now enforce a specific format. The time and offset are now required, and fractional seconds are limited to 7\. This aligns it with the DateTime Scalar spec (), with the one difference being that fractions of a second are optional, and 0-7 digits may be specified. Please ensure that your clients are sending date/time strings in the correct format to avoid errors. You can opt out of the format check with the following code: C# ``` builder.Services .AddGraphQLServer() .AddType(new DateTimeType(disableFormatCheck: true)); ``` ### Persisted Queries renamed to Persisted Operations #### Packages renamed | Old package name | New package name | | ---------------------------------------- | ------------------------------------------- | | HotChocolate.PersistedQueries.FileSystem | HotChocolate.PersistedOperations.FileSystem | | HotChocolate.PersistedQueries.InMemory | HotChocolate.PersistedOperations.InMemory | | HotChocolate.PersistedQueries.Redis | HotChocolate.PersistedOperations.Redis | #### Interfaces renamed | Old interface name | New interface name | | ------------------------------ | ---------------------------------- | | IPersistedQueryOptionsAccessor | IPersistedOperationOptionsAccessor | #### Methods renamed | Old method name | New method name | | ---------------------------------- | -------------------------------------- | | UsePersistedQueryPipeline | UsePersistedOperationPipeline | | UseAutomaticPersistedQueryPipeline | UseAutomaticPersistedOperationPipeline | | AddFileSystemQueryStorage | AddFileSystemOperationDocumentStorage | | AddInMemoryQueryStorage | AddInMemoryOperationDocumentStorage | | AddRedisQueryStorage | AddRedisOperationDocumentStorage | | AllowNonPersistedQuery | AllowNonPersistedOperation | | UseReadPersistedQuery | UseReadPersistedOperation | | UseAutomaticPersistedQueryNotFound | UseAutomaticPersistedOperationNotFound | | UseWritePersistedQuery | UseWritePersistedOperation | #### Options renamed | Old option name | New option name | | ----------------------------------- | ----------------------------------------------- | | OnlyAllowPersistedQueries | PersistedOperations.OnlyAllowPersistedDocuments | | OnlyPersistedQueriesAreAllowedError | PersistedOperations.OperationNotAllowedError | #### Defaults changed | Parameter | Old default | New default | | -------------- | -------------------- | ----------------------- | | cacheDirectory | "persisted\_queries" | "persisted\_operations" | ### MutationResult renamed to FieldResult | Old name | New name | | ----------------------- | -------------------- | | MutationResult | FieldResult | | IMutationResult | IFieldResult | ### IReadStoredQueries and IWriteStoredQueries now IOperationDocumentStorage `IReadStoredQueries` and `IWriteStoredQueries` have been merged into a single interface named `IOperationDocumentStorage`. Renamed interface methods: | Old name | New name | | ----------------- | ------------ | | TryReadQueryAsync | TryReadAsync | | WriteQueryAsync | SaveAsync | ### Required keyed services Accessing a keyed service that has not been registered will now throw, instead of returning `null`. The return type is now non-nullable. This change aligns the API with the regular (non-keyed) service access API. ## Deprecations Things that will continue to function this release, but we encourage you to move away from. ### SetPagingOptions In an effort to align our configuration APIs, we're now also offering a delegate based configuration API for pagination options. **Before** C# ``` builder.Services .AddGraphQLServer() .SetPagingOptions(new PagingOptions { MaxPageSize = 100, DefaultPageSize = 25 }); ``` **After** C# ``` builder.Services .AddGraphQLServer() .ModifyPagingOptions(opt => { opt.MaxPageSize = 100; opt.DefaultPageSize = 25; }); ``` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/migrating/migrate-from-13-to-14.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Migrate Hot Chocolate from 14 to 15 - Hot Chocolate > Everything you need to upgrade a Hot Chocolate GraphQL server from version 14 to 15: supported target frameworks, runtime type changes, and scalar behavior. Canonical source: https://chillicream.com/docs/hotchocolate/migrating/migrate-from-14-to-15 This guide will walk you through the manual migration steps to update your Hot Chocolate GraphQL server to version 15. Start by installing the latest `15.x.x` version of **all** of the `HotChocolate.*` packages referenced by your project. > This guide is still a work in progress with more updates to follow. ## Breaking changes Things that have been removed or had a change in behavior that may cause your code not to compile or lead to unexpected behavior at runtime if not addressed. ### Supported target frameworks Support for .NET Standard 2.0, .NET 6, and .NET 7 has been removed. ### F# support removed `HotChocolate.Types.FSharp` has been replaced by the community project [FSharp.HotChocolate](https://www.nuget.org/packages/FSharp.HotChocolate). ### Runtime type changes - The runtime type for `LocalDateType` and `DateType` has been changed from `DateTime` to `DateOnly`. - The runtime type for `LocalTimeType` has been changed from `DateTime` to `TimeOnly`. ### DateTime serialized in universal time for the Date type `DateTime`s are now serialized in universal time for the `Date` type. For example, the `DateTime` `2018-06-11 02:46:14` in a time zone of `04:00` will now serialize as `2018-06-10` and not `2018-06-11`. Use the `LocalDate` type if you do not want the date to be converted to universal time. ### LocalDate, LocalTime, and Date scalars enforce a specific format - `LocalDate`: `yyyy-MM-dd` - `LocalTime`: `HH:mm:ss` - `Date`: `yyyy-MM-dd` Please ensure that your clients are sending date/time strings in the correct format to avoid errors. ### LocalDate and LocalTime scalars moved `LocalDate` and `LocalTime` have been moved from `HotChocolate.Types.Scalars` to `HotChocolate.Types`, and are therefore available without installing the additional package. ### DateOnly and TimeOnly binding change - `DateOnly` is now bound to `LocalDateType` instead of `DateType`. - `TimeOnly` is now bound to `LocalTimeType` instead of `TimeSpanType`. ### DataLoaderOptions are now required Starting with Hot Chocolate 15, the `DataLoaderOptions` must be passed down to the DataLoaderBase constructor. C# ``` public class ProductByIdDataLoader : BatchDataLoader { private readonly IServiceProvider _services; public ProductDataLoader1( IBatchScheduler batchScheduler, DataLoaderOptions options) // the options are now required ... : base(batchScheduler, options) { } } ``` ### DataLoader Dependency Injection DataLoader must not be manually registered with the dependency injection and must use the extension methods provided by GreenDonut. C# ``` services.AddDataLoader(); services.AddDataLoader(); services.AddDataLoader(sp => ....); ``` We recommend to use the source-generated DataLoaders and let the source generator write the registration code for you. > If you register DataLoader manually they will be stuck in the auto-dispatch mode, which basically means that they will no longer batch. DataLoader are available as scoped services and can be injected like any other scoped service. C# ``` public class ProductService(IProductByIdDataLoader productByIdData) { public async Task GetProductById(int id) { return await productByIdDataLoader.LoadAsync(id); } } ``` ## Deprecations ### GroupDataLoader We no longer recommend using the `GroupDataLoader`, as the same functionality can be achieved with a BatchDataLoader, which provides greater flexibility in determining the type of list returned. Use the following patter to replace the `GroupDataLoader`: C# ``` internal static class ProductDataLoader { [DataLoader] public static async Task> GetProductsByBrandIdAsync( IReadOnlyList brandIds, CatalogContext context, CancellationToken cancellationToken) => await context.Products .Where(t => brandIds.Contains(t.BrandId)) .GroupBy(t => t.BrandId) .Select(t => new { t.Key, Items = t.OrderBy(p => p.Name).ToArray() }) .ToDictionaryAsync(t => t.Key, t => t.Items, cancellationToken); } ``` ### AdHoc DataLoader The ad-hoc DataLoader methods on IResolverContext have been deprecated. C# ``` public async Task GetProductById(int id, IResolverContext context, CatalogContext catalogContext) { return context .BatchDataLoader( (productIds, ct) => catalogContext.Products.Where(p => productIds.Contains(p.Id)).ToDictionaryAsync(p => p.Id, ct), "productById") .LoadAsync(id); } ``` Use the source-generated DataLoaders instead. C# ``` internal static class ProductDataLoader { [DataLoader] public static async Task> GetProductByIdAsync( IReadOnlyList productIds, CatalogContext context, CancellationToken cancellationToken) => await context.Products .Where(t => productIds.Contains(t.Id)) .ToDictionaryAsync(t => t.Id, cancellationToken); } ``` This approach leads to better resolvers and helps avoid errors when capturing the context within the resolver. C# ``` public async Task GetProductById(int id, IProductDataLoader productDataLoader) => await productDataLoader.LoadAsync(id); ``` To pass state to the DataLoader, use the new branching and state APIs available on the DataLoader. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/migrating/migrate-from-14-to-15.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Hot Chocolate 16 Migration Guide > Walks through upgrading your Hot Chocolate GraphQL server from version 15 to 16, covering eager initialization, service separation, and other breaking changes. Canonical source: https://chillicream.com/docs/hotchocolate/migrating/migrate-from-15-to-16 This guide will walk you through the manual migration steps to update your Hot Chocolate GraphQL server to version 16. Start by installing the latest `16.x.x` version of **all** of the `HotChocolate.*` packages referenced by your project. ## Breaking changes Things that have been removed or had a change in behavior that may cause your code not to compile or lead to unexpected behavior at runtime if not addressed. ### Eager initialization by default Previously, Hot Chocolate would only construct the schema and request executor upon the first request. This deferred initialization could create a performance penalty on initial requests and delayed the discovery of schema errors until runtime. To address this, we previously offered an `InitializeOnStartup` helper that would initialize the schema and request executor in a blocking hosted service during startup. This ensured everything GraphQL-related was ready before Kestrel began accepting requests. Since we believe eager initialization is the right default, it's now the standard behavior. This means your schema and request executor are constructed during application startup, before your server begins accepting traffic. As a bonus, this tightens your development loop, since schema errors surface immediately when you start debugging rather than only appearing when you send your first request. If you're currently using `InitializeOnStartup`, you can safely remove it. If you also provided the `warmup` argument to run a task during the initialization, you can migrate that task to the new `AddWarmupTask` API: Diff ``` builder.AddGraphQL() - .InitializeOnStartup(warmup: (executor, ct) => { /* ... */ }); + .AddWarmupTask((executor, ct) => { /* ... */ }); ``` Warmup tasks registered with `AddWarmupTask` run at startup **and** when the schema is updated at runtime by default. Check out the [documentation](https://chillicream.com/docs/hotchocolate/server/warmup), if you need your warmup task to only run at startup. If you need to preserve lazy initialization for specific scenarios (though this is rarely recommended), you can opt out by setting the `LazyInitialization` option to `true`: C# ``` builder.AddGraphQL() .ModifyOptions(options => options.LazyInitialization = true); ``` ### Clearer separation between schema and application services Hot Chocolate has long maintained a second `IServiceProvider` for schema services, separate from the application service provider where you register your services and configuration. This schema service provider is scoped to a particular schema and contains all of Hot Chocolate's internal services. To access application services within schema services like diagnostic event listeners or error filters, we previously used a combined service provider for activating various Hot Chocolate components. However, this approach made it difficult to track service origins and created challenges for AOT compatibility. Starting with v16, we're introducing a more explicit model where Hot Chocolate configuration is instantiated exclusively through the internal schema service provider. Application services must now be explicitly cross-registered in the schema service provider to be accessible. Diff ``` builder.Services.AddSingleton(); builder.AddGraphQL() + .AddApplicationService() // either .AddDiagnosticEventListener(); // or .AddDiagnosticEventListener(sp => new MyDiagnosticEventListener(sp.GetRequiredService())); public class MyDiagnosticEventListener(MyService service) : ExecutionDiagnosticEventListener; ``` Sometimes the registration of required services is not as obvious. For example, the types for logging are registered in framework code. Diff ``` builder.Services.AddLogging(); builder.AddGraphQL() + .AddApplicationService>() // either .AddDiagnosticEventListener(); // or .AddDiagnosticEventListener(sp => new MyLoggingDiagnosticEventListener(sp.GetRequiredService>())); public class MyLoggingDiagnosticEventListener(ILogger logger) : ExecutionDiagnosticEventListener; ``` Services registered via `AddApplicationService()` are resolved once during schema initialization from the application service provider and registered as singletons in the schema service provider. If you're using any of the following configuration APIs, ensure that the application services required for their activation are registered via `AddApplicationService()`: - `AddHttpRequestInterceptor` - `AddSocketSessionInterceptor` - `AddErrorFilter` - `AddDiagnosticEventListener` - `AddOperationCompilerOptimizer` - `AddRedisOperationDocumentStorage` - `AddAzureBlobStorageOperationDocumentStorage` - `AddInstrumentation` with a custom `ActivityEnricher` Note Service injection into resolvers is not affected by this change. If you need to access the application service provider from within the schema service provider, you can use: C# ``` IServiceProvider applicationServices = schemaServices.GetRootServiceProvider(); ``` ### Internal directives hidden from schema endpoint Previously, the `/graphql/schema.graphql` endpoint was returning the schema containing internal directives like `@authorize`. Starting with v16 the endpoint no longer includes internal directives by default. If you need to retain the previous behavior, set `DisableInternalDirectives` to `true` through `ModifyOptions`. This treats every directive as public, even directives that explicitly call `Internal()` and regardless of `DefaultDirectiveVisibility`: C# ``` builder.Services .AddGraphQLServer() .ModifyOptions(o => o.DisableInternalDirectives = true); ``` Be aware that internal directives may carry sensitive information (for example, authorization policies attached via `@authorize`). Only enable this if you understand and accept that risk. ### New analyzers If you reference the `HotChocolate.Types.Analyzers` package, version 16 ships a number of new analyzers that surface misconfigurations and discouraged patterns at compile time. Several of them report at `Error` severity, so they can break a build that compiled cleanly on version 15\. In most cases the fix is exactly what the diagnostic suggests. If you believe a diagnostic is a false positive, please [open an issue](https://github.com/ChilliCream/graphql-platform/issues). The following new analyzers report at `Error` severity and can fail your build: | ID | Title | What it reports | | ------ | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | HC0094 | Bind member not found | The member referenced by BindMember/nameof(...) does not exist on the target type. | | HC0095 | Bind member type mismatch | The type used in a nameof expression does not match the \[ObjectType\] type. | | HC0097 | Parent attribute type mismatch | A \[Parent\] parameter's type must be the parent runtime type or a base type/interface it implements. | | HC0098 | Parent method type mismatch | The type argument in Parent() must be the parent runtime type or a base type/interface it implements. | | HC0099 | QueryContext with UseProjection | A resolver with a QueryContext parameter cannot also use \[UseProjection\]. | | HC0100 | Data attribute order | \[UsePaging\], \[UseProjection\], \[UseFiltering\] and \[UseSorting\] must be applied in that order. | | HC0101 | QueryContext connection type mismatch | The QueryContext type argument must match the connection's node type. | | HC0092 | ID attribute redundant on node resolver parameters | \[ID\] is redundant on a \[NodeResolver\] parameter, since the attribute already declares the id parameter as an ID type. Remove \[ID\]. | | HC0093 | Node resolver must be public | A \[NodeResolver\] method must be public. | | HC0104 | Node resolver id parameter | The first parameter of a node resolver must be the node ID and must be named id. | | HC0105 | ID attribute must target the property | On a record parameter, the \[ID\] attribute must use the property: target specifier (\[property: ID\]). | | HC0106 | Microsoft authorization attribute not allowed | Use the \[Authorize\] attribute from HotChocolate.Authorization instead of the one from Microsoft.AspNetCore.Authorization. | ### Cache size configuration Previously, document and operation cache sizes were globally configured through the `IServiceCollection`. In an effort to align and properly scope our configuration APIs, we've moved the configuration of these caches to the `IRequestExecutorBuilder`. If you're currently calling `AddDocumentCache` or `AddOperationCache` directly on the `IServiceCollection`, move the configuration to `ModifyOptions` on the `IRequestExecutorBuilder`: Diff ``` -builder.Services.AddDocumentCache(200); -builder.Services.AddOperationCache(100); builder.AddGraphQL() + .ModifyOptions(options => + { + options.OperationDocumentCacheSize = 200; + options.PreparedOperationCacheSize = 100; + }); ``` If your application contains multiple GraphQL servers, the cache configuration has to be repeated for each one as the configuration is now scoped to a particular GraphQL server. If you were previously accessing `IDocumentCache` or `IPreparedOperationCache` through the root service provider, you now need to access it through the schema-specific service provider instead. For instance, to populate the document cache during startup, create a custom `IRequestExecutorWarmupTask` that injects `IDocumentCache`: C# ``` builder .AddGraphQL() .AddWarmupTask(); public class MyWarmupTask(IDocumentCache cache) : IRequestExecutorWarmupTask { public bool ApplyOnlyOnStartup => false; public async Task WarmupAsync( IRequestExecutor executor, CancellationToken cancellationToken) { // Modify the cache } } ``` ### Document hash provider configuration Previously, document hash providers were globally configured through the `IServiceCollection`. In an effort to align and properly scope our configuration APIs, we've moved the configuration of the hash provider to the `IRequestExecutorBuilder`. If you're currently calling `AddMD5DocumentHashProvider`, `AddSha256DocumentHashProvider` or `AddSha1DocumentHashProvider` directly on the `IServiceCollection`, move the call to the `IRequestExecutorBuilder`: Diff ``` -builder.Services.AddSha256DocumentHashProvider(); builder.AddGraphQL() + .AddSha256DocumentHashProvider() ``` If your application contains multiple GraphQL servers, the hash provider configuration has to be repeated for each one as the configuration is now scoped to a particular GraphQL server. ### NATS subscriptions now use the official NATS v2 client The `HotChocolate.Subscriptions.Nats` package now uses the official NATS v2 client packages. If you are migrating an application that previously used `AlterNats.Hosting`, replace it with `NATS.Extensions.Microsoft.DependencyInjection` and update your NATS client registration from `AddNats(...)` to `AddNatsClient(...)`. Diff ``` builder.Services - .AddNats(poolSize: 1, opts => opts with - { - Url = "nats://localhost:4222" - }); + .AddNatsClient(nats => nats.ConfigureOptions( + options => options.Configure( + opts => opts.Opts = opts.Opts with + { + Url = "nats://localhost:4222" + }))); builder .AddGraphQL() .AddSubscriptionType() .AddNatsSubscriptions(); ``` If your code directly references NATS client types, add the `NATS.Client.Core` package as well. ### MaxAllowedNodeBatchSize & EnsureAllNodesCanBeResolved options moved Diff ``` builder.AddGraphQL() - .ModifyOptions(options => - { - options.MaxAllowedNodeBatchSize = 100; - options.EnsureAllNodesCanBeResolved = false; - }) - .AddGlobalObjectIdentification() + .AddGlobalObjectIdentification(options => + { + options.MaxAllowedNodeBatchSize = 100; + options.EnsureAllNodesCanBeResolved = false; + }); ``` ### IRequestContext We've removed the `IRequestContext` abstraction in favor of the concrete `RequestContext` class. Additionally, all information related to the parsed operation document has been consolidated into a new `OperationDocumentInfo` class, accessible via `RequestContext.OperationDocumentInfo`. | Before | After | | --------------------------- | ----------------------------------------- | | context.DocumentId | context.OperationDocumentInfo.Id.Value | | context.Document | context.OperationDocumentInfo.Document | | context.DocumentHash | context.OperationDocumentInfo.Hash.Value | | context.ValidationResult | context.OperationDocumentInfo.IsValidated | | context.IsCachedDocument | context.OperationDocumentInfo.IsCached | | context.IsPersistedDocument | context.OperationDocumentInfo.IsPersisted | Here's how you would update a custom request middleware implementation: Diff ``` public class CustomRequestMiddleware { - public async ValueTask InvokeAsync(IRequestContext context) + public async ValueTask InvokeAsync(RequestContext context) { - string documentId = context.DocumentId; + string documentId = context.OperationDocumentInfo.Id.Value; await _next(context).ConfigureAwait(false); } } ``` ### IRequestExecutorResolver split into provider, events, and manager The single `IRequestExecutorResolver` interface from v15 has been removed and its responsibilities split across three more focused abstractions in `HotChocolate.Execution.Abstractions`: | v15 (IRequestExecutorResolver) | v16 | | ----------------------------------- | -------------------------------------------------------------------------- | | GetRequestExecutorAsync(schemaName) | IRequestExecutorProvider.GetExecutorAsync(schemaName) | | Events (IObservable<…>) | IRequestExecutorEvents (which itself is IObservable) | | EvictRequestExecutor(schemaName) | IRequestExecutorManager.EvictExecutor(schemaName) | `IRequestExecutorManager` derives from `IRequestExecutorProvider`, so inject the manager when you need both lookup and eviction, the provider when you only need lookup, and `IRequestExecutorEvents` when you only need to react to executor lifecycle events: Diff ``` -public class MyService(IRequestExecutorResolver resolver) +public class MyService( + IRequestExecutorManager executors, + IRequestExecutorEvents events) { - public ValueTask GetExecutorAsync(CancellationToken ct) - => resolver.GetRequestExecutorAsync(cancellationToken: ct); + public ValueTask GetExecutorAsync(CancellationToken ct) + => executors.GetExecutorAsync(cancellationToken: ct); - public void EvictDefault() => resolver.EvictRequestExecutor(); + public void EvictDefault() => executors.EvictExecutor(); - public IDisposable Subscribe(IObserver observer) - => resolver.Events.Subscribe(observer); + public IDisposable Subscribe(IObserver observer) + => events.Subscribe(observer); } ``` The `IServiceProvider.GetRequestExecutorAsync(...)` extension method has been kept as a convenience and now resolves `IRequestExecutorProvider` from the container internally, so call sites such as `services.GetRequestExecutorAsync()` continue to compile unchanged. The legacy `RequestExecutorEvicted` event (already marked obsolete in v15) has been removed; subscribe to `IRequestExecutorEvents` and filter on `RequestExecutorEventType.Evicted` instead. ### RequestExecutorProxy `RequestExecutorProxy` still exists and serves the same purpose, giving you a long-lived handle to the executor for a particular schema that is hot-swapped automatically when the schema is rebuilt. Two things have changed: **Constructor signature.** The proxy no longer takes an `IRequestExecutorResolver`; it now takes the new provider and events abstractions explicitly: Diff ``` -var proxy = new RequestExecutorProxy(resolver, "Schema"); +var proxy = new RequestExecutorProxy(executorProvider, executorEvents, "Schema"); ``` **The proxy now implements `IRequestExecutor`.** You can pass a `RequestExecutorProxy` directly anywhere an `IRequestExecutor` is expected, without first calling `GetRequestExecutorAsync()`. `ExecuteAsync` and `ExecuteBatchAsync` resolve the current executor internally. The CLR events `ExecutorUpdated` and `ExecutorEvicted` have been removed. If you need to react to swaps, derive from `RequestExecutorProxy` and override `OnConfigureRequestExecutor(newExecutor, oldExecutor)` (runs under the proxy's lock, before `CurrentExecutor` is replaced) or `OnAfterRequestExecutorSwapped(newExecutor, oldExecutor)` (runs after the swap, outside the lock). The separate `AutoUpdateRequestExecutorProxy` helper has been removed; the base `RequestExecutorProxy` now subscribes to executor events on construction and updates `CurrentExecutor` automatically. ### Schema.DefaultName moved to ISchemaDefinition.DefaultName The `Schema.DefaultName` constant is no longer available in v16\. Use `ISchemaDefinition.DefaultName` instead: Diff ``` -var schemaName = Schema.DefaultName; +var schemaName = ISchemaDefinition.DefaultName; ``` If you previously used a string literal for the default schema name, replace it with `ISchemaDefinition.DefaultName` (current value: `_Default`). ### Resolver Selection API changes In v16, `context.Selection` is a compiled execution selection. The old `context.Selection.SelectionSet` is no longer available. - `context.Selection.DeclaringSelectionSet` is the parent selection set (where the current field is declared), not the current field's child selection set. - `context.Selection.SyntaxNodes` now returns `FieldSelectionNode` wrappers. Use `.Node` to access the underlying `FieldNode`. - Because selections are merged during operation compilation, one execution selection can map to multiple syntax nodes. ### OperationResultBuilder is now internal If you've previously used the `OperationResultBuilder` to construct an `OperationResult`, switch to constructing it directly instead: C# ``` var errors = ImmutableList.Create([]); var extensions = ImmutableOrderedDictionary.Create([]); context.Result = new OperationResult(errors, extensions); ``` If you've used `OperationResultBuilder.FromResult()` to alter an existing `OperationResult`, switch to directly modifying the `OperationResult`: Diff ``` if (context.Result is OperationResult result) { - var resultBuilder = OperationResultBuilder.FromResult(result); - resultBuilder.SetExtension("foo", "bar"); - context.Result = resultBuilder.Build(); + result.Extensions = result.Extensions.SetItem("foo", "bar"); } ``` Most of the properties you'd want to modify are now immutable data structures that can be modified. `OperationResultBuilder.CreateError(error)` can be simply replaced with `new OperationResult([error])`. ### Page and cursor API changes #### Page is now abstract `Page` can no longer be instantiated directly. Use the static factory methods instead: - Use `Page.Empty` when you just need to return an empty page. - Use `Page.Create(...)` when you need to construct a page yourself. Diff ``` -return new Page( - items, - hasNextPage: hasNext, - hasPreviousPage: false, - createCursor: product => CreateCursor(product), - totalCount: totalCount); +return Page.Create( + items, + hasNextPage: hasNext, + hasPreviousPage: false, + createCursor: product => CreateCursor(product), + totalCount: totalCount); ``` #### CreateCursor now takes an index instead of an item `Page.CreateCursor` previously accepted a `T` item. It now accepts a zero-based `int` index into the page's `Items` array. This enables cursor generation from the underlying source element when a `valueSelector` projection is used. Diff ``` -string cursor = page.CreateCursor(page.First); +string cursor = page.CreateCursor(page.FirstIndex!.Value); ``` Use the new convenience extension methods `CreateStartCursor()` and `CreateEndCursor()` when you only need boundary cursors: Diff ``` -var startCursor = page.First is not null ? page.CreateCursor(page.First) : null; -var endCursor = page.Last is not null ? page.CreateCursor(page.Last) : null; +var startCursor = page.CreateStartCursor(); +var endCursor = page.CreateEndCursor(); ``` Two new properties, `FirstIndex` and `LastIndex`, return the zero-based indices of the first and last items (or `null` for an empty page). #### Edge constructor changes A new constructor overload accepts the item, its zero-based index, and a `Func` cursor resolver: Diff ``` -new Edge(item, cursor: page.CreateCursor) +new Edge(item, index, cursor: page.CreateCursor) ``` The existing `Edge(T node, Func resolveCursor)` constructor is still available for cases where the cursor is resolved from the item itself. #### ToConnectionAsync with custom edge factory The `ToConnectionAsync` overloads that accept a custom edge factory now pass the zero-based item index instead of the item's cursor: Diff ``` -.ToConnectionAsync((source, page) => - new MyEdge(source, edge => page.CreateCursor(edge.Node))); +.ToConnectionAsync((source, page, index) => + new MyEdge(source, page.CreateCursor(index))); ``` ### OperationResult changes We've removed the `IOperationResult` abstraction. If you've previously pattern-matched on this, you can simply replace it with `OperationResult`. To assert that an `IExecutionResult` is an `OperationResult` in tests, use `result.ExpectOperationResult();`. We've also switched the `OperationResult.Errors` and `OperationResult.Extensions` properties to always be initialized instead of being nullable. If you were previously asserting these properties as `null` in tests, switch to asserting them as empty instead. ### Skip/include disallowed on root subscription fields The `@skip` and `@include` directives are now disallowed on root subscription fields, as specified in the RFC: [Prevent @skip and @include on root subscription selection set](https://github.com/graphql/graphql-spec/pull/860). ### Deprecation of fields not deprecated in the interface Deprecating a field now requires the implemented field in the interface to also be deprecated, as specified in the [draft specification](https://spec.graphql.org/draft/#sec-Objects.Type-Validation). ### Global ID formatter conditionally added to filter fields Previously, the global ID input value formatter was added to ID filter fields regardless of whether or not Global Object Identification was enabled. This is now conditional. ### Filter operation limit Filtering now limits each filter argument to **64** operations by default. This protects the server from very large generated filters, including large `and` or `or` lists passed through variables. If a client sends more than 64 filter operations in a single filter argument, Hot Chocolate rejects the request before the filter expression is built and returns a GraphQL error with the code `HC0117`. If your application accepts larger filters, raise the limit on the filtering convention: C# ``` builder .AddGraphQL() .AddFiltering(x => x .AddDefaults() .MaxAllowedFilterOperations(256)); ``` To disable this limit, set `MaxAllowedFilterOperations` to `null`: C# ``` builder .AddGraphQL() .AddFiltering(x => x .AddDefaults() .MaxAllowedFilterOperations(null)); ``` ### fieldCoordinate renamed to coordinate in error extensions Some GraphQL validation errors included an extension named `fieldCoordinate` that provided a schema coordinate pointing to the field or argument that caused the error. Since schema coordinates can reference various schema elements (not just fields), we've renamed this extension to `coordinate` for clarity. Diff ``` { "errors": [ { "message": "Some error", "locations": [ { "line": 3, "column": 21 } ], "path": [ "field" ], "extensions": { "code": "HC0001", - "fieldCoordinate": "Query.field" + "coordinate": "Query.field" } } ], "data": { "field": null } } ``` ### FileValueNode renamed to UploadValueNode The upload literal node has been renamed from `FileValueNode` to `UploadValueNode`. If you are referencing this type directly in custom scalar logic or tests, update your code accordingly: Diff ``` -if (valueLiteral is FileValueNode fileValue) +if (valueLiteral is UploadValueNode uploadValue) { var file = uploadValue.File; var key = uploadValue.Key; } ``` If you are constructing upload value nodes manually, note that the constructor now also requires the multipart key: Diff ``` -var valueNode = new FileValueNode(file); +var valueNode = new UploadValueNode("0", file); ``` ### Errors from TypeConverters are now accessible in the ErrorFilter Previously, exceptions thrown by a `TypeConverter` were not forwarded to the `ErrorFilter`. Such exceptions are now properly propagated and can therefore be intercepted. In addition, the default output for such errors has been standardized: earlier, type conversion errors resulted in different responses depending on where in the document they occurred. Now, all exceptions thrown by type converters are reported in a unified format: JSON ``` { "errors": [ { "message": "The value provided for `[name of field or argument that caused the error]` is not in a valid format.", "locations": [ { "line": , "column": } ], "path": [ path to output field that caused the error], "extensions": { "code": "HC0001", "coordinate": "schema coordinate pointing to the field or argument that caused the error", "inputPath": [path to nested input field or argument (if any) that caused the error] "...": "other extensions" } } ], "data": { ... } } ``` ### Generic ID-attribute now infers the actual GraphQL type name Previously, `[ID]` used the CLR type name (`nameof(Type)`), even when a different GraphQL type name was configured via `[GraphQLName]` or `descriptor.Name()`. It now uses the actual GraphQL type name if one is defined, for example: C# ``` [GraphQLName("Book")] public sealed class BookDTO { [ID] public int Id { get; set; } public string Title { get; set; } } [ID] // uses "Book" now, not "BookDTO" anymore ``` Note that this change implies that all type parameters of the generic `ID`\-attribute must now be valid GraphQL types. If you need the old behavior, use can still use the non-generic `ID`\-attribute and set the type name explicitly: `[ID("BookDTO")]`. ### DescriptorAttribute attributeProvider is nullable Previously the `TryConfigure` or `OnConfigure` methods carried a non-nullable parameter of the member the descriptor attribute was annotated to. With the new source generator we moved away from pure reflection based APIs. This means that when you use the source generator ### HotChocolate.Fusion.SourceSchema The `HotChocolate.Fusion.SourceSchema` package has been removed and you can safely remove any references to it from your project. The `[Internal]`, `[Lookup]`, `[Is]`, and `[Require]` attributes have moved to the `HotChocolate.Types` package under the `HotChocolate.Types.Composite` namespace. You don't need to install `HotChocolate.Types` separately — it's already included in the `HotChocolate.AspNetCore` meta-package. ### Merged Assemblies HotChocolate.Types, HotChocolate.Execution, HotChocolate.Fetching With Hot Chocolate 16 we introduced a lot more abstractions, meaning we pulled out abstractions of the type system or the execution into separate libraries. But at the same time we simplified the implementation of the type system and the execution by moving the implementations of HotChocolate.Execution and HotChocolate.Fetching into HotChocolate.Types. This allowed us to simplify the implementation and make it more efficient. So, if you were referencing HotChocolate.Execution or HotChocolate.Fetching directly make sure to remove references to these libraries and replace them with HotChocolate.Types. ### Simpler Scalar Type In v16, creating custom scalar types is more straightforward. The `ScalarType` base class now uses a streamlined API. Instead of overriding both `Serialize`/`Deserialize` and `ParseLiteral`/`ParseValue`/`ParseResult`, you override a smaller set of methods: - `OnCoerceOutputValue(TRuntimeType runtimeValue, ResultElement resultValue)` \-- writes the serialized value directly to the result element - `OnValueToLiteral(TRuntimeType runtimeValue)` \-- converts a runtime value to an AST literal node - `OnLiteralToValue(IValueNode valueLiteral)` \-- converts an AST literal node to a runtime value The old `Serialize`, `Deserialize`, `ParseLiteral`, `ParseValue`, and `ParseResult` methods still exist on the base `ScalarType` class for backward compatibility, but the new methods on `ScalarType` are the recommended approach. Diff ``` -public class MyScalar : ScalarType +public class MyScalar : ScalarType { - public MyScalar() : base("MyScalar") { } - - public override Type RuntimeType => typeof(MyRuntimeType); - - public override bool IsInstanceOfType(IValueNode valueSyntax) => ...; - public override object? ParseLiteral(IValueNode valueSyntax) => ...; - public override IValueNode ParseValue(object? runtimeValue) => ...; - public override IValueNode ParseResult(object? resultValue) => ...; - public override bool TrySerialize(object? runtimeValue, out object? resultValue) => ...; - public override bool TryDeserialize(object? resultValue, out object? runtimeValue) => ...; + public MyScalar() : base("MyScalar") { } + + protected override MyRuntimeType OnLiteralToValue(IValueNode valueLiteral) => ...; + + protected override IValueNode OnValueToLiteral(MyRuntimeType runtimeValue) => ...; + + protected override void OnCoerceOutputValue( + MyRuntimeType runtimeValue, ResultElement resultValue) => ...; } ``` ### Removed Scalars The following scalar types have been removed in v16\. If your schema uses any of them, you need to either remove the usage or re-implement them as custom scalars. | Removed Scalar | Description | | ---------------- | ---------------------------------------------------- | | NegativeFloat | Represented a float value less than 0 | | NonNegativeFloat | Represented a float value greater than or equal to 0 | | NegativeInt | Represented an int value less than 0 | | NonPositiveInt | Represented an int value less than or equal to 0 | | NonEmptyString | Represented a non-empty string value | | NonNegativeInt | Represented an int value greater than or equal to 0 | If you need equivalent validation behavior, create a custom scalar that extends `ScalarType` and validates the value in `OnLiteralToValue` and `OnCoerceOutputValue`. ### OperationRequestBuilder The `OperationRequestBuilder` has been updated in v16\. The most notable changes: **`AddVariableValues` renamed to `SetVariableValues`** Diff ``` var request = OperationRequestBuilder.New() .SetDocument("{ hero { name } }") - .AddVariableValues(new Dictionary { ["id"] = 1 }) + .SetVariableValues(new Dictionary { ["id"] = 1 }) .Build(); ``` **Variable values are now JSON-based** `SetVariableValues` now accepts JSON strings, `JsonDocument`, `IEnumerable>`, or `IReadOnlyDictionary`. The preferred way to provide variables now is as a JSON string. C# ``` var request = OperationRequestBuilder.New() .SetDocument("query ($id: ID!) { node(id: $id) { id } }") .SetVariableValues("""{ "id": "42" }""") .Build(); ``` CLR objects passed via `SetVariableValues(Dictionary)` are now serialized to JSON internally. As a result, the JSON shape of a value must match what the target scalar expects. Some examples: - `DateTime` no longer fits a `Date` scalar, since its JSON form does not match the required `yyyy-MM-dd`. - Enums must be passed as their GraphQL name (`"VALUE"`) rather than the CLR member (`MyEnum.Value`). If you hit a mismatch, you have two options: 1. Provide variables as raw JSON through `SetVariableValues(string)`, bypassing CLR serialization entirely. 2. Register a custom `JsonConverter` for the affected type so the emitted JSON matches the scalar's expected format. If you need to pass an `Upload` scalar value, register the file on the builder via `AddFile` and reference it from your variables by the same key: C# ``` var file = new StreamFile("Foo.txt", () => new MemoryStream(/* your bytes */)); var request = OperationRequestBuilder.New() .SetDocument("mutation ($file: Upload!) { upload(file: $file) }") .SetVariableValues("""{ "file": "yourKey" }""") .AddFile("yourKey", file) .Build(); ``` `yourKey` is just a marker you choose to correlate the variable value with the file. Call `AddFile` multiple times to register additional files on the same request. **Global state methods** The context data methods have been renamed: Diff ``` -builder.AddProperty("key", value); +builder.SetGlobalState("key", value); ``` Additional methods include `AddGlobalState`, `TryAddGlobalState`, and `RemoveGlobalState`. **`From` factory method** Use `OperationRequestBuilder.From(request)` to create a builder pre-populated from an existing request, instead of manually copying properties. **Features collection** The builder now exposes a `Features` property of type `IFeatureCollection` for attaching extensibility features. ### Snapshot matching on IExecutionResult The internal layout of `IExecutionResult` implementations has changed and is no longer compatible with general-purpose object serializers used by snapshot libraries like `Snapshooter` or `Verify`. Snapshotting the result instance directly will either fail or produce unstable output. Serialize the result to JSON first and snapshot that instead: Diff ``` var result = await executor.ExecuteAsync("{ example }"); - result.MatchSnapshot(); + result.ToJson().MatchSnapshot(); ``` If you're using **CookieCrumble**, you don't need to convert manually: it has native snapshot support for `IExecutionResult` and serializes it correctly out of the box. ### AllowNonPersistedOperation moved The `AllowNonPersistedOperation` extension method has moved from `OperationRequestBuilderExtensions` (in `HotChocolate.Abstractions`) to `PersistedOperationRequestOverridesExtensions` (in `HotChocolate.Execution.Abstractions`). The namespace (`HotChocolate.Execution`) and the method signature are unchanged, so normal call sites continue to compile: C# ``` builder.AllowNonPersistedOperation(); ``` If you called the method through its declaring type, update the reference: Diff ``` -OperationRequestBuilderExtensions.AllowNonPersistedOperation(builder); +PersistedOperationRequestOverridesExtensions.AllowNonPersistedOperation(builder); ``` The method now writes a `PersistedOperationRequestOverrides` feature on the request instead of setting the `HotChocolate.Execution.NonPersistedOperationAllowed` global state entry. The `OnlyAllowPersistedDocuments` middleware only reads the feature in v16\. If you previously bypassed the extension method and set the flag through global state, switch to writing the feature: Diff ``` -builder.SetGlobalState("HotChocolate.Execution.NonPersistedOperationAllowed", true); +builder.Features.Set(new PersistedOperationRequestOverrides(AllowNonPersistedOperation: true)); ``` ### Any and Json scalars merged The `Json` scalar has been removed and its functionality merged into the `Any` scalar. The `Any` scalar now uses `System.Text.Json.JsonElement` as its .NET runtime type, which was previously the runtime type of the `Json` scalar. **`JsonElement` is now inferred as `Any` instead of `Json`.** If you used `[GraphQLType]` annotations or explicit `JsonType` bindings, replace them with `AnyType`: C# ``` // before [GraphQLType] public JsonElement GetData() => ...; // after [GraphQLType] public JsonElement GetData() => ...; ``` #### Returning dictionaries or arbitrary .NET types If you previously returned `Dictionary` or other .NET types from a field typed as `Json` or `Any`, you now need to register the JSON type converter explicitly. Without it, the type system has no way to convert arbitrary .NET types to `JsonElement`: C# ``` builder .AddGraphQL() .AddJsonTypeConverter(); ``` For custom reference types that need specific serialization, register a dedicated converter instead: C# ``` builder .AddGraphQL() .AddTypeConverter( value => JsonSerializer.SerializeToElement(value.Id)); ``` #### Any input fields now deserialize complex types as JsonElement Previously, complex input values for `Any`\-typed input variables were deserialized as `IDictionary`. They are now deserialized as `JsonElement`, aligning input behavior with arbitrary output types. C# ``` public string Foo([GraphQLType]object? input) => input?.GetType().Name; ``` GraphQL ``` query { foo(input: { key: "value" }) # Now returns: "JsonElement" # Previously (v15): "Dictionary`2" } ``` ### Byte and SignedByte types renamed - The GraphQL type `Byte` has been renamed to `UnsignedByte` (CLR type: `byte`). - The GraphQL type `SignedByte` has been renamed to `Byte` (CLR type: `sbyte`). This is to align the GraphQL type names with the core types (`Int`, etc.), which are signed. ### Byte arrays now mapped to Base64String C# byte arrays (`byte[]`) are now mapped to the GraphQL `Base64String` type by default, as the `ByteArray` type has been deprecated. ### Uri now mapped to URI scalar instead of URL The CLR type `Uri` is now mapped to a new `URI` scalar, instead of the `URL` scalar. - The `URI` scalar should be used for absolute or relative URIs. - The `URL` scalar should be used for absolute URIs/URLs only. For backwards compatibility, you can set `allowRelativeUris` to `true`: C# ``` AddGraphQL().AddType(new UrlType(allowRelativeUris: true)) ``` Note that this option is likely to be removed in a later release, so it's recommended that you switch types as soon as possible. ### DateTime scalar serialization The `DateTime` scalar now serializes with up to 7 fractional seconds (`FFFFFFF`) as opposed to exactly 3 (`fff`). Trailing zeros are stripped, and the fractional component is omitted entirely when zero, so `2023-12-24T15:30:00.5000000Z` is now emitted as `2023-12-24T15:30:00.5Z` and `2023-12-24T15:30:00.0000000Z` is emitted as `2023-12-24T15:30:00Z`. If you need fractional seconds to always be present in the output (for example, to preserve a fixed-width format your clients depend on), set `AlwaysOutputFractionalSeconds = true` on `DateTimeOptions`. You can also tune the precision via `OutputPrecision`. To restore the exact v15 behavior of always emitting three fractional digits, combine both: C# ``` builder.AddGraphQL() .AddType(new DateTimeType(new DateTimeOptions { OutputPrecision = 3, AlwaysOutputFractionalSeconds = true })); ``` The same options apply to the `LocalDateTime` and `LocalTime` scalars (and to their counterparts in `HotChocolate.Types.NodaTime`, which expose a matching `DateTimeOptions` struct). ### IHasRuntimeType is now IRuntimeTypeProvider In an effort to standardize our abstractions, we've renamed `IHasRuntimeType` to `IRuntimeTypeProvider`. ### GUIDs converted to strings using the "D" format The conversion from GUID to string in the default type converter has been updated to format with hyphens (format "D") instead of without (format "N"), to follow the documented behavior. ### EnableOneOf option removed The `EnableOneOf` option has been removed, as the `@oneOf` directive is now built in. ### GraphQLToolOptions replaced by NitroAppOptions The `GraphQLToolOptions` class has been removed. Nitro configuration is now done directly through `NitroAppOptions` from the `ChilliCream.Nitro.App` namespace. The `GraphQLServerOptions.Tool` property is now of type `NitroAppOptions` instead of `GraphQLToolOptions`. #### WithOptions now uses a delegate pattern Per-endpoint `WithOptions` overrides now use a delegate pattern instead of object initializers: Diff ``` endpoints.MapGraphQL() - .WithOptions(o => o.Tool.Enable = false); + .WithOptions(o => o.Tool.Enable = false); // No change for GraphQLServerOptions — already used delegates endpoints.MapNitroApp() - .WithOptions(new GraphQLToolOptions { Enable = false }); + .WithOptions(o => o.Enable = false); ``` #### GraphQLToolServeMode replaced by ServeMode Replace `GraphQLToolServeMode` with `ServeMode` from `ChilliCream.Nitro.App`: Diff ``` -using HotChocolate.AspNetCore; +using ChilliCream.Nitro.App; -GraphQLToolServeMode.Embedded → ServeMode.Embedded -GraphQLToolServeMode.Latest → ServeMode.Latest -GraphQLToolServeMode.Insider → ServeMode.Insider -GraphQLToolServeMode.Version(v) → ServeMode.Version(v) ``` #### DefaultHttpMethod replaced by UseGet The `DefaultHttpMethod` enum has been removed. Use the `UseGet` boolean property on `NitroAppOptions` instead: Diff ``` -o.HttpMethod = DefaultHttpMethod.Get; +o.UseGet = true; ``` ### Nitro integration The Nitro NuGet packages have been restructured in v16\. Versions now align with the rest of the platform, so you are migrating from `1.x` to `16.x`. For the full migration guide covering all package renames, API changes, and complete before/after examples, see [Migrating Nitro from 1 to 16](https://chillicream.com/docs/nitro/migration/migrate-from-1-to-16). The key changes for HotChocolate projects: - **Package rename:** The old `ChilliCream.Nitro` package is now `ChilliCream.Nitro.HotChocolate`. A new meta-package takes the old `ChilliCream.Nitro` ID. - **Connection settings** are configured once on the service collection via `AddNitro()`. - **Per-schema feature options** (persisted operations, metrics, operation reporting) are configured via `ModifyNitroOptions()` on the GraphQL builder. - **`AddNitroExporter()`** is replaced by `AddOpenTelemetry()` on the `INitroBuilder`. - **Asset cache** is now configured globally on `INitroBuilder` instead of per-schema. - **`AddDefaults()`** is a source-generated method that wires up the default integration when the correct packages are referenced. Note If you are self-hosting the Nitro backend, make sure to update it to the latest version as well. `10.1.0` is the minimum version required to work with the `ChilliCream.Nitro.*` packages. **Before** C# ``` builder.Services .AddGraphQLServer() .AddNitro(o => { o.ApiId = "..."; o.ApiKey = "..."; o.Stage = "..."; o.EnablePersistedQueries = true; o.Metrics.Enabled = true; }); ``` **After** C# ``` builder.Services .AddNitro(o => { o.ApiId = "..."; o.ApiKey = "..."; o.Stage = "..."; }) .AddDefaults(); builder.Services .AddGraphQLServer() .ModifyNitroOptions(o => { o.PersistedOperations.Enabled = true; o.Metrics.Enabled = true; }); ``` ### Server options now configured via ModifyServerOptions `GraphQLServerOptions` (GET requests, multipart, batching, schema requests, etc.) are now configured at the schema level using `ModifyServerOptions` instead of per-endpoint: Diff ``` builder.AddGraphQL() + .ModifyServerOptions(o => + { + o.EnableGetRequests = false; + o.Batching = AllowedBatching.All; + }); ``` Per-endpoint overrides are still supported via `WithOptions` on the endpoint builder: C# ``` endpoints.MapGraphQL().WithOptions(o => o.EnableGetRequests = false); ``` ### Batching is now disabled by default In v15, request batching was enabled by default (`EnableBatching = true`). In v16, batching is **disabled by default** as a security measure. The `EnableBatching` property has been replaced by `Batching`, which uses the `AllowedBatching` flags enum for fine-grained control: Diff ``` -o.EnableBatching = true; +o.Batching = AllowedBatching.All; ``` If you were relying on the previous default, you need to explicitly enable batching: C# ``` builder.AddGraphQL() .ModifyServerOptions(o => o.Batching = AllowedBatching.All); ``` Additionally, a new `MaxBatchSize` property limits the number of operations in a single batch. The default is **1024**. Set it to `0` for unlimited. Note Fusion subgraphs automatically enable batching via `AddSourceSchemaDefaults()`. No action is needed for subgraphs. For more details, see [Batching](https://chillicream.com/docs/hotchocolate/server/batching). ### New default incremental delivery format for @defer and @stream Hot Chocolate v16 changes the default wire format for incremental delivery (`@defer` / `@stream`) from the legacy path-based format (v0.1) to the newer id-based format (v0.2). This affects all streaming transports: multipart, SSE, and JSON Lines. **v0.1 (legacy)** used `path` and `label` to identify deferred fragments: JSON ``` {"data":{"product":{"name":"Abc"}},"hasNext":true} {"incremental":[{"data":{"description":"Abc desc"},"path":["product"]}],"hasNext":false} ``` **v0.2 (new default)** uses `pending`, `incremental` with `id`, and `completed`: JSON ``` {"data":{"product":{"name":"Abc"}},"pending":[{"id":"2","path":["product"]}],"hasNext":true} {"incremental":[{"id":"2","data":{"description":"Abc desc"}}],"completed":[{"id":"2"}],"hasNext":false} ``` If your clients depend on the legacy format, you have two options: **Option 1: Client sends `incrementalSpec=v0.1` in the `Accept` header** Clients can opt into the legacy format per-request by adding the `incrementalSpec` parameter to the `Accept` header: ``` Accept: multipart/mixed; incrementalSpec=v0.1 Accept: text/event-stream; incrementalSpec=v0.1 Accept: application/jsonl; incrementalSpec=v0.1 ``` **Option 2: Change the server default** To restore v0.1 as the server-wide default (used when the client doesn't specify `incrementalSpec`): C# ``` builder .AddGraphQL() .AddHttpResponseFormatter( incrementalDeliveryFormat: IncrementalDeliveryFormat.Version_0_1); ``` Or with the options overload: C# ``` builder .AddGraphQL() .AddHttpResponseFormatter( new HttpResponseFormatterOptions { /* ... */ }, incrementalDeliveryFormat: IncrementalDeliveryFormat.Version_0_1); ``` ### TimeSpan scalar renamed to Duration The `TimeSpan` scalar has been renamed to `Duration` to better reflect the underlying specification (ISO 8601), and move away from .NET-oriented naming. For backwards compatibility, you can rename the type as follows: C# ``` builder .AddGraphQL() .AddType(new DurationType("TimeSpan")); ``` ### NodaTime scalars now implement the GraphQL scalar specifications The `HotChocolate.Types.NodaTime` package was rewritten in v16 to align its scalar behavior with the specifications published on [scalars.graphql.org](https://scalars.graphql.org/). This is a breaking change if you relied on the old NodaTime scalar set or on the looser parsing behavior of the previous implementation. #### Only five NodaTime scalars remain built in The package now only ships these spec-based scalar implementations: - `DateTimeType` - `DurationType` - `LocalDateType` - `LocalDateTimeType` - `LocalTimeType` These scalars expose `@specifiedBy` URLs and follow the corresponding scalar specifications for parsing and serialization. #### Legacy NodaTime scalars were removed The following scalar types are no longer included in `HotChocolate.Types.NodaTime`: - `DateTimeZoneType` - `InstantType` - `IsoDayOfWeekType` - `OffsetDateType` - `OffsetTimeType` - `OffsetType` - `PeriodType` - `ZonedDateTimeType` If your schema used any of these scalars in v15, your project will no longer compile after upgrading until you remove them or provide your own replacement implementations. If you still need one of the removed scalars, add it back manually in your application as a custom scalar. #### Use AddNodaTime() to register the new scalars v16 adds a dedicated `AddNodaTime()` extension method that registers all five built-in NodaTime scalars and the related CLR bindings and converters: Diff ``` builder .AddGraphQL() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType(); + .AddNodaTime(); ``` `AddNodaTime()` also configures these runtime type mappings: - `DateTimeOffset` to `DateTimeType` - `DateTime` to `LocalDateTimeType` - `DateOnly` to `LocalDateType` - `TimeOnly` to `LocalTimeType` If you prefer, you can still register the remaining scalar types individually instead of using `AddNodaTime()`. ### AddInstrumentation #### InstrumentationOptions changes - `RenameRootActivity` was removed. See [Recreating RenameRootActivity](#recreating-renamerootactivity) to reproduce the previous behavior in user code. - `RequestDetails.Operation` was renamed to `RequestDetails.OperationName`. - `RequestDetails.Query` was renamed to `RequestDetails.Document`. #### Recreating `RenameRootActivity` The root span is usually owned by the transport instrumentation (for example ASP.NET Core), not Hot Chocolate. Recreate the old behavior with a diagnostic event listener that publishes the operation name, then apply it from the transport instrumentation in `EnrichWithHttpResponse` (not `EnrichWithHttpRequest`, since the operation is only known after execution): C# ``` using System.Diagnostics; using HotChocolate.Execution; using HotChocolate.Execution.Instrumentation; public sealed class RenameRootActivityListener : ExecutionDiagnosticEventListener { public override IDisposable ExecuteRequest(RequestContext context) => new Scope(context); private sealed class Scope(RequestContext context) : IDisposable { public void Dispose() { if (Activity.Current is not { } activity || !context.TryGetOperation(out var operation) || string.IsNullOrEmpty(operation.Name)) { return; } var name = $"{operation.Kind.ToString().ToLowerInvariant()} {operation.Name}"; var root = activity; while (root.Parent is { } parent) { root = parent; } root.SetCustomProperty("graphqlDisplayName", name); } } } ``` C# ``` builder.Services .AddGraphQLServer() .AddInstrumentation() .AddDiagnosticEventListener(); builder.Services .AddOpenTelemetry() .WithTracing(tracing => tracing .AddAspNetCoreInstrumentation(o => o.EnrichWithHttpResponse = (activity, _) => { if (activity.GetCustomProperty("graphqlDisplayName") is string name) { activity.DisplayName = name; } }) .AddHotChocolateInstrumentation()); ``` ### OpenTelemetry span and status changes The OpenTelemetry spans and attributes emitted by `AddInstrumentation()` have been updated to align with the [proposed OpenTelemetry semantic conventions for GraphQL](https://github.com/graphql/otel-wg/blob/main/spec). If you have dashboards or alerts that filter on the old attribute names or values, update them accordingly. Besides changes to the attributes, the most notable change is that the name of the root GraphQL span has been changed to just include the operation type (`query`, `mutation` or `subscription`), and no longer the operation name, to keep the cardinality low. The operation name can still be retrieved from the `graphql.operation.name` span attribute. #### Removed attributes | Attribute | | --------------------------- | | graphql.operation.id | | graphql.selection.type | | graphql.selection.hierarchy | #### Renamed attributes | Old Attribute | New Attribute | | ------------------------------------- | ------------------------------------ | | graphql.operation.kind | graphql.operation.type | | graphql.selection.field.declaringType | graphql.selection.field.parent\_type | | graphql.dataLoader.keys.count | graphql.dataloader.batch.size | | graphql.dataLoader.keys | graphql.dataloader.batch.keys | | graphql.fusion.node.schema | graphql.source.name | | graphql.fusion.node.type | graphql.operation.step.kind | | graphql.error.location.line/column | graphql.error.locations | #### Changed attribute values | Attribute | Old Value | New Value | | ---------------------- | ------------------------------- | --------------------------------------------------- | | graphql.operation.type | Query / Mutation / Subscription | query / mutation / subscription | | graphql.http.kind | operation-batch | operation\_batch | | graphql.document.hash | | : , e.g. md5: | | graphql.document.id | \- | Value is only set if document is a trusted document | #### Custom enricher changes If you've implemented a custom `ActivityEnricher`, you no longer need to pass the `ObjectPool` down to the base class: Diff ``` public class CustomActivityEnricher( - ObjectPool stringBuilderPool, InstrumentationOptions options -) : ActivityEnricher(stringBuilderPool, options); +) : ActivityEnricher(options); ``` There have also been some changes to the methods you can override in your enricher: | v15 | v16 | | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | EnrichParserErrors(HttpContext, IError, Activity) | Replaced by EnrichParserErrors(HttpContext, IReadOnlyList, Activity). | | EnrichRequestError(RequestContext, Activity, Exception) | Replaced by EnrichRequestError(RequestContext, Exception, Activity). | | EnrichRequestError(RequestContext, Activity, IError) | Replaced by EnrichRequestError(RequestContext, IError, Activity). | | EnrichValidationError(RequestContext, Activity, IError) | Replaced by EnrichValidationErrors(RequestContext, IReadOnlyList, Activity). | | EnrichAnalyzeOperationComplexity(RequestContext, Activity) | Replaced by EnrichAnalyzeOperationCost(RequestContext, Activity). | | EnrichDataLoaderBatch(IDataLoader, IReadOnlyList, Activity) | Replaced by EnrichExecuteBatch(IDataLoader, IReadOnlyList, Activity). | | EnrichResolverError(RequestContext, IError, Activity) | Removed. Use EnrichRequestError(...) for request-level errors and EnrichResolverError(IMiddlewareContext, IError, Activity) for field resolver errors. | | EnrichRequestVariables(...) | Removed. | | EnrichBatchVariables(...) | Removed. | | EnrichRequestExtensions(...) | Removed. | | EnrichBatchExtensions(...) | Removed. | | CreateOperationDisplayName(...) | Removed. | | CreateRootActivityName(...) | Removed. | | EnrichError(...) | Removed. | Note Overriding enricher methods without calling `base` no longer prevents the standard span attributes from being emitted. The semantic-convention attributes are now applied by the instrumentation itself, and custom enrichers are only intended for adding extra information. ### Diagnostic Listeners We removed the following methods from the `IExecutionDiagnosticEventListener` since they no longer apply: - `ExecuteStream` - `ExecuteDeferredTask` - `DispatchBatch` - `SubscriptionTransportError` - `SubscriptionEventResult` Some other methods also had a change in their signature - simply override them again to fix any compilation issues. ### IOperationMessagePayload exposes raw JSON The `IOperationMessagePayload` interface, used by `ISocketSessionInterceptor` hooks (`OnConnectAsync`, `OnPingAsync`, `OnPongAsync`), no longer exposes the `As()` deserialization helper. It now provides direct access to the raw `JsonElement?` through a `Payload` property: Diff ``` -public interface IOperationMessagePayload -{ - T? As() where T : class; -} +public interface IOperationMessagePayload +{ + JsonElement? Payload { get; } +} ``` If you were calling `.As()` to deserialize the payload, switch to `Payload?.Deserialize()`: Diff ``` public override ValueTask OnConnectAsync( ISocketSession session, IOperationMessagePayload connectionInitMessage, CancellationToken cancellationToken = default) { - var payload = connectionInitMessage.As(); + var payload = connectionInitMessage.Payload?.Deserialize(); // ... } ``` ### Experimental @semanticNonNull support removed Hot Chocolate v15 included experimental support for the `@semanticNonNull` directive, which let you mark fields as semantically non-null while still returning `null` (rather than propagating to the parent) when a resolver errored. We've removed this feature in v16 in favor of the [onError proposal](https://github.com/graphql/graphql-spec/pull/1163). If you previously opted in to this feature, remove the option: Diff ``` builder.AddGraphQL() .ModifyOptions(o => { - o.EnableSemanticNonNull = true; }); ``` If you still need to keep the behavior of not propagating nulls for errors on non-null fields, set the `DefaultErrorHandlingMode` to `ErrorHandlingMode.Null`: C# ``` builder .AddGraphQL() .ModifyRequestOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Null); ``` #### Clients that still need a schema with @semanticNonNull annotations If you have a client that still relies on the schema being annotated with `@semanticNonNull`, you have a few options to obtain such a schema. **Schema snapshot tests** If you're producing a schema string for snapshot tests like this: C# ``` ISchemaDefinition schema = await new ServiceCollection() .AddGraphQL() // ... .BuildSchemaAsync(); string schemaStr = schema.ToString(); // assert schemaStr ... ``` Switch to `SchemaFormatter` with `RewriteToSemanticNonNull` enabled: C# ``` string schemaStr = SchemaFormatter.FormatAsString( schema, new SchemaFormatterOptions { RewriteToSemanticNonNull = true }); ``` **Downloading the schema from the server** If you're using `MapGraphQLSchema()` to expose the schema at `/graphql/schema`, you can additionally call `MapGraphQLSemanticNonNullSchema()` to expose a variant annotated with `@semanticNonNull` at `/graphql/semantic-non-null-schema.graphql`: C# ``` app.MapGraphQLSchema(); app.MapGraphQLSemanticNonNullSchema(); ``` **Exporting the schema via the CLI** If you're using the schema export command, add the `--semantic-non-null` flag to emit the schema with `@semanticNonNull` annotations: Bash ``` dotnet run -- schema export --output schema.graphql --semantic-non-null ``` ### Parameterless handler registration on filter, sort, and projection providers removed The parameterless activator overloads have been removed from the filtering, sorting, and projection provider descriptors. Custom handlers must now be registered either by passing an instance or by passing a factory that receives a provider context. The context exposes `InputParser`, `InputFormatter`, `SchemaServices`, `TypeConverter`, etc. Affected APIs: - `IFilterProviderDescriptor.AddFieldHandler()` \-> `AddFieldHandler(Func)` - `ISortProviderDescriptor.AddFieldHandler()` \-> `AddFieldHandler(Func)` - `ISortProviderDescriptor.AddOperationHandler()` \-> `AddOperationHandler(Func)` - `IProjectionProviderDescriptor.RegisterFieldHandler()` \-> `RegisterFieldHandler(Func)` - `IProjectionProviderDescriptor.RegisterFieldInterceptor()` \-> `RegisterFieldInterceptor(Func)` - `IProjectionProviderDescriptor.RegisterOptimizer()` \-> `RegisterOptimizer(Func)` **Before** C# ``` public class CustomFilteringConvention : FilterConvention { protected override void Configure(IFilterConventionDescriptor descriptor) { descriptor.AddDefaults(); descriptor.Provider( new QueryableFilterProvider( x => x .AddFieldHandler() .AddDefaultFieldHandlers())); } } ``` **After** C# ``` public class CustomFilteringConvention : FilterConvention { protected override void Configure(IFilterConventionDescriptor descriptor) { descriptor.AddDefaults(); descriptor.Provider( new QueryableFilterProvider( x => x .AddFieldHandler(ctx => new QueryableStringInvariantEqualsHandler(ctx.InputParser)) .AddDefaultFieldHandlers())); } } ``` The `CanHandle` signature also changed on the filtering, sorting, and projection handler interfaces. If you have overridden it, re-override against the new signature. ### Extension type resolvers are no longer projected by default A field whose resolver is defined on a different type than the entity being projected, most commonly a resolver class annotated with `[ExtendObjectType]`, is no longer added to the queryable projection by default. In v15, the projection included the backing member of such a field even though a custom resolver produced the value, and opting out required an explicit `[IsProjected(false)]`. v16 treats a resolver defined on a separate type as a genuine custom resolver: its backing member is left out of the projection unless you opt in. Fields whose resolver member is declared on the entity's runtime type, on an interface the entity implements, or on a base type it extends are unaffected and continue to be projected. The following resolver reads a member of its parent through `[ExtendObjectType]`: C# ``` [ExtendObjectType(typeof(Author))] public sealed class AuthorExtensions { public string DisplayName([Parent] Author author) => author.Name; } ``` In v15, `Author.Name` was projected, so `displayName` returned the author's name. In v16, `Name` is no longer projected, so the resolver receives an `Author` whose `Name` is unset (the CLR default, `null` for a string), and `displayName` no longer returns the real name. To keep the backing member in the projection, annotate the resolver with `[BindMember]` and name the member it reads: Diff ``` [ExtendObjectType(typeof(Author))] public sealed class AuthorExtensions { + [BindMember(nameof(Author.Name))] public string DisplayName([Parent] Author author) => author.Name; } ``` `[IsProjected(true)]` re-enables projection as well. If the resolver does not read a member of the entity, because it calls a service, resolves a DataLoader, or returns a computed value, no member needs to be projected and no change is required. ### Transaction scope handlers removed `AddTransactionScopeHandler` and `AddDefaultTransactionScopeHandler` have been removed. The `ITransactionScopeHandler` abstraction wrapped an entire mutation operation in a single transaction and rolled back all root field results when any root field errored. This violates the GraphQL specification, which defines mutation root fields as independent: each field's result must be observable regardless of whether subsequent fields succeed or fail. If you relied on transaction handlers to keep multiple mutation fields consistent, model that coupling explicitly in the schema. Replace fine-grained root fields with a single coarse-grained mutation that accepts a composite input and performs the work as one unit: GraphQL ``` mutation { # Before: separate fields, transactionality was implicit and spec-violating addProducts(productIds: [...]) { ... } removeProducts(productIds: [...]) { ... } # After: one mutation that answers the client use case updateCart(input: { productsToAdd: [...], productsToRemove: [...] }) { ... } } ``` The transaction boundary now lives inside the resolver for the coarse-grained mutation, where you control it directly with your data access layer. ### Marten nullable boolean `neq` filter now includes null rows `HotChocolate.Data.Marten` has been updated to Marten 8.37.0 (from 8.0.0) to address the critical advisory [GHSA-vmw2-qwm8-x84c](https://github.com/advisories/GHSA-vmw2-qwm8-x84c). This change lands in **16.0.10**. Filtering a nullable `bool` property with `neq` now also returns rows where the value is `null`. Marten 8.10.1 changed the SQL it emits for `!=` predicates on nullable boolean JSON properties ([JasperFx/marten#3953](https://github.com/JasperFx/marten/issues/3953)): the `WHERE` clause now includes an `IS NULL OR ...` branch. Other nullable property types (numeric, string, enum) are unaffected. For a query against a nullable `Bar` column: GraphQL ``` { root(where: { bar: { neq: true } }) { bar } } ``` - Previously: only rows where `Bar = false` were returned. - Now: rows where `Bar = false` and rows where `Bar IS NULL` are returned. ### Default values are validated against their type Argument and input field default values are now validated for compatibility with their type when the schema is built. Previously an incompatible default (for example an argument typed `Int` with a default of `"abc"`, or an enum default naming a value the enum does not define) built successfully and failed only when the default was actually used, or produced silently wrong behavior. Such schemas now fail at build with a schema error. This applies to defaults on object and interface field arguments, input object fields, and directive definition arguments, at any nesting depth (inside lists and input objects). If your schema fails to build after upgrading, correct the default value to a literal compatible with its type. `[ID]`\-typed defaults given as strings remain valid; only genuinely incompatible literals are rejected. ### ProjectionFeature removed The public `HotChocolate.Data.Projections.ProjectionFeature` record has been removed. The state it carried is now tracked through internal field flags. This change lands in **16.6.2**. Use the existing `IsProjected()` descriptor extension or the `[IsProjected]` attribute to configure projection behavior; reading the feature from a field's `Features` collection is no longer possible. ## Deprecations Things that will continue to function this release, but we encourage you to move away from. ### ByteArray The GraphQL `ByteArray` type has been deprecated. Use the `Base64String` type instead. ## Noteworthy changes ### Validation walker is now operation-scoped for fragment visits by default The base `DocumentValidatorVisitor` no longer re-walks a fragment definition on every sibling spread within an operation. Each fragment is now visited at most once per operation. Cycle detection continues to work via `context.Path.Contains(fragment)` in `FragmentVisitor`. User-visible effect: some queries that previously failed validation with false-positive errors now validate cleanly. For example, a `@defer` directive with a label inside a fragment spread twice was reported as a duplicate label collision against itself; that case (and similar over-counted errors for argument names, variable usage, input fields, and fragment spread possibility) now behaves correctly. Queries that should fail still fail, with no duplicates per spread. If you wrote a custom `DocumentValidatorVisitor` that called `context.Fragments.Leave(...)`, you have two options: 1. **Match the new default (operation-scoped):** remove the `Leave` call. Each fragment is walked at most once per operation; sibling spreads short-circuit. 2. **Opt back into per-spread re-walks:** keep the `Leave` call. This is what `CostAnalyzer` does, because per-spread re-walks are required to correctly accumulate cost across reused fragments. Diff ``` if (context.Fragments.TryEnter(node, out var fragment)) { var result = Visit(fragment, node, context); - context.Fragments.Leave(fragment); // remove for operation-scoped (recommended for validation rules) // keep the Leave(...) call if your rule needs per-spread re-walks (e.g. cost analysis) if (result.IsBreak()) { return Break; } } ``` ### RunWithGraphQLCommandsAsync returns exit code `RunWithGraphQLCommandsAsync` and `RunWithGraphQLCommands` now return exit codes (`Task` and `int` respectively, instead of `Task` and `void`). We recommend updating your `Program.cs` to return this exit code. This ensures that command failures signal an error to shell scripts, CI/CD pipelines, and other tools: Diff ``` var app = builder.Build(); - await app.RunWithGraphQLCommandsAsync(args); + return await app.RunWithGraphQLCommandsAsync(args); ``` ### Parser recursion depth limit The parser now enforces a maximum recursion depth of **200** by default. Deeply nested selection sets, list values, object values, or type references that exceed this depth are rejected with a `SyntaxException` instead of causing a stack overflow. If your queries legitimately exceed this depth, increase the limit: C# ``` builder .AddGraphQL() .ModifyParserOptions(o => { o.MaxAllowedRecursionDepth = 500; }); ``` ### Parser directive limit The parser now limits the number of directives per location (field, operation, fragment definition) to **4** by default. Documents with more directives on a single location are rejected at parse time. If you use more than 4 directives per location, increase the limit: C# ``` builder .AddGraphQL() .ModifyParserOptions(o => { o.MaxAllowedDirectives = 8; }); ``` ### Fragment visit budget Validation now caps the total number of fragment visits per operation at **1,000** by default. Each time a fragment spread is entered during validation counts as one visit. Queries with deeply nested or heavily reused fragment spreads that exceed this budget will have remaining fragments skipped during validation. If you have complex queries with many fragment spreads, increase the limit: C# ``` builder .AddGraphQL() .ModifyValidationOptions(o => { o.MaxAllowedFragmentVisits = 5_000; }); ``` ### Field merge comparison budget The overlapping-fields-can-be-merged validation rule now caps comparison work at **100,000** by default. Queries that exceed this budget are rejected. If you have very complex queries that trigger this limit, increase it: C# ``` builder .AddGraphQL() .SetMaxAllowedFieldMergeComparisons(200_000); ``` ### Concurrent execution gate Hot Chocolate v16 introduces a concurrency gate that limits how many GraphQL operations execute at the same time. The gate sits in the request pipeline just before operation execution and applies uniformly to queries, mutations, subscription handshakes, and each subscription event. Configure the limit through `ModifyServerOptions`: C# ``` builder .AddGraphQL() .ModifyServerOptions(o => o.MaxConcurrentExecutions = 128); ``` The default is **64**. Operations that arrive while the gate is full queue up and run as slots free. Set the limit to `null` to disable the gate entirely. Every execution is bounded by the `ExecutionTimeout` option (default 30 seconds). This applies uniformly to queries, mutations, subscription handshakes, and each subscription event. The budget covers both the time an execution spends waiting for a concurrency slot and the time it spends running. When the budget is exceeded, the execution is cancelled and the caller receives a clean timeout error. `ExecutionTimeout` is the single setting that controls cancellation for every execution. Subscriptions participate in the limit like any other operation. The initial subscribe consumes a slot while the subscribe resolver runs, and each emitted event consumes a slot while its result is being produced. Idle subscriptions (waiting on the next event) cost nothing. The slot is released between events. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/migrating/migrate-from-15-to-16.md) Maintained by ChilliCream. Last updated on **August 28, 2026** by **Glen** --- # Hot Chocolate 16.7 Migration Guide > Migration guide for Hot Chocolate v16.6 to v16.7: replace raw condition masks with ConditionFlags and configure wide operation limits. Canonical source: https://chillicream.com/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7 Update every `HotChocolate.*` package in the application to version 16.7 before applying these changes. ## Deprecations ### Raw condition masks replaced by ConditionFlags Hot Chocolate can now compile and execute operations with more than 64 distinct `@skip`/`@include` conditions or `@defer` conditions. `MaxAllowedIncludeConditions` limits the combined `@skip` and `@include` conditions, and `MaxAllowedDeferConditions` limits the `@defer` conditions. Both limits default to **1,024**. An operation that exceeds either limit produces a GraphQL request error during operation compilation. Configure the limits through `RequestExecutorOptions`: C# ``` builder .AddGraphQL() .ModifyRequestOptions(options => { options.MaxAllowedIncludeConditions = 2_048; options.MaxAllowedDeferConditions = 2_048; }); ``` `ConditionFlags` contains the first 64 evaluated conditions and any remaining conditions. Replace `IResolverContext.IncludeFlags` and the raw `IsIncluded` overload with their `ConditionFlags` equivalents: Diff ``` - ulong includeFlags = context.IncludeFlags; - bool included = selection.IsIncluded(includeFlags); + ConditionFlags includeFlags = context.IncludeConditionFlags; + bool included = selection.IsIncluded(includeFlags); ``` `ISelectionVisitorContext.IncludeFlags` is also replaced by `IncludeConditionFlags`: Diff ``` - ulong includeFlags = visitorContext.IncludeFlags; + ConditionFlags includeFlags = visitorContext.IncludeConditionFlags; ``` Pass the same `ConditionFlags` value to `SelectionEnumerator` and `AsSelector`: Diff ``` - var enumerator = new SelectionEnumerator(selectionSet, context.IncludeFlags); - var selector = selection.AsSelector(context.IncludeFlags); + var enumerator = new SelectionEnumerator(selectionSet, context.IncludeConditionFlags); + var selector = selection.AsSelector(context.IncludeConditionFlags); ``` The complete Core selection migration is: | Deprecated 16.6 member | 16.7 replacement | | ------------------------------------------------ | --------------------------------------------------------- | | ISelection.IsIncluded(ulong) | ISelection.IsIncluded(ConditionFlags) | | ISelection.IsDeferred(ulong) | ISelection.IsDeferred(ConditionFlags) | | Selection.IsSkipped(ulong) | Selection.IsSkipped(ConditionFlags) | | Selection.IsIncluded(ulong) | Selection.IsIncluded(ConditionFlags) | | Selection.IsDeferred(ulong) | Selection.IsDeferred(ConditionFlags) | | Selection.IsDeferred(ulong, DeferUsage?) | Selection.IsDeferred(ConditionFlags, DeferUsage?) | | Selection.GetPrimaryDeferUsage(ulong) | Selection.GetPrimaryDeferUsage(ConditionFlags) | | Selection.GetActiveDeferUsages(ulong) | Selection.GetActiveDeferUsages(ConditionFlags) | | Selection.HasActiveDeferUsage(ulong, DeferUsage) | Selection.HasActiveDeferUsage(ConditionFlags, DeferUsage) | | IResolverContext.IncludeFlags | IResolverContext.IncludeConditionFlags | | SelectionEnumerator(SelectionSet, ulong) | SelectionEnumerator(SelectionSet, ConditionFlags) | | AsSelector(this ISelection, ulong) | AsSelector(this ISelection, ConditionFlags) | | AsSelector(this Selection, ulong) | AsSelector(this Selection, ConditionFlags) | The Data projection APIs accept the same `ConditionFlags` value. Replace every raw `Select` sink as follows: | Deprecated 16.6 extension signature | 16.7 replacement | | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | Select(this IDataLoader, ISelection, ulong) | Select(this IDataLoader, ISelection, ConditionFlags) | | Select(this IDataLoader, ISelection, ulong) | Select(this IDataLoader, ISelection, ConditionFlags) | | Select(this IDataLoader>, ISelection, ulong) | Select(this IDataLoader>, ISelection, ConditionFlags) | | Select(this IDataLoader>, ISelection, ulong) | Select(this IDataLoader>, ISelection, ConditionFlags) | | Select(this IQueryable, Selection, ulong) | Select(this IQueryable, Selection, ConditionFlags) | At call sites, pass `IncludeConditionFlags` instead of `IncludeFlags` to each overload: Diff ``` - valueLoader.Select(selection, context.IncludeFlags); - arrayLoader.Select(selection, context.IncludeFlags); - listLoader.Select(selection, context.IncludeFlags); - pageLoader.Select(selection, context.IncludeFlags); - queryable.Select(selection, context.IncludeFlags); + valueLoader.Select(selection, context.IncludeConditionFlags); + arrayLoader.Select(selection, context.IncludeConditionFlags); + listLoader.Select(selection, context.IncludeConditionFlags); + pageLoader.Select(selection, context.IncludeConditionFlags); + queryable.Select(selection, context.IncludeConditionFlags); ``` The deprecated evaluation overloads continue to work for operations with at most 64 conditions. When an operation has more than 64 conditions of the corresponding kind, the deprecated raw inclusion overloads throw `InvalidOperationException` for every conditional selection and the deprecated raw defer overloads throw for every deferrable selection, including selections whose own conditions are all among the first 64; raw inclusion evaluation does not throw for an unconditional selection, and raw defer evaluation does not throw for a non-deferrable selection. The deprecated `SelectionEnumerator` and projection overloads throw for wider include operations. The deprecated `IncludeFlags` properties expose only the first 64 flags. Releases before 16.7 rejected operations with more than 64 conditions during compilation. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md) Maintained by ChilliCream. Last updated on **September 01, 2026** by **Michael Staib** --- # Performance - Hot Chocolate > Overview of Hot Chocolate performance features: faster startup, trusted documents, and persisted operations that cut parsing and validation overhead per request. Canonical source: https://chillicream.com/docs/hotchocolate/performance This section covers ways to improve the performance of your Hot Chocolate GraphQL server. ## Startup Performance Hot Chocolate constructs the schema eagerly at startup by default. You can register warmup tasks to pre-populate in-memory caches before the server begins accepting requests. [Learn more about server warmup](https://chillicream.com/docs/hotchocolate/server/warmup) ## Trusted Documents The size of individual GraphQL requests can become a significant bottleneck. This affects both the transport layer and the server, since large requests need to be parsed and validated repeatedly. Hot Chocolate supports persisted operations (also known as trusted documents) to address this. With persisted operations, you store operation documents on the server in a key-value store. When executing a persisted operation, the client sends the key under which the operation document is stored instead of the full document. This saves bandwidth and improves execution time because the server validates, parses, and compiles persisted operation documents only once. Hot Chocolate supports two flavors of persisted operations. ### Regular Persisted Operations The first approach stores operation documents ahead of time (before deployment). You extract the operations from your client applications at build time. This reduces the size of requests and the bundle size of your application because operations can be removed from the client code at build time and replaced with operation document hashes. 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](https://chillicream.com/docs/hotchocolate/performance/trusted-documents) ### Automatic Persisted Operations Automatic persisted operations let you store operation documents dynamically on the server at runtime. This approach gives your applications the same performance benefits as regular persisted operations without requiring a more complex build process. However, you do not get bundle size improvements for your applications because the operations are still needed at runtime. Both Strawberry Shake and [Apollo](https://www.apollographql.com/docs/apollo-server/performance/apq/) client support this approach. [Learn more about automatic persisted operations](https://chillicream.com/docs/hotchocolate/performance/automatic-persisted-operations) [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/performance/index.md) Maintained by ChilliCream. Last updated on **July 01, 2026** by **Tobias Tengler** --- # Automatic persisted operations - Hot Chocolate > Set up automatic persisted operations in Hot Chocolate with UseAutomaticPersistedOperationPipeline so clients can send operation hashes instead of full documents. Canonical source: https://chillicream.com/docs/hotchocolate/performance/automatic-persisted-operations This guide walks you through how automatic persisted operations work and how to set them up with a Hot Chocolate GraphQL server. ## How It Works The Automatic Persisted Queries (APQ) protocol was originally specified by Apollo and represents an evolution of the persisted operations feature that many GraphQL servers implement. Instead of storing operation documents ahead of time, the client stores operation documents dynamically. This preserves the performance benefits of persisted operations but removes the friction of setting up build processes that post-process client application source code. When the client makes a request to the server, it optimistically sends a short cryptographic hash instead of the full operation document. ### Optimized Path Hot Chocolate inspects the incoming request for an operation ID or a full GraphQL operation document. If the request has only an operation ID, the execution engine tries to resolve the full operation document from the operation document storage. If the storage contains an operation document that matches the provided operation ID, the request is upgraded to a fully valid GraphQL request and executed. ### New Operation Path If the operation document storage does not contain an operation document that matches the sent operation ID, Hot Chocolate returns an error result indicating the operation document was not found (this only happens the first time a client asks for a certain operation document). The client application then sends a second request with the specified operation document ID and the complete GraphQL operation document. This triggers Hot Chocolate to store the new operation document in its operation document storage and execute the operation, returning the result. ## Setup The following tutorial walks you through creating a Hot Chocolate GraphQL server configured to support automatic persisted operations. ### Step 1: Create a GraphQL Server Project Open your preferred terminal and select a directory for this tutorial. 1. Install the Hot Chocolate templates. Bash ``` dotnet new install HotChocolate.Templates ``` 1. Create a new Hot Chocolate GraphQL server project. Bash ``` dotnet new graphql ``` 1. Add the in-memory persisted operations package to your project. Bash ``` dotnet add package HotChocolate.PersistedOperations.InMemory ``` Warning All `HotChocolate.*` packages need to have the same version. ### Step 2: Configure Automatic Persisted Operations Configure your GraphQL server to handle automatic persisted operation requests. Register the in-memory operation storage and configure the automatic persisted operation request pipeline. 1. Configure the GraphQL server to use the automatic persisted operation pipeline. C# ``` builder .AddGraphQL() .AddQueryType() .UseAutomaticPersistedOperationPipeline(); ``` 1. Register the in-memory operation document storage. C# ``` builder .AddGraphQL() .AddQueryType() .UseAutomaticPersistedOperationPipeline() .AddInMemoryOperationDocumentStorage(); ``` 1. Add the Microsoft Memory Cache, which the in-memory operation document storage uses as the key-value store. C# ``` builder.Services.AddMemoryCache(); builder .AddGraphQL() .AddQueryType() .UseAutomaticPersistedOperationPipeline() .AddInMemoryOperationDocumentStorage(); ``` ### Step 3: Verify Server Setup Now that your server is set up with automatic persisted operations, verify that it works as expected. You can do this using your console and `curl`. For this example, you will use a dummy operation `{__typename}` with an MD5 hash serialized to base64 as an operation ID `71yeex4k3iYWQgg9TilDIg==`. The following steps walk you through the full automatic persisted operation flow. 1. Start the GraphQL server. Bash ``` dotnet run ``` 1. First, ask the GraphQL server to execute the operation with the optimized request containing only the operation hash. At this point, the server does not know this operation and returns an error. **Request** Bash ``` curl -g 'http://localhost:5000/graphql/?extensions={"persistedQuery":{"version":1,"md5Hash":"71yeex4k3iYWQgg9TilDIg=="}}' ``` **Response** The response indicates, as expected, that this operation is unknown. JSON ``` { "errors": [ { "message": "PersistedQueryNotFound", "extensions": { "code": "HC0020" } } ] } ``` 1. Next, store the dummy operation document on the server. Send the hash as before but now also provide the `query` parameter with the full GraphQL operation string. **Request** Bash ``` curl -g 'http://localhost:5000/graphql/?query={__typename}&extensions={"persistedQuery":{"version":1,"md5Hash":"71yeex4k3iYWQgg9TilDIg=="}}' ``` **Response** The GraphQL server responds with the operation result and indicates that the operation was stored on the server (`"persisted": true`). JSON ``` { "data": { "__typename": "Query" }, "extensions": { "persistedQuery": { "md5Hash": "71yeex4k3iYWQgg9TilDIg==", "persisted": true } } } ``` 1. Verify that you can now use the optimized request by executing the initial request containing only the operation document hash. **Request** Bash ``` curl -g 'http://localhost:5000/graphql/?extensions={"persistedQuery":{"version":1,"md5Hash":"71yeex4k3iYWQgg9TilDIg=="}}' ``` **Response** This time the server knows the operation and responds with the result. JSON ``` { "data": { "__typename": "Query" } } ``` > In this example, you used GraphQL HTTP GET requests, which are also useful in caching scenarios with CDNs. The automatic persisted operation flow also works with GraphQL HTTP POST requests. ### Step 4: Configure the Hashing Algorithm Hot Chocolate is configured to use the MD5 hashing algorithm by default, serialized to a base64 string. Hot Chocolate ships with support for MD5, SHA1, and SHA256 and can serialize the hash to base64 or hex. The following steps walk you through changing the hashing algorithm to SHA256 with hex serialization. 1. Add the SHA256 document hash provider to your GraphQL server configuration. C# ``` builder.Services.AddMemoryCache(); builder .AddGraphQL() .AddSha256DocumentHashProvider(HashFormat.Hex) .AddQueryType() .UseAutomaticPersistedOperationPipeline() .AddInMemoryOperationDocumentStorage(); ``` 1. Start the GraphQL server. Bash ``` dotnet run ``` 1. Verify that the server now operates with the new hash provider and serialization format. Store an operation document on the server with the new SHA256 hash: `7f56e67dd21ab3f30d1ff8b7bed08893f0a0db86449836189b361dd1e56ddb4b`. **Request** Bash ``` curl -g 'http://localhost:5000/graphql/?query={__typename}&extensions={"persistedQuery":{"version":1,"sha256Hash":"7f56e67dd21ab3f30d1ff8b7bed08893f0a0db86449836189b361dd1e56ddb4b"}}' ``` **Response** JSON ``` { "data": { "__typename": "Query" }, "extensions": { "persistedQuery": { "sha256Hash": "7f56e67dd21ab3f30d1ff8b7bed08893f0a0db86449836189b361dd1e56ddb4b", "persisted": true } } } ``` ### Step 5: Use a Persisted Operation Document Storage If you run multiple Hot Chocolate server instances and want to preserve stored operation documents after a server restart, use a persisted operation document storage. Hot Chocolate supports a file-system-based operation document storage, Azure Blob Storage, or a Redis cache. 1. Set up a Redis Docker container. Bash ``` docker run --name redis-stitching -p 7000:6379 -d redis ``` 1. Add the Redis persisted operations package to your server. Bash ``` dotnet add package HotChocolate.PersistedOperations.Redis ``` Warning All `HotChocolate.*` packages need to have the same version. 1. Configure the server to use Redis as operation document storage. C# ``` builder.Services.AddMemoryCache(); builder .AddGraphQL() .AddSha256DocumentHashProvider(HashFormat.Hex) .AddQueryType() .UseAutomaticPersistedOperationPipeline() .AddRedisOperationDocumentStorage(services => ConnectionMultiplexer.Connect("localhost:7000").GetDatabase()); ``` 1. Start the GraphQL server. Bash ``` dotnet run ``` 1. Verify that the server works correctly by storing an operation document first. **Request** Bash ``` curl -g 'http://localhost:5000/graphql/?query={__typename}&extensions={"persistedQuery":{"version":1,"sha256Hash":"7f56e67dd21ab3f30d1ff8b7bed08893f0a0db86449836189b361dd1e56ddb4b"}}' ``` **Response** JSON ``` { "data": { "__typename": "Query" }, "extensions": { "persistedQuery": { "sha256Hash": "7f56e67dd21ab3f30d1ff8b7bed08893f0a0db86449836189b361dd1e56ddb4b", "persisted": true } } } ``` 1. Stop your GraphQL server. 2. Start your GraphQL server again. Bash ``` dotnet run ``` 1. Execute the optimized operation to verify the operation document was correctly stored in your Redis cache. **Request** Bash ``` curl -g 'http://localhost:5000/graphql/?extensions={"persistedQuery":{"version":1,"sha256Hash":"7f56e67dd21ab3f30d1ff8b7bed08893f0a0db86449836189b361dd1e56ddb4b"}}' ``` **Response** JSON ``` { "data": { "__typename": "Query" } } ``` ## Next Steps - [Persisted Operations](https://chillicream.com/docs/hotchocolate/performance/trusted-documents) for pre-registering operations ahead of deployment. - [HTTP Transport](https://chillicream.com/docs/hotchocolate/server/http-transport) for details on HTTP GET caching. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/performance/automatic-persisted-operations.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Persisted operations - Hot Chocolate > Learn how to use persisted operations (trusted documents) in GraphQL with Hot Chocolate. Canonical source: https://chillicream.com/docs/hotchocolate/performance/trusted-documents Persisted operations (also known as trusted documents) let you pre-register all required operations of your clients. You extract the operations from your client applications at build time and place them in the server's operation document storage. Extracting operations is supported by client libraries like [Relay](https://relay.dev/docs/guides/persisted-queries/) and in the case of [Strawberry Shake](https://chillicream.com/products/strawberryshake) no additional work is needed. ## How It Works - All operations that your clients execute are extracted during the client build process. Individual operation documents are hashed to generate a unique identifier for each operation. - Before your server is deployed, the extracted operation documents are placed in the server's operation document storage. - After the server has been deployed, clients execute persisted operations by specifying the operation ID (hash) in their requests. - If Hot Chocolate finds an operation that matches the specified hash in the operation document storage, it executes the operation and returns the result to the client. Note There are also [automatic persisted operations](https://chillicream.com/docs/hotchocolate/performance/automatic-persisted-operations), which let clients persist operation documents at runtime. They might be a better fit if your API is used by many clients with different requirements. ## Benefits There are two main benefits to using persisted operations: **Performance** - Only a hash and optionally variables need to be sent to the server, reducing network traffic. - Operations no longer need to be embedded into the client code, reducing the bundle size in the case of websites. - Hot Chocolate can optimize the execution of persisted operations because they are always the same. **Security** The server can be configured to [only execute persisted operations](#blocking-regular-operations) and refuse any other operation provided by a client. This eliminates a whole class of potential attack vectors because malicious actors can no longer craft and execute harmful operations against your GraphQL server. ## Usage Instruct your server to handle persisted operations by calling `UsePersistedOperationPipeline()` on the `IRequestExecutorBuilder`: C# ``` builder .AddGraphQL() .AddQueryType() .UsePersistedOperationPipeline(); ``` ## Production-Ready Persisted Operations Setting up a persisted operation file is not sufficient for a robust production environment. A key aspect of managing persisted operations at scale involves version management and ensuring compatibility with your GraphQL schema. The client registry is the resource for this purpose. The client registry simplifies the management of your GraphQL clients. It stores and retrieves persisted operation documents through their hashes and ensures that operations are validated against the current schema on publish, preventing runtime errors due to schema-operation mismatches. It also supports versioning of your clients, allowing updates and maintenance without disrupting existing operations. Check out the [client registry documentation](https://chillicream.com/docs/nitro/apis/client-registry) for more information. ## Storage Mechanisms Hot Chocolate supports several operation document storages for persisted operations. ### Filesystem To load persisted operation documents from the filesystem, add the following package: Bash ``` dotnet add package HotChocolate.PersistedOperations.FileSystem ``` Warning All `HotChocolate.*` packages need to have the same version. Specify where the persisted operation documents are located. The argument to `AddFileSystemOperationDocumentStorage()` specifies the directory containing the operation documents. C# ``` builder .AddGraphQL() .AddQueryType() .UsePersistedOperationPipeline() .AddFileSystemOperationDocumentStorage("./persisted_operations"); ``` When presented with an operation document hash, Hot Chocolate checks the specified folder for a file in the format: `{Hash}.graphql`. Example: `0c95d31ca29272475bf837f944f4e513.graphql` This file is expected to contain the operation document that the hash was generated from. Warning Ensure that the server has access to the directory. ### Redis To load persisted operation documents from Redis, add the following package: Bash ``` dotnet add package HotChocolate.PersistedOperations.Redis ``` Warning All `HotChocolate.*` packages need to have the same version. Specify where the persisted operation documents are located. Using `AddRedisOperationDocumentStorage()`, point to a specific Redis database containing the operation documents. C# ``` builder .AddGraphQL() .AddQueryType() .UsePersistedOperationPipeline() .AddRedisOperationDocumentStorage(services => ConnectionMultiplexer.Connect("host:port").GetDatabase()); ``` Keys in the specified Redis database are expected to be operation IDs (hashes) and contain the actual operation document as the value. ### Azure Blob Storage To load persisted operation documents from Azure Blob Storage, add the following package: Bash ``` dotnet add package HotChocolate.PersistedOperations.AzureBlobStorage ``` Warning All `HotChocolate.*` packages need to have the same version. Specify where the persisted operation documents are located. Using `AddAzureBlobStorageOperationDocumentStorage()`, point to a specific Azure Blob Storage container. The blob's name is the hash of the query, and its content is the corresponding GraphQL query. Caution The Azure Blob Storage container must already exist when Hot Chocolate uses it for the first time. C# ``` builder .AddGraphQL() .AddQueryType() .UsePersistedOperationPipeline() .AddAzureBlobStorageOperationDocumentStorage(services => services.GetService().GetBlobContainerClient("hotchocolate")); ``` Unlike Redis, a Blob Storage client has no built-in way to set the expiration of files in Azure Blob Storage. However, you can define [a Lifecycle Management Policy](https://learn.microsoft.com/en-us/azure/storage/blobs/lifecycle-management-overview?tabs=azure-portal). The following sample policy instructs Azure to remove all files from the `hotchocolate` container when they have not been accessed for 10 days. JSON ``` { "rules": [ { "enabled": true, "name": "remove-after-10d", "type": "Lifecycle", "definition": { "actions": { "baseBlob": { "delete": { "daysAfterLastAccessTimeGreaterThan": 10 } } }, "filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["hotchocolate/"] } } } ] } ``` ## Hashing Algorithms By default, Hot Chocolate uses the MD5 hashing algorithm. You can override this default by specifying a `DocumentHashProvider`: C# ``` builder .AddGraphQL() .AddQueryType() // choose one of the following providers .AddMD5DocumentHashProvider() .AddSha256DocumentHashProvider() .AddSha1DocumentHashProvider() .UsePersistedOperationPipeline() .AddFileSystemOperationDocumentStorage("./persisted_operations"); ``` You can also configure how these hashes are encoded by specifying a `HashFormat` as argument: C# ``` AddSha256DocumentHashProvider(HashFormat.Hex) AddSha256DocumentHashProvider(HashFormat.Base64) ``` Note [Relay](https://relay.dev/) uses the MD5 hashing algorithm. No additional Hot Chocolate configuration is required. ## Blocking Regular Operations If you want to disallow any dynamic operations, enable `OnlyAllowPersistedDocuments`: C# ``` builder .AddGraphQL() // Omitted for brevity .ModifyRequestOptions( options => options .PersistedOperations .OnlyAllowPersistedDocuments = true); ``` This blocks any dynamic operations that do not contain the `id` of a persisted operation. You might still want to allow the execution of dynamic operations in certain circumstances. Override the `OnlyAllowPersistedDocuments` rule on a per-request basis using the `AllowNonPersistedOperation` method on the `OperationRequestBuilder`. Implement a custom [IHttpRequestInterceptor](https://chillicream.com/docs/hotchocolate/server/interceptors#ihttprequestinterceptor) and call `AllowNonPersistedOperation` if a certain condition is met: C# ``` builder .AddGraphQL() // Omitted for brevity .AddHttpRequestInterceptor() .ModifyRequestOptions( options => options .PersistedOperations .OnlyAllowPersistedDocuments = true); public class CustomHttpRequestInterceptor : 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); } } ``` In the above example, requests containing the `X-Developer` header can execute dynamic operations. In your production application, replace this check with an authorization policy, an API key, or whatever fits your requirements. ## Client Expectations A client is expected to send an `id` field containing the operation document hash instead of a `query` field. **HTTP POST** JSON ``` { "id": "0c95d31ca29272475bf837f944f4e513", "variables": { // ... } } ``` Note [Relay's persisted queries documentation](https://relay.dev/docs/guides/persisted-queries/#network-layer-changes) uses `doc_id` instead of `id`. Be sure to change it to `id`. ## Next Steps - [Automatic Persisted Operations](https://chillicream.com/docs/hotchocolate/performance/automatic-persisted-operations) for dynamically storing operations at runtime. - [Interceptors](https://chillicream.com/docs/hotchocolate/server/interceptors) for per-request customization. - [Client Registry](https://chillicream.com/docs/nitro/apis/client-registry) for production-ready persisted operation management. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/performance/trusted-documents.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # GraphQL Resolvers in Hot Chocolate > Introduction to resolvers in Hot Chocolate: how the resolver tree executes a query, defining resolver methods, accessing arguments, and injecting services. Canonical source: https://chillicream.com/docs/hotchocolate/resolvers Every field in a GraphQL schema is backed by a resolver function that produces the field's value. Understanding how resolvers compose into a tree is the key mental model for building efficient GraphQL APIs with Hot Chocolate. ## The Resolver Tree When Hot Chocolate receives a query, it builds a resolver tree that mirrors the shape of the request. Consider this query: GraphQL ``` query { me { name company { id name } } } ``` This produces the following resolver tree: The execution engine traverses this tree starting from root resolvers. A child resolver can only execute after its parent has produced a value. Sibling resolvers at the same level run in parallel. Because of this parallel execution, resolvers (except top-level mutation field resolvers) must be free of side effects. Execution completes when every resolver in the tree has produced a result. ## Defining a Resolver Resolvers can be defined in a way that should feel very familiar to C# developers, as they either translate to methods or delegates. In the implementation-first approach, a public method is automatically inferred as a resolver. This means the method defines both the field in your schema and the logic to resolve its value. C# ``` [QueryType] public partial class Query { public static string Foo() => "Bar"; } ``` This generates the following schema: SDL ``` type Query { foo: String! } ``` Resolvers do not have to be methods. Public properties are also inferred as resolvers and exposed as fields in your schema. C# ``` [QueryType] public partial class Query { public static User User => new User("Ted"); } public record User(string Name); ``` In this case, the property `Name` of the `User` object is also inferred as a resolver. ### Async Resolver Resolvers can be synchronous or asynchronous. Most data fetching operations, such as calling a service or database, are asynchronous. The most important aspect of async resolvers is to honor the CancellationToken. This allows execution to be cancelled if the client abandons the request, preventing unnecessary work and resource usage. When using the implementation-first approach, you can add a `CancellationToken` parameter to your resolver method. The execution engine will automatically inject the request's cancellation token. C# ``` public class Query { public async Task GetProductByIdAsync( int id, ProductService productService, CancellationToken cancellationToken) => await productService.GetAsync(cancellationToken); } ``` ## Arguments In GraphQL, fields are conceptually similar to methods in C#. Just like methods, fields can have arguments, and you can access these argument values directly in your resolvers. When using the implementation-first approach, any parameter in your resolver method that is not a service, a `CancellationToken`, or specially annotated is treated as a GraphQL argument. The execution engine will inject the argument value from the query into these parameters. For example, in the method below, the `id` parameter is recognized as an argument, while `ProductService` is injected as a service from the DI container. C# ``` public class Query { public async Task GetProductByIdAsync( int id, ProductService productService, CancellationToken cancellationToken) => await productService.GetAsync(cancellationToken); } ``` [Learn more about arguments](https://chillicream.com/docs/hotchocolate/defining-a-schema/arguments) ## Injecting Services Hot Chocolate automatically recognizes types registered in the DI container and injects them into resolver parameters. C# ``` public class Query { public List GetUsers(UserService userService) => userService.GetUsers(); } ``` While you can take attributes to annotate services, you do not have to for non-keyed services. C# ``` public class Query { public List GetUsers([Service] UserService userService) => userService.GetUsers(); } ``` [Learn more about dependency injection](https://chillicream.com/docs/hotchocolate/resolvers/dependency-injection) ## Accessing parent values Each field resolver has access to the value that was resolved for its parent type. For example, consider the following schema: SDL ``` type Query { me: User!; } type User { id: ID!; friends: [User!]!; } ``` The `User` schema type is represented by a `User` runtime class. The `id` field is a property on this class. C# ``` public class User { public string Id { get; set; } } ``` The `friends` resolver, by contrast, is independent: it is not declared on the `User` type and uses the user's `Id` to compute its result. From the `friends` resolver's perspective, the `User` runtime object is its _parent_. Access the parent value like this: In the implementation-first approach, the parent object can be injected as a resolver parameter: C# ``` [ObjectType] public static partial class UserNode { public static Task> GetFriendsAsync( [Parent] User user, UserService userService, CancellationToken cancellationToken) { // Omitted code for brevity } } ``` If database projections are enabled, the parent object may only contain the fields requested by the client. To ensure the projections engine also loads properties required by the resolver, declare those requirements on the parent parameter: C# ``` [ObjectType] public static partial class UserNode { public static Task> GetFriendsAsync( [Parent(requires: nameof(User.Id))] User user, UserService userService, CancellationToken cancellationToken) { // Omitted code for brevity } } ``` Use `nameof` to make this requirement refactoring-safe. ## What's in This Section - [Dependency Injection](https://chillicream.com/docs/hotchocolate/resolvers/dependency-injection) explains how services are injected into resolvers, scoping behavior, keyed services, and switching the service provider. - [Errors](https://chillicream.com/docs/hotchocolate/resolvers/errors) covers how exceptions become GraphQL errors, error filters for mapping domain exceptions, and throwing `GraphQLException` for explicit errors. - [Field Middleware](https://chillicream.com/docs/hotchocolate/resolvers/field-middleware) shows how to build reusable middleware that runs before or after resolvers, including ordering, class-based middleware, and attribute-based middleware. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/resolvers/index.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Dependency Injection - Hot Chocolate > Inject services into Hot Chocolate resolvers as method parameters, control service scoping, use keyed services, and switch the resolver service provider. Canonical source: https://chillicream.com/docs/hotchocolate/resolvers/dependency-injection If you're unfamiliar with dependency injection, the ASP.NET Core documentation is a good starting point: - [Dependency injection in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/dependency-injection) Dependency injection in Hot Chocolate works the same as in a regular ASP.NET Core application: register services with the DI container as usual. C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddSingleton() .AddScoped() .AddTransient(); ``` Hot Chocolate automatically recognizes types registered as services in the DI container and injects them into resolver method parameters without requiring any attribute. This works similarly to [Minimal APIs parameter binding](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding). When the execution engine encounters a resolver parameter whose type is registered in the DI container, it resolves the service automatically. You do not need to apply the `[Service]` attribute. C# ``` public static async Task GetBookByIdAsync( Guid id, BookService bookService) { return await bookService.GetBookAsync(id); } ``` ## Resolver Injection Injecting services at the method level has several benefits: - The execution engine can optimize the resolver and adjust the execution strategy based on the needs of a specific service. - Refactoring (moving the resolver method between classes) becomes easier because the resolver does not depend on its outer class. - If many resolvers are colocated in a single class we only need to resolve the services that are needed for resolvers that are actually being executed In the following example, `BookService` is injected automatically when it is registered as a service in the DI container: C# ``` [QueryType] public static partial class Query { public static async Task GetBookByIdAsync( Guid id, BookService bookService) { return await bookService.GetBookAsync(id); } } ``` ### Service Scoping In GraphQL, resolvers are expected to be side-effect free. The execution engine may run them in parallel or out of order, so relying on shared mutable state within services can lead to issues. For example, if multiple resolvers use the same Entity Framework DbContext concurrently, it can cause thread-safety problems and execution errors. To avoid this, Hot Chocolate creates a new service scope for each async resolver and each DataLoader dispatch. This ensures every resolver or DataLoader execution receives its own service instance. If you want to change the default scoping behavior, you can update the GraphQL options. C# ``` builder .AddGraphQL() .ModifyOptions(o => { o.DefaultQueryDependencyInjectionScope = DependencyInjectionScope.Resolver; o.DefaultMutationDependencyInjectionScope = DependencyInjectionScope.Request; }); ``` You can also override the scope on a per-resolver basis: C# ``` [QueryType] public static class Query { [UseRequestScope] public static async Task GetBookByIdAsync( Guid id, BookService bookService) => // ... } ``` ## Constructor Injection If you're coming from an ASP.NET core controller-based workflow, you might be used to injecting services into your types via the constructor. In Hot Chocolate, you should **avoid** injecting services into GraphQL type definitions for these reasons: - GraphQL type definitions are registered as singletons, so constructor-injected services would also become singletons. - Hot Chocolate cannot synchronize access to those services during request execution, which can cause issues with services that are not thread-safe (such as EF Core's DbContext). Note This guidance does not apply to your own application services. For example, `ServiceA` can still inject `ServiceB` via constructor injection. ## Keyed Services A keyed service is a service registered in the DI container with an associated key or name. This allows you to register multiple instances of the same service type, each identified by a unique key. Keyed services are useful when you need different configurations or implementations of the same service type within your application. You can register a keyed service like this: C# ``` internal enum BookServiceKey { Primary } builder.Services.AddKeyedScoped("bookService"); builder.Services.AddKeyedScoped(BookServiceKey.Primary); ``` You can then access the keyed service in your resolver by applying the `[Service]` attribute with the key. `[Service]` has parity with `[FromKeyedServices]` and supports any attribute constant, including strings, enum values, and integers. String keys remain supported unchanged. C# ``` [QueryType] public static class Query { public static async Task GetBookByIdAsync( Guid id, [Service("bookService")] BookService bookService, [Service(BookServiceKey.Primary)] BookService primaryBookService) { return await bookService.GetBookAsync(id); } } ``` ## Switching the Service Provider While Hot Chocolate's internals rely on Microsoft's dependency injection container, you are not required to manage your own dependencies using Microsoft's container. By default, Hot Chocolate uses the request-scoped [HttpContext.RequestServices](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestservices) `IServiceProvider` to provide services to your resolvers. You can switch out the service provider used for GraphQL requests, as long as your DI container implements the [IServiceProvider](https://docs.microsoft.com/dotnet/api/system.iserviceprovider) interface and supports scoping. To switch the service provider, call [SetServices](https://chillicream.com/docs/hotchocolate/server/interceptors#setservices) on the [OperationRequestBuilder](https://chillicream.com/docs/hotchocolate/server/interceptors#operationrequestbuilder) in both the [IHttpRequestInterceptor](https://chillicream.com/docs/hotchocolate/server/interceptors#ihttprequestinterceptor) and the [ISocketSessionInterceptor](https://chillicream.com/docs/hotchocolate/server/interceptors#isocketsessioninterceptor). C# ``` public sealed class HttpRequestInterceptor : DefaultHttpRequestInterceptor { public override async ValueTask OnCreateAsync( HttpContext context, IRequestExecutor requestExecutor, OperationRequestBuilder requestBuilder, CancellationToken cancellationToken) { // keeping these lines is important! await base.OnCreateAsync( context, requestExecutor, requestBuilder, cancellationToken); requestBuilder.SetServices(YOUR_SERVICE_PROVIDER); } } public sealed class SocketSessionInterceptor : DefaultSocketSessionInterceptor { public override async ValueTask OnRequestAsync( ISocketConnection connection, OperationRequestBuilder requestBuilder, CancellationToken cancellationToken) { // keeping these lines is important! await base.OnRequestAsync( connection, requestBuilder, cancellationToken); requestBuilder.SetServices(YOUR_SERVICE_PROVIDER); } } ``` Register these interceptors for them to take effect: C# ``` builder .AddGraphQL() .AddHttpRequestInterceptor() .AddSocketSessionInterceptor(); ``` [Learn more about interceptors](https://chillicream.com/docs/hotchocolate/server/interceptors) ## Next Steps - [Interceptors](https://chillicream.com/docs/hotchocolate/server/interceptors) for setting request-scoped state and services. - [Global State](https://chillicream.com/docs/hotchocolate/server/global-state) for sharing per-request data between resolvers. - [Migrate from v15 to v16](https://chillicream.com/docs/hotchocolate/migrating/migrate-from-15-to-16#clearer-separation-between-schema-and-application-services) for the full migration details on schema vs application services. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/resolvers/dependency-injection.md) Maintained by ChilliCream. Last updated on **August 30, 2026** by **Michael Staib** --- # Errors - Hot Chocolate > Learn how to handle, create, and filter GraphQL errors in Hot Chocolate. Canonical source: https://chillicream.com/docs/hotchocolate/resolvers/errors In GraphQL, errors are not all-or-nothing. If a resolver fails, the rest of the query can still return data. This is called a _field error_ (or non-terminating error). When this happens, the failed field returns `null`, and an entry is added to the `errors` array. Other fields continue to resolve as usual. ## Exceptions The easiest way to signal an error is to throw an exception. Hot Chocolate will catch any exception thrown during resolver execution and automatically turn it into a GraphQL error. C# ``` [QueryType] public static partial class Query { public static Book GetBook() { throw new InvalidOperationException("Something went wrong."); } } ``` By default, Hot Chocolate does **not** send exception details to the client. Instead, the response contains a generic message to avoid exposing internal information. JSON ``` { "errors": [ { "message": "Unexpected Execution Error", "locations": [{ "line": 1, "column": 3 }], "path": ["book"] } ], "data": { "book": null } } ``` ## Error Filters If you use typed exceptions and want to return specific GraphQL errors, you can implement error filters. Error filters let you catch errors before they reach the client and rewrite them as needed. For example, if your service throws a `NotFoundException`, you can map it to a clear error message and code: C# ``` builder .AddGraphQL() .AddErrorFilter(error => { if (error.Exception is NotFoundException ex) { return ErrorBuilder .FromError(error) .SetMessage(ex.Message) .SetCode("NOT_FOUND") .Build(); } return error; }); ``` Now, when a resolver throws a `NotFoundException`, the client receives a structured error instead of a generic message: JSON ``` { "errors": [ { "message": "The book with ID '123' was not found.", "locations": [{ "line": 1, "column": 3 }], "path": ["book"], "extensions": { "code": "NOT_FOUND" } } ], "data": { "book": null } } ``` Note Errors are immutable. Methods like `WithMessage`, `WithCode`, and `RemoveExtension` return a new error instance. Use `ErrorBuilder.FromError(error)` to change multiple properties at once. ## GraphQLException If you want full control over the error in a resolver, throw a `GraphQLException`. Unlike regular exceptions, these errors are sent to the client as-is, without being wrapped in a generic message. C# ``` [QueryType] public static partial class Query { public static Book GetBook() { throw new GraphQLException("The book could not be found."); } } ``` JSON ``` { "errors": [ { "message": "The book could not be found.", "locations": [{ "line": 1, "column": 3 }], "path": ["book"] } ], "data": { "book": null } } ``` You can also use `GraphQLException` with an `ErrorBuilder` to add a code, extensions, or multiple errors: C# ``` throw new GraphQLException( ErrorBuilder .New() .SetMessage("The book could not be found.") .SetCode("BOOK_NOT_FOUND") .Build()); ``` JSON ``` { "errors": [ { "message": "The book could not be found.", "locations": [{ "line": 1, "column": 3 }], "path": ["book"], "extensions": { "code": "BOOK_NOT_FOUND" } } ] } ``` ## Next Steps - [Mutation conventions](https://chillicream.com/docs/hotchocolate/defining-a-schema/mutations) for structured mutation error handling - [Instrumentation](https://chillicream.com/docs/hotchocolate/server/instrumentation) for logging and diagnostics - [Options reference](https://chillicream.com/docs/hotchocolate/server/options) for `IncludeExceptionDetails` and other settings [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/resolvers/errors.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Field Middleware - Hot Chocolate > Learn how to create and apply field middleware in Hot Chocolate to run reusable logic before or after field resolvers. Canonical source: https://chillicream.com/docs/hotchocolate/resolvers/field-middleware Field middleware in Hot Chocolate lets you add reusable logic to a field, either before or after the field resolver runs. This is useful for adding features like logging, validation, or authorization. You can stack multiple middleware, and they run in the order you declare them. The field resolver always comes last in the chain. Each middleware only knows about the next step in the chain. It can: - Run logic before the next step - Run logic after the next step (including after the resolver) - Skip the next step entirely Every middleware receives an `IMiddlewareContext`. This extends `IResolverContext`, so you can use all the same APIs as in a resolver. The `IMiddlewareContext` also has properties like `Result`, which holds the value returned by the resolver or another middleware. ## Middleware Order The order in which you add middleware matters. It controls how the resolver result is processed. For example, if you use both `UsePaging` and `UseFiltering`, filtering should happen before paging. So, you declare `UsePaging` before `UseFiltering`. C# ``` descriptor .UsePaging() .UseFiltering() .Resolve(context => { // Omitted code for brevity }); ``` At first, this might look backwards. The diagram below explains why this order works: Middleware runs in the order you declare it, but the result from the resolver travels back through the chain in reverse. This is why the order is important. ## Defining Field Middleware You can create field middleware as either a delegate or a class. In both cases, you get a `FieldDelegate` (which calls the next middleware) and the `IMiddlewareContext`. When you await the `FieldDelegate`, you let all later middleware and the resolver run before your code continues. ### Delegate-based middleware To define middleware as a delegate, use the code-first API: C# ``` public class QueryType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor .Field("example") .Use(next => async context => { // Runs before the next middleware and the field resolver // Call the next middleware or the resolver await next(context); // Runs after all later middleware and the resolver }) .Resolve(context => { // Omitted for brevity }); } } ``` #### Reusing the middleware delegate The example above adds middleware to one field. To reuse it, create an extension method on `IObjectFieldDescriptor`: C# ``` public static class MyMiddlewareObjectFieldDescriptorExtension { public static IObjectFieldDescriptor UseMyMiddleware( this IObjectFieldDescriptor descriptor) { return descriptor .Use(next => async context => { // Omitted code for brevity await next(context); // Omitted code for brevity }); } } ``` > It's a good idea to start your extension method name with `Use` to show it adds middleware. Now you can use this middleware on any field in your schema: C# ``` public class QueryType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor .Field("example") .UseMyMiddleware() .Resolve(context => { // Omitted for brevity }); } } ``` ### Class-based middleware If you prefer, you can write middleware as a class. Create a class that takes a `FieldDelegate` in its constructor and has a method called `InvokeAsync` or `Invoke`. C# ``` public class MyMiddleware { private readonly FieldDelegate _next; public MyMiddleware(FieldDelegate next) { _next = next; } public async Task InvokeAsync(IMiddlewareContext context) { // Runs before the next middleware and the resolver await _next(context); // Runs after all later middleware and the resolver } } ``` You can inject services into the constructor (for singletons) or as parameters on `InvokeAsync` (for scoped or transient services): C# ``` public class MyMiddleware { private readonly FieldDelegate _next; private readonly IMySingletonService _singletonService; public MyMiddleware(FieldDelegate next, IMySingletonService singletonService) { _next = next; _singletonService = singletonService; } public async Task InvokeAsync(IMiddlewareContext context, IMyScopedService scopedService) { // Omitted code for brevity } } ``` This flexibility is why there is no interface or base class for field middleware. #### Applying class-based middleware To use your class-based middleware, call `Use()` on the field: C# ``` public class QueryType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor .Field("example") .Use() .Resolve(context => { // Omitted for brevity }); } } ``` It's still a good idea to wrap `Use()` in an extension method like `UseMyMiddleware()`. This makes it easier to change the middleware later without updating every field. If you need to pass a custom argument, use the factory overload: C# ``` descriptor .Field("example") .Use((provider, next) => new MyMiddleware(next, "custom", provider.GetRequiredService())); ``` ## Using Middleware as an Attribute You can also add middleware to resolvers that use attributes. To do this, create an attribute that inherits from `ObjectFieldDescriptorAttribute` and call your middleware in the `OnConfigure` method. > In C#, attribute order is not guaranteed. Middleware attributes use the `CallerLineNumberAttribute` to capture the line number at compile time, which sets the order. Avoid inheriting middleware attributes from a base method or property, as this can make the order unclear. Always pass the `order` argument if you inherit from another middleware attribute. Start your attribute name with `Use` to show it adds middleware. C# ``` public class UseMyMiddlewareAttribute : ObjectFieldDescriptorAttribute { public UseMyMiddlewareAttribute([CallerLineNumber] int order = 0) { Order = order; } protected override void OnConfigure(IDescriptorContext context, IObjectFieldDescriptor descriptor, MemberInfo member) { descriptor.UseMyMiddleware(); } } ``` Now you can add the attribute to a resolver: C# ``` public class Query { [UseMyMiddleware] public string MyResolver() { // Omitted code for brevity } } ``` ## Accessing the Resolver Result The `IMiddlewareContext` has a `Result` property you can use to read or change the value returned by the resolver: C# ``` descriptor .Use(next => async context => { await next(context); // After calling next(context), you can access the result object? result = context.Result; // You can check the type and work with it if (result is string stringResult) { // Work with the stringResult } }); ``` Middleware can also set or override the result by assigning to `context.Result`. > The field resolver only runs if no earlier middleware has set the `Result` property. If any middleware sets `Result`, the resolver is skipped. ## Short-Circuiting Sometimes, you may want to stop the middleware chain and skip the field resolver. To do this, simply do not call the `FieldDelegate` (`next`). C# ``` descriptor .Use(next => context => { if (context.Parent() is IDictionary dict) { context.Result = dict[context.Field.Name]; // The rest of the middleware and the resolver will not run return Task.CompletedTask; } else { return next(context); } }) ``` ## Next Steps - Learn more about [resolvers](https://chillicream.com/docs/hotchocolate/resolvers) for field resolution - Explore [filtering](https://chillicream.com/docs/hotchocolate/fetching-data/filtering) and [sorting](https://chillicream.com/docs/hotchocolate/fetching-data/sorting) middleware - Read about [pagination](https://chillicream.com/docs/hotchocolate/fetching-data/pagination) middleware [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/resolvers/field-middleware.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Securing Your API - Hot Chocolate > Overview of securing a Hot Chocolate GraphQL API: cost analysis for public APIs, trusted documents for private ones, plus authorization and request limits. Canonical source: https://chillicream.com/docs/hotchocolate/security Securing a GraphQL API requires more than authentication and authorization. Unlike REST, where each endpoint has a predictable cost, a single GraphQL query can traverse deep relationships and request large datasets. You need a strategy that matches your API's threat model. Hot Chocolate provides two golden paths depending on whether your API is public or private. ## Public APIs: Cost Analysis Public APIs face unpredictable clients. You do not control who sends queries or how complex those queries are. An attacker can craft a deeply nested query that consumes significant server resources. **Cost analysis** is your primary defense for public APIs. It assigns a weight to each field and list in your schema, then calculates the total cost of an incoming query before executing it. Queries that exceed your cost budget are rejected. Combine cost analysis with: - **Pagination limits** to cap the number of items returned per connection. - **Execution depth limits** to prevent deeply nested queries. - **Execution timeouts** to abort long-running queries. [Learn more about cost analysis](https://chillicream.com/docs/hotchocolate/security/cost-analysis) ## Private APIs: Trusted Documents Private APIs serve known clients that you control, such as your own web or mobile applications. For these APIs, **trusted documents** (also called persisted operations) provide the strongest security guarantee. 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](https://chillicream.com/docs/hotchocolate/performance/trusted-documents) ## Defense in Depth Regardless of whether your API is public or private, apply these additional protections: ### Authentication 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](https://chillicream.com/docs/hotchocolate/security/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](https://chillicream.com/docs/hotchocolate/security/authorization) ### Request Limits Hot Chocolate enforces limits at every stage of request processing -- parsing, validation, and execution -- to keep resource consumption bounded. This includes limits on fields, directives, nesting depth, execution depth, timeouts, and more. [Learn more about request limits](https://chillicream.com/docs/hotchocolate/security/request-limits) ### Introspection Introspection powers developer tools but can also reveal your schema to attackers. You can restrict or disable introspection in production. [Learn more about introspection](https://chillicream.com/docs/hotchocolate/security/introspection#disabling-introspection) ### FIPS Compliance Hot Chocolate uses MD5 for document hashing by default. If you need FIPS compliance, switch to SHA256: C# ``` builder .AddGraphQL() .AddSha256DocumentHashProvider(); ``` [Learn more about hashing providers](https://chillicream.com/docs/hotchocolate/performance/trusted-documents#hashing-algorithms) ## Next Steps - **Building a public API?** Start with [Cost Analysis](https://chillicream.com/docs/hotchocolate/security/cost-analysis). - **Building a private API?** Start with [Trusted Documents](https://chillicream.com/docs/hotchocolate/performance/trusted-documents). - **Need authentication?** See [Authentication](https://chillicream.com/docs/hotchocolate/security/authentication). - **Need authorization?** See [Authorization](https://chillicream.com/docs/hotchocolate/security/authorization). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/security/index.md) Maintained by ChilliCream. Last updated on **July 01, 2026** by **Tobias Tengler** --- # Authentication - Hot Chocolate > Add authentication to a Hot Chocolate GraphQL server with ASP.NET Core's AddAuthentication and access the authenticated ClaimsPrincipal inside your resolvers. Canonical source: https://chillicream.com/docs/hotchocolate/security/authentication Authentication determines a user's identity. It is a prerequisite for authorization and also lets you access the authenticated user in your resolvers, for example to build a `me` field that returns the current user's profile. Hot Chocolate integrates with the ASP.NET Core authentication system, so you can reuse existing authentication configuration and any supported authentication provider. [Learn more about authentication in ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/authentication) ## Setup Setting up authentication follows the same pattern as any ASP.NET Core application. The example below uses JWT bearer tokens, but you can substitute any authentication scheme that ASP.NET Core supports. ### 1\. Install the JWT Bearer Package Bash ``` dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer ``` ### 2\. Register the Authentication Scheme C# ``` var signingKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes("MySuperSecretKey")); builder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = "https://auth.chillicream.com", ValidAudience = "https://graphql.chillicream.com", ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey }; }); ``` > This is an example configuration. Use a proper key management solution for production. ### 3\. Add Authentication Middleware Register the authentication middleware in the request pipeline: C# ``` app.UseRouting(); app.UseAuthentication(); app.UseEndpoints(endpoints => { endpoints.MapGraphQL(); }); ``` ### 4\. Install the Hot Chocolate Authorization Package Bash ``` dotnet add package HotChocolate.AspNetCore.Authorization ``` Warning All `HotChocolate.*` packages need to have the same version. ### 5\. Register Authorization on the Schema C# ``` builder .AddGraphQL() .AddAuthorization() .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](https://chillicream.com/docs/hotchocolate/security/authorization). ## Accessing the ClaimsPrincipal After authentication, the `ClaimsPrincipal` of the current user is available in your resolvers. C# ``` [QueryType] public static partial class UserQueries { public static User? GetMe(ClaimsPrincipal claimsPrincipal, UserService users) { var userId = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier); if (userId is null) { return null; } return users.GetById(userId); } } ``` Use `ClaimsPrincipal` to read claims such as the user ID, email, or roles: C# ``` var userId = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier); var email = claimsPrincipal.FindFirstValue(ClaimTypes.Email); var isAdmin = claimsPrincipal.IsInRole("Administrator"); ``` ## Modifying the ClaimsPrincipal If you need to add claims or identities to the `ClaimsPrincipal` before it reaches your resolvers, register an `IHttpRequestInterceptor`: C# ``` public class HttpRequestInterceptor : DefaultHttpRequestInterceptor { public override ValueTask OnCreateAsync( HttpContext context, IRequestExecutor requestExecutor, OperationRequestBuilder requestBuilder, CancellationToken cancellationToken) { var identity = new ClaimsIdentity(); identity.AddClaim(new Claim(ClaimTypes.Country, "us")); context.User.AddIdentity(identity); return base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken); } } ``` C# ``` builder .AddGraphQL() .AddHttpRequestInterceptor(); ``` [Learn more about interceptors](https://chillicream.com/docs/hotchocolate/server/interceptors) ## Next Steps - **Need to restrict access to fields?** See [Authorization](https://chillicream.com/docs/hotchocolate/security/authorization). - **Need an overview of security options?** See [Security Overview](https://chillicream.com/docs/hotchocolate/security). - **Need to customize request handling?** See [Interceptors](https://chillicream.com/docs/hotchocolate/server/interceptors). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/security/authentication.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Authorization - Hot Chocolate > Protect fields and types in Hot Chocolate with the @authorize directive, integrating ASP.NET Core roles and policies for fine-grained access control. Canonical source: https://chillicream.com/docs/hotchocolate/security/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. Authentication is a prerequisite. You must first validate a user's identity before evaluating their permissions. [Learn how to set up authentication](https://chillicream.com/docs/hotchocolate/security/authentication) ## Setup After configuring authentication, complete these steps to enable authorization. ### 1\. Install the Authorization Package Bash ``` dotnet add package HotChocolate.AspNetCore.Authorization ``` Warning All `HotChocolate.*` packages need to have the same version. ### 2\. Register the Required Services Call `AddAuthorization()` on both `IServiceCollection` (for ASP.NET Core services) and `IRequestExecutorBuilder` (for the `@authorize` directive and middleware): C# ``` builder.Services.AddAuthorization(); builder .AddGraphQL() .AddAuthorization() .AddQueryType(); ``` ### 3\. Add Authorization Middleware C# ``` app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapGraphQL(); }); ``` ## Applying Authorization The `@authorize` directive can be applied to types and fields. When applied to a type, it applies to every field on that type. A directive on a specific field overrides the one on the type. Warning **Use `HotChocolate.Authorization.AuthorizeAttribute`**, not `Microsoft.AspNetCore.Authorization.AuthorizeAttribute`. The Microsoft attribute does not integrate with the Hot Chocolate authorization pipeline. Using the wrong attribute is a common source of authorization not working. C# ``` [Authorize] public class User { public string Name { get; set; } [Authorize(Roles = ["Administrator"])] public Address Address { get; set; } } ``` With the source generator, you can apply `[Authorize]` to resolver methods: C# ``` [QueryType] public static partial class UserQueries { [Authorize] public static async Task GetMeAsync( ClaimsPrincipal claimsPrincipal, UserService users, CancellationToken ct) { var userId = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier); return userId is not null ? await users.GetByIdAsync(userId, ct) : null; } } ``` If no arguments are specified on `[Authorize]`, the directive requires the user to be authenticated. Unauthenticated users who access an authorized field receive a GraphQL error with the code `AUTH_NOT_AUTHENTICATED`, and the field value is set to `null`. JSON ``` { "errors": [ { "message": "The current user is not authorized to access this resource.", "path": ["me"], "extensions": { "code": "AUTH_NOT_AUTHENTICATED" } } ], "data": { "me": null } } ``` ## Roles Roles provide a straightforward way to group users by access level. Add role claims to the `ClaimsPrincipal`: C# ``` claims.Add(new Claim(ClaimTypes.Role, "Administrator")); ``` Then restrict access by role: C# ``` [Authorize(Roles = ["Guest", "Administrator"])] public class User { public string Name { get; set; } [Authorize(Roles = ["Administrator"])] public Address Address { get; set; } } ``` When multiple roles are specified, a user needs to match only one of them to gain access. [Learn more about role-based authorization in ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/authorization/roles) ## Policies Policies decouple authorization logic from your GraphQL resolvers. A policy consists of an `IAuthorizationRequirement` and an `AuthorizationHandler`. Register policies on the service collection: C# ``` builder.Services.AddAuthorization(options => { options.AddPolicy("AtLeast21", policy => policy.Requirements.Add(new MinimumAgeRequirement(21))); options.AddPolicy("HasCountry", policy => policy.RequireAssertion(context => context.User.HasClaim(c => c.Type == ClaimTypes.Country))); }); builder.Services.AddSingleton(); ``` Apply policies to fields: C# ``` [Authorize(Policy = "AllEmployees")] public class User { public string Name { get; set; } [Authorize(Policy = "SalesDepartment")] public Address Address { get; set; } } ``` The `@authorize` directive is repeatable. When multiple policies are specified, the user must satisfy all of them: C# ``` [Authorize(Policy = "AtLeast21")] [Authorize(Policy = "HasCountry")] public class User { public string Name { get; set; } } ``` [Learn more about policy-based authorization in ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/authorization/policies) ### Accessing IResolverContext in an AuthorizationHandler When you need access to GraphQL-specific data in your authorization handler, use `IResolverContext` as the resource type: C# ``` public class MinimumAgeHandler : AuthorizationHandler { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, MinimumAgeRequirement requirement, IResolverContext resolverContext) { // Access GraphQL context data, arguments, etc. // Omitted for brevity } } ``` ## Allow Anonymous Access Use `[AllowAnonymous]` to bypass authorization on specific fields. This is useful for registration or public content endpoints. Warning **Use `HotChocolate.Authorization.AllowAnonymousAttribute`**, not `Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute`. C# ``` [MutationType] public static partial class AccountMutations { [Authorize] public static async Task AddAddressAsync(/* ... */) { // Requires authentication } [AllowAnonymous] public static async Task RegisterAsync(/* ... */) { // Open to everyone } } ``` `[AllowAnonymous]` removes all other authorization requirements on the field. Use it carefully to avoid exposing sensitive data. ## Global Authorization Apply authorization to the entire GraphQL endpoint by calling `RequireAuthorization()`: C# ``` app.UseEndpoints(endpoints => { endpoints.MapGraphQL().RequireAuthorization(); }); ``` This returns HTTP 401 for unauthorized requests and blocks access to all middleware including Nitro. To keep Nitro accessible while protecting the GraphQL endpoint, split the middleware: C# ``` app.UseEndpoints(endpoints => { endpoints.MapGraphQLHttp().RequireAuthorization(); endpoints.MapNitroApp(); }); ``` [Learn more about available middleware](https://chillicream.com/docs/hotchocolate/server/endpoints) ## Next Steps - **Need to set up authentication first?** See [Authentication](https://chillicream.com/docs/hotchocolate/security/authentication). - **Need to protect against expensive queries?** See [Cost Analysis](https://chillicream.com/docs/hotchocolate/security/cost-analysis). - **Need an overview of security options?** See [Security Overview](https://chillicream.com/docs/hotchocolate/security). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/security/authorization.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Cost Analysis - Hot Chocolate > Guard a public Hot Chocolate API with cost analysis: the @cost and @listSize directives assign weights so overly expensive queries are rejected before execution. Canonical source: https://chillicream.com/docs/hotchocolate/security/cost-analysis If you expose a GraphQL API to the public internet, you cannot predict what queries clients will send. A single deeply nested query requesting thousands of nodes can bring your server to its knees. Cost analysis prevents this by calculating the cost of a query before executing it and rejecting queries that exceed your budget. Hot Chocolate implements static cost analysis based on the draft [IBM Cost Analysis specification](https://ibm.github.io/graphql-specs/cost-spec.html). It assigns weights to fields and estimates list sizes, then computes two metrics: **field cost** (execution impact) and **type cost** (data impact). Queries that exceed either limit are rejected before any resolver runs. ## Why This Matters for Public APIs With REST, each endpoint has a predictable cost. You know that `GET /users` returns a page of users and takes a roughly constant amount of server time. With GraphQL, a client can construct a query that fans out across relationships: GraphQL ``` query { users(first: 50) { edges { node { orders(first: 50) { edges { node { items(first: 50) { edges { node { product { reviews(first: 50) { edges { node { author { name } } } } } } } } } } } } } } } ``` This query requests up to 50 x 50 x 50 x 50 = 6,250,000 nodes. Without cost analysis, the server would attempt to resolve all of them. Cost analysis catches this at validation time and rejects the query before it consumes resources. ## How Cost Is Calculated Hot Chocolate assigns default weights and computes two metrics: - **Field cost** represents the execution impact on the server. Async resolvers default to `10`, composite types to `1`, and scalars to `0`. - **Type cost** represents the number of objects the server instantiates. ### Field Cost Example GraphQL ``` query { book { # 10 (async resolver) title # 0 (scalar) author { # 1 (composite type) name # 0 (scalar) } } } # Field cost: 11 ``` For paginated fields, costs multiply by the page size: GraphQL ``` query { books(first: 50) { # 10 (async resolver) edges { # 1 (composite type) node { # 50 (1 x 50 items) title # 0 (scalar) author { # 50 (1 x 50 items) name # 0 (scalar) } } } } } # Field cost: 111 ``` ### Type Cost Example GraphQL ``` query { # 1 Query books(first: 50) { # 50 BooksConnections edges { # 1 BooksEdge node { # 50 Books title author { # 50 Authors name } } } } } # Type cost: 152 ``` ## Defaults for Paginated Fields Hot Chocolate automatically annotates paginated fields with cost and list size directives. For connection-based pagination: GraphQL ``` books(first: Int, after: String, last: Int, before: String): BooksConnection @listSize( assumedSize: 50 slicingArguments: ["first", "last"] sizedFields: ["edges", "nodes"] ) @cost(weight: "10") ``` The `assumedSize` defaults to the `MaxPageSize` from your pagination options. ## Applying a Cost Weight Override the default cost for a specific field: C# ``` [QueryType] public static partial class BookQueries { [Cost(100)] public static async Task GetBookAsync(int id, CatalogContext db, CancellationToken ct) => await db.Books.FindAsync([id], ct); } ``` ## Applying List Size Settings For fields that return lists, control how cost analysis estimates the list size: C# ``` [QueryType] public static partial class BookQueries { [ListSize( AssumedSize = 100, SlicingArguments = ["first", "last"], SizedFields = ["edges", "nodes"], RequireOneSlicingArgument = false)] public static IEnumerable GetBooks() => [new Book("C# in depth", new Author("Jon Skeet"))]; } ``` ## Inspecting Cost Metrics To see the cost of a query without changing enforcement, set the `GraphQL-Cost` HTTP header: | Header Value | Behavior | | ------------ | --------------------------------------------------------------- | | report | Executes the request and includes cost metrics in the response. | | validate | Returns cost metrics without executing the request. | This is invaluable when tuning your cost configuration. Send representative queries from your client applications and review their costs before deploying changes. ### Accessing Costs in Code Read cost metrics from `IResolverContext` or `IMiddlewareContext`: C# ``` public static Book GetBook(IResolverContext context) { var costMetrics = (CostMetrics)context.ContextData[WellKnownContextData.CostMetrics]!; double fieldCost = costMetrics.FieldCost; double typeCost = costMetrics.TypeCost; // Use for logging, monitoring, etc. } ``` ## Tuning Guide ### Start with Defaults The defaults (`MaxFieldCost = 1000`, `MaxTypeCost = 1000`) work for many schemas. Deploy with defaults first and observe which queries are rejected. ### Measure Real Queries Use the `GraphQL-Cost: report` header to measure the cost of your actual client queries. This gives you a baseline to tune from. ### Adjust MaxFieldCost and MaxTypeCost Increase the limits if legitimate queries are rejected. Decrease them if you want tighter protection. The right values depend on your infrastructure and acceptable load. C# ``` builder .AddGraphQL() .ModifyCostOptions(options => { options.MaxFieldCost = 5_000; options.MaxTypeCost = 5_000; }); ``` ### Assign Custom Weights to Expensive Fields If a resolver calls an external API or runs an expensive query, increase its cost weight: C# ``` [Cost(50)] public static async Task GetReportAsync(/* ... */) ``` ### Use RequirePagingBoundaries Force clients to specify `first` or `last` on paginated fields. Without this, the cost analyzer uses `MaxPageSize` as the assumed list size, which may overestimate the cost of well-behaved queries: C# ``` builder .AddGraphQL() .ModifyPagingOptions(opt => opt.RequirePagingBoundaries = true); ``` ## Real-World Example Consider a product catalog API with this schema: GraphQL ``` type Query { products(first: Int, after: String): ProductsConnection } type Product { name: String reviews(first: Int, after: String): ReviewsConnection } type Review { text: String author: User } ``` With `MaxPageSize = 50` and default costs, a query requesting `products(first: 50) { ... reviews(first: 50) { ... } }` has: - Field cost: 10 (products resolver) + 1 (edges) + 50 (node) + 500 (reviews resolver, 10 x 50) + 50 (reviews edges) + 2500 (review node, 50 x 50) + 2500 (author, 50 x 50) = \~5,611 - Type cost: 1 (Query) + 50 (Products) + 50 (ProductEdges) + 2500 (Reviews) + 2500 (ReviewEdges) + 2500 (Authors) = \~7,601 With default limits of 1,000, this query is rejected. You can either increase the limits or reduce `MaxPageSize` for the `reviews` field: C# ``` [UsePaging(MaxPageSize = 10)] public IQueryable GetReviews([Parent] Product product, CatalogContext db) => db.Reviews.Where(r => r.ProductId == product.Id); ``` Now the cost drops to a level within the default budget. ## Options Reference ### Cost Options | Option | Default | Description | | ------------------- | ------- | ---------------------------------------------------- | | MaxFieldCost | 1\_000 | Maximum allowed field cost. | | MaxTypeCost | 1\_000 | Maximum allowed type cost. | | EnforceCostLimits | true | Whether to reject queries that exceed cost limits. | | ApplyCostDefaults | true | Whether to apply default cost weights to the schema. | | DefaultResolverCost | 10.0 | Default cost for an async resolver. | C# ``` builder .AddGraphQL() .ModifyCostOptions(options => { options.MaxFieldCost = 5_000; options.MaxTypeCost = 5_000; options.EnforceCostLimits = true; options.ApplyCostDefaults = true; options.DefaultResolverCost = 10.0; }); ``` ### Filtering Cost Options | Option | Default | Description | | ----------------------------------- | ------- | ----------------------------------------------------------- | | DefaultFilterArgumentCost | 10.0 | Cost for a filter argument. | | DefaultFilterOperationCost | 10.0 | Cost for a filter operation. | | DefaultExpensiveFilterOperationCost | 20.0 | Cost for an expensive filter operation. | | VariableMultiplier | 5 | Multiplier when a variable is used for the filter argument. | C# ``` options.Filtering.DefaultFilterArgumentCost = 10.0; options.Filtering.DefaultFilterOperationCost = 10.0; ``` ### Sorting Cost Options | Option | Default | Description | | ------------------------ | ------- | --------------------------------------------------------- | | DefaultSortArgumentCost | 10.0 | Cost for a sort argument. | | DefaultSortOperationCost | 10.0 | Cost for a sort operation. | | VariableMultiplier | 5 | Multiplier when a variable is used for the sort argument. | C# ``` options.Sorting.DefaultSortArgumentCost = 10.0; options.Sorting.DefaultSortOperationCost = 10.0; ``` ## Disabling Cost Enforcement If you protect your API through other means (such as trusted documents), you can disable cost enforcement. The analyzer still computes costs for reporting, but does not reject queries: C# ``` builder .AddGraphQL() .ModifyCostOptions(o => o.EnforceCostLimits = false); ``` ## Next Steps - **Need to restrict access to fields?** See [Authorization](https://chillicream.com/docs/hotchocolate/security/authorization). - **Building a private API?** See [Trusted Documents](https://chillicream.com/docs/hotchocolate/performance/trusted-documents). - **Need to limit query depth?** See [Request Limits](https://chillicream.com/docs/hotchocolate/security/request-limits). - **Need an overview of security options?** See [Security Overview](https://chillicream.com/docs/hotchocolate/security). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/security/cost-analysis.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # GraphQL Introspection in Hot Chocolate > Control GraphQL introspection in Hot Chocolate: disable __schema and __type fields in production and selectively allow requests with AllowIntrospection. Canonical source: https://chillicream.com/docs/hotchocolate/security/introspection Introspection is what enables GraphQL's rich tooling ecosystem and powerful IDEs like [Nitro](https://chillicream.com/products/nitro) or GraphiQL. Every GraphQL server exposes a `__schema` and `__type` field on the query type as well as a `__typename` field on each type. These fields provide insights into the schema of your GraphQL server. Using the `__schema` field, you could list the names of all types your GraphQL server contains: GraphQL ``` { __schema { types { name } } } ``` You could also request the fields plus their arguments of a specific type using the `__type` field: GraphQL ``` { __type(name: "Book") { fields { name args { name type { name } } } } } ``` The `__typename` field is the introspection feature you will use the most in day-to-day development. When working with [unions](https://chillicream.com/docs/hotchocolate/defining-a-schema/unions), for example, it tells you the name of the type being returned, letting you handle the result accordingly. GraphQL ``` { posts { __typename ... on VideoPost { videoUrl } ... on TextPost { text } } } ``` While these fields can be useful to you directly, they are mainly intended for developer tooling. You are unlikely to write your own introspection queries on a daily basis. [Learn more about introspection](https://graphql.org/learn/introspection) ## Disabling Introspection While introspection is a powerful feature that can improve your development workflow, it can also be used as an attack vector. Recursive introspection queries (e.g., deeply nested `__Type.ofType` or `__Type.fields` chains) can consume significant server resources. Note that disabling introspection is not about hiding your schema. When using `MapGraphQL`, the schema is available as a static file at `/graphql/schema.graphql`. This file is computed once and has no performance impact. The purpose of disabling introspection is to prevent expensive recursive queries against production systems. Disable introspection by calling `AllowIntrospection()` with a `false` argument on the `IRequestExecutorBuilder`: C# ``` builder .AddGraphQL() .AllowIntrospection(false); ``` While clients can still issue introspection queries, Hot Chocolate returns an error response. You most likely do not want to disable introspection while developing, so you can toggle it based on the current hosting environment: C# ``` builder .AddGraphQL() .AllowIntrospection(builder.Environment.IsDevelopment()); ``` ### Allowlisting Requests You can allow introspection on a per-request basis while keeping it disabled for the majority of requests. Create a request interceptor and determine based on the request (the `HttpContext`) whether to allow introspection. C# ``` public class IntrospectionInterceptor : DefaultHttpRequestInterceptor { public override ValueTask OnCreateAsync(HttpContext context, IRequestExecutor requestExecutor, OperationRequestBuilder requestBuilder, CancellationToken cancellationToken) { if (context.Request.Headers.ContainsKey("X-Allow-Introspection")) { requestBuilder.AllowIntrospection(); } return base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken); } } ``` C# ``` builder .AddGraphQL() // Disable introspection by default .AllowIntrospection(false) .AddHttpRequestInterceptor(); ``` [Learn more about interceptors](https://chillicream.com/docs/hotchocolate/server/interceptors) ### Custom Error Message If a client tries to execute an introspection query when introspection is not allowed, they receive an error message similar to the following: JSON ``` { "errors": [ { "message": "Introspection is not allowed for the current request.", "locations": [ { "line": 2, "column": 3 } ], "extensions": { "field": "__schema", "code": "HC0046" } } ] } ``` If you need to customize the error message, do so in your request interceptor: C# ``` public class IntrospectionInterceptor : DefaultHttpRequestInterceptor { public override ValueTask OnCreateAsync(HttpContext context, IRequestExecutor requestExecutor, OperationRequestBuilder requestBuilder, CancellationToken cancellationToken) { if (context.Request.Headers.ContainsKey("X-Allow-Introspection")) { requestBuilder.AllowIntrospection(); } else { // the header is not present, introspection continues // to be disallowed requestBuilder.SetIntrospectionNotAllowedMessage( "Missing `X-Allow-Introspection` header"); } return base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken); } } ``` ## Next Steps - [Interceptors](https://chillicream.com/docs/hotchocolate/server/interceptors) for per-request customization. - [Security](https://chillicream.com/docs/hotchocolate/security) for a broader look at securing your GraphQL server. - [Endpoints](https://chillicream.com/docs/hotchocolate/server/endpoints) for configuring the Nitro IDE. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/security/introspection.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Request Limits - Hot Chocolate > Bound resource usage in Hot Chocolate with request limits such as MaxExecutionDepth, parser limits, and execution timeouts that block adversarial queries. Canonical source: https://chillicream.com/docs/hotchocolate/security/request-limits Unlike REST, where each endpoint has a predictable cost, a single GraphQL request can trigger unbounded work through deep nesting, alias amplification, or fragment expansion. Hot Chocolate enforces limits at every stage of request processing (parsing, validation, and execution) to keep resource consumption bounded even under adversarial workloads. ## Parser Limits Parser limits stop malicious payloads before the AST is fully constructed. Because parsing happens before validation, these limits are your first line of defense. Configure parser limits with `ModifyParserOptions`: C# ``` builder .AddGraphQL() .ModifyParserOptions(o => { o.MaxAllowedFields = 1024; o.MaxAllowedDirectives = 4; o.MaxAllowedRecursionDepth = 100; }); ``` | Option | Default | Description | | ------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------ | | MaxAllowedFields | 2048 | Maximum number of fields allowed in a query document. | | MaxAllowedDirectives | 4 per location | Maximum number of directives allowed on a single location (field, operation, or fragment definition). | | MaxAllowedRecursionDepth | 200 | Maximum nesting depth the parser allows for selection sets, list values, object values, and type references. | | MaxAllowedNodes | Unlimited | Maximum number of AST nodes the parser produces from a document. Defaults to unlimited. | | MaxAllowedTokens | Unlimited | Maximum number of tokens the lexer processes. Defaults to unlimited. | ## Validation Limits After parsing, the validation layer checks the document against your schema. Some validation rules can become expensive on adversarial inputs. ### Execution Depth Limits how deeply nested a query can be: C# ``` builder .AddGraphQL() .AddMaxExecutionDepthRule(10); ``` This prevents queries that traverse deep relationship chains (e.g., `user.friends.friends.friends...`). Unlike the parser recursion depth (which prevents stack overflows), execution depth measures the logical nesting of field selections against your schema. You can skip introspection fields from the depth count and allow per-request overrides: C# ``` builder .AddGraphQL() .AddMaxExecutionDepthRule( maxAllowedExecutionDepth: 10, skipIntrospectionFields: true, allowRequestOverrides: true); ``` ### Fragment Visits Each time a visitor enters a fragment spread counts as one visit. Queries with deeply nested or repeated fragment spreads can cause exponential visitor work. Hot Chocolate caps the total number of fragment visits per operation at **1,000** by default: C# ``` builder .AddGraphQL() .ModifyValidationOptions(o => { o.MaxAllowedFragmentVisits = 1_000; }); ``` ### Field Merge Comparisons The "overlapping fields can be merged" validation rule checks that fields with the same response name have compatible types and arguments. On adversarial inputs with deeply nested fragments, this check can become expensive. Hot Chocolate caps the comparison budget at **100,000** by default. Queries exceeding this budget are rejected: C# ``` builder .AddGraphQL() .SetMaxAllowedFieldMergeComparisons(50_000); ``` ### Field Coordinate Cycles Some schemas contain self-referential relationships. For example, a `User` type with a `friends` field that returns `[User]`. Without a limit, a client can nest this relationship arbitrarily deep, causing resolver fan-out that grows exponentially with each level. The field cycle depth rule tracks how many times each schema coordinate (e.g., `User.friends`) appears on the query path: C# ``` builder .AddGraphQL() .AddMaxAllowedFieldCycleDepthRule(defaultCycleLimit: 3); ``` With a limit of 3, the following query is valid: GraphQL ``` { user { friends { # User.friends — cycle 1 friends { # User.friends — cycle 2 friends { # User.friends — cycle 3 name } } } } } ``` Adding a fourth level of `friends` would be rejected. You can override the limit for specific coordinates if some relationships are safe to traverse more deeply than others: C# ``` builder .AddGraphQL() .AddMaxAllowedFieldCycleDepthRule( defaultCycleLimit: 3, coordinateCycleLimits: [ (new SchemaCoordinate("Category", "parent"), 10), ]); ``` This rule is enabled by default in non-development environments as part of the default security policy. You can remove it with `RemoveMaxAllowedFieldCycleDepthRule()` if your schema does not contain self-referential relationships. ### Validation Errors Documents designed to generate excessive validation errors can consume memory accumulating error objects. The default limit is **5**. When the limit is reached, validation stops early instead of continuing to accumulate errors. C# ``` builder .AddGraphQL() .SetMaxAllowedValidationErrors(5); ``` ### Introspection Depth Introspection queries with recursive fields like `__Type.ofType` or `__Type.fields` can be used to construct expensive queries that consume significant server resources. The concern is not schema discovery (the schema is available at `/graphql/schema.graphql` by default when using `MapGraphQL`, computed once with no performance impact), but resource consumption from deeply recursive introspection operations. Recursive introspection queries are limited by default: - **`ofType` chain:** 16 levels - **List fields** (`fields`, `inputFields`, `interfaces`, `possibleTypes`): 1 level of recursion C# ``` builder .AddGraphQL() .SetIntrospectionAllowedDepth( maxAllowedOfTypeDepth: 8, maxAllowedListRecursiveDepth: 1); ``` ## Execution Limits ### Timeout Requests are aborted after 30 seconds by default. The timeout is not enforced when a debugger is attached. C# ``` builder .AddGraphQL() .ModifyRequestOptions(o => { o.ExecutionTimeout = TimeSpan.FromSeconds(10); }); ``` ### Nodes Batch Size The `nodes(ids: [ID!]!)` field allows fetching multiple entities at once. The default batch limit is **50**: C# ``` builder .AddGraphQL() .AddGlobalObjectIdentification(o => o.MaxAllowedNodeBatchSize = 25); ``` ## Next Steps - **Need cost analysis?** See [Cost Analysis](https://chillicream.com/docs/hotchocolate/security/cost-analysis). - **Need trusted documents?** See [Trusted Documents](https://chillicream.com/docs/hotchocolate/performance/trusted-documents). - **Need to control introspection?** See [Introspection](https://chillicream.com/docs/hotchocolate/security/introspection). - **Back to overview?** See [Securing Your API](https://chillicream.com/docs/hotchocolate/security). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/security/request-limits.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Server - Hot Chocolate > Overview of configuring and operating a Hot Chocolate GraphQL server: endpoints, HTTP transport, interceptors, dependency injection, and instrumentation. Canonical source: https://chillicream.com/docs/hotchocolate/server This section covers how you configure and operate a Hot Chocolate GraphQL server. You will find details on transport protocols, middleware, dependency injection, and runtime behavior. ## Endpoints Hot Chocolate provides ASP.NET Core endpoint middleware for accepting HTTP and WebSocket GraphQL requests, downloading the schema, and serving the [Nitro](https://chillicream.com/products/nitro) GraphQL IDE. [Learn more about endpoints](https://chillicream.com/docs/hotchocolate/server/endpoints) ## HTTP Transport Hot Chocolate implements the GraphQL over HTTP specification. The default incremental delivery format is v0.2, and clients can select the format through the `Accept` header. [Learn more about the HTTP transport](https://chillicream.com/docs/hotchocolate/server/http-transport) ## Cache Control Cache control lets your GraphQL server emit `Cache-Control` and `Vary` response headers that CDNs, reverse proxies, and browsers can use for HTTP caching decisions. [Learn more about cache control](https://chillicream.com/docs/hotchocolate/server/cache-control) ## Interceptors Interceptors let you intercept GraphQL requests before execution. There are interceptors for both HTTP requests and WebSocket sessions. For WebSockets, the interceptor also handles lifecycle events such as when a client first connects. [Learn more about interceptors](https://chillicream.com/docs/hotchocolate/server/interceptors) ## Dependency Injection Hot Chocolate recognizes services registered in your DI container and injects them into resolvers automatically. Services are resolved implicitly without requiring the `[Service]` attribute. [Learn more about dependency injection](https://chillicream.com/docs/hotchocolate/resolvers/dependency-injection) ## Warmup Hot Chocolate constructs the schema eagerly at startup. You can go further by registering warmup tasks that pre-populate caches before the server begins accepting traffic. [Learn more about warmup](https://chillicream.com/docs/hotchocolate/server/warmup) ## Global State Global State lets you define properties on a per-request basis and makes them available to all resolvers and middleware. [Learn more about global state](https://chillicream.com/docs/hotchocolate/server/global-state) ## Introspection Introspection lets you query the type system of your GraphQL server using regular GraphQL queries. While this powers developer tooling, it can also be an attack vector. You can control who is allowed to issue introspection queries. [Learn more about introspection](https://chillicream.com/docs/hotchocolate/security/introspection) ## Files Hot Chocolate provides file upload support through the `Upload` scalar, even though file handling is not traditionally a GraphQL server concern. You can also return presigned URLs for a hybrid approach. [Learn more about handling files](https://chillicream.com/docs/hotchocolate/server/files) ## Instrumentation Hot Chocolate exposes diagnostic events across the server, execution engine, and DataLoader layers. The built-in OpenTelemetry integration aligns with the proposed GraphQL semantic conventions. [Learn more about instrumentation](https://chillicream.com/docs/hotchocolate/server/instrumentation) ## Batching Batching lets you send and execute multiple GraphQL operations in a single request. Batching is disabled by default and you enable it through the `AllowedBatching` flags enum. [Learn more about batching](https://chillicream.com/docs/hotchocolate/server/batching) ## Command Line The command-line interface lets you export your GraphQL schema from the terminal, which is useful for CI/CD pipelines. [Learn more about the command line](https://chillicream.com/docs/hotchocolate/server/command-line) [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/server/index.md) Maintained by ChilliCream. Last updated on **July 01, 2026** by **Tobias Tengler** --- # Batching - Hot Chocolate > Execute multiple GraphQL operations in a single HTTP request with Hot Chocolate batching: variable batching, request batching, and streamed batch results. Canonical source: https://chillicream.com/docs/hotchocolate/server/batching Batching lets you send and execute multiple GraphQL operations in a single HTTP request. Hot Chocolate supports two forms of batching: **variable batching** and **request batching**. Both deliver results as a stream, so the client receives each result as soon as it is ready without waiting for the entire batch to complete. Variable batching is based on an [open proposal](https://github.com/graphql/graphql-over-http/pull/307) to the [GraphQL over HTTP specification](https://github.com/graphql/graphql-over-http). ## Enabling Batching Batching is disabled by default as a security measure. You enable the types of batching you want to allow through the `AllowedBatching` flags enum using `ModifyServerOptions`: C# ``` builder .AddGraphQL() .ModifyServerOptions(o => o.Batching = AllowedBatching.VariableBatching); ``` You can combine flags to enable multiple batching modes: C# ``` builder .AddGraphQL() .ModifyServerOptions(o => o.Batching = AllowedBatching.VariableBatching | AllowedBatching.RequestBatching); ``` To enable all batching modes at once: C# ``` builder .AddGraphQL() .ModifyServerOptions(o => o.Batching = AllowedBatching.All); ``` Note If your GraphQL server is a Fusion subgraph, both variable batching and request batching are enabled by default. You do not need to configure this explicitly. ### Batch Size Limits The maximum number of operations in a single batch defaults to **1024**. You can adjust this limit: C# ``` builder .AddGraphQL() .ModifyServerOptions(o => o.MaxBatchSize = 2048); ``` A value of `0` means unlimited. ## Variable Batching Variable batching lets you execute a **single operation multiple times** with different sets of variables. Instead of sending `variables` as an object, you send it as an array of objects: JSON ``` { "query": "query getHero { hero { name } }", "operationName": "getHero", "id": "W5vrrAIypCbniaIYeroNnw==", "variables": [ { "a": 1, "b": "abc" }, { "a": 2, "b": "def" } ], "extensions": { "a": 1, "b": "abc" } } ``` The operation executes once per variable set. Each result in the response stream includes a `variableIndex` (0-based) so the client can match results back to their corresponding variable set: ``` { "data": { "hero": { "name": "R2-D2" } }, "variableIndex": 0 } { "data": { "hero": { "name": "Luke Skywalker" } }, "variableIndex": 1 } ``` Results are delivered out of order. Whichever variable set finishes first is streamed to the client first. The `variableIndex` field is how the client correlates each result back to its input. ## Request Batching Request batching lets you send a JSON array of independent GraphQL operations in a single HTTP request. Each entry in the array is a complete operation with its own query, variables, and operation name. Individual entries in the array can also use variable batching by providing `variables` as an array: JSON ``` [ { "query": "query getHero { hero { name } }", "operationName": "getHero", "id": "W5vrrAIypCbniaIYeroNnw==", "variables": { "a": 1, "b": "abc" }, "extensions": { "a": 1, "b": "abc" } }, { "query": "query getHero { hero { name } }", "operationName": "getHero", "id": "W5vrrAIypCbniaIYeroNnw==", "variables": [ { "a": 1, "b": "abc" }, { "a": 2, "b": "def" } ], "extensions": { "a": 1, "b": "abc" } } ] ``` Each result includes a `requestIndex` (0-based) that identifies which entry in the request array it belongs to. When an entry uses variable batching, its results also include a `variableIndex`: ``` { "data": { "hero": { "name": "R2-D2" } }, "requestIndex": 1, "variableIndex": 0 } { "data": { "hero": { "name": "Han Solo" } }, "requestIndex": 0 } { "data": { "hero": { "name": "Luke Skywalker" } }, "requestIndex": 1, "variableIndex": 1 } ``` Like variable batching, results are delivered out of order. In the example above, the second request (index 1) returned its first variable result before the first request (index 0) completed. The `requestIndex` and `variableIndex` fields let the client reassemble the results correctly. ## Response Formats Batch results are delivered as a result stream. Hot Chocolate streams result data back to your client as soon as each item in the batch has been executed. The response transport is selected via the `Accept` header: | Accept header | Transport | Content-Type | | ----------------- | ---------- | ----------------- | | multipart/mixed | Multipart | multipart/mixed | | text/event-stream | SSE | text/event-stream | | application/jsonl | JSON Lines | application/jsonl | If no streaming `Accept` header is provided, the default is `multipart/mixed`. **JSON Lines** (`application/jsonl`) is well-suited for batch responses. Each result is written as a single line of JSON, making it straightforward for clients to parse results incrementally: ``` {"requestIndex":0,"data":{"hero":{"name":"R2-D2"}}} {"requestIndex":1,"data":{"hero":{"name":"Luke Skywalker"}}} ``` If you are using a JavaScript client, consider: - [meros](https://github.com/maraisr/meros) for handling `multipart/mixed` responses - [graphql-sse](https://github.com/enisdenjo/graphql-sse) for handling `text/event-stream` responses For more details about streaming transports, see [HTTP Transport](https://chillicream.com/docs/hotchocolate/server/http-transport#streaming-transports). ## Next Steps - [HTTP Transport](https://chillicream.com/docs/hotchocolate/server/http-transport) for details on streaming transports and incremental delivery. - [Persisted Operations](https://chillicream.com/docs/hotchocolate/performance/trusted-documents) for reducing request payload size. - [Migrate from v15 to v16](https://chillicream.com/docs/hotchocolate/migrating/migrate-from-15-to-16#batching-is-now-disabled-by-default) for the batching migration details. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/server/batching.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Cache Control - Hot Chocolate > Understand HTTP Cache-Control and Vary headers, then learn how Hot Chocolate uses GraphQL @cacheControl directives to generate safe CDN and browser caching policies. Canonical source: https://chillicream.com/docs/hotchocolate/server/cache-control Cache-Control is the HTTP header field that tells browsers, reverse proxies, and CDNs how they are allowed to store and reuse a response instead of sending the same request back to the server every time. Together with related headers such as `Vary`, it makes cached responses safe and predictable by defining whether a response may be reused, how long it may be reused, and which parts of the request affect that decision. ## Why Cache Control Matters Good cache rules help you: - Reduce latency for users. - Reduce load on your backend services. - Keep cache behavior predictable. - Protect user-specific data from shared caches. To make caching work safely and predictably, you need two things: 1. A deterministic GET route so caches get a stable cache key. 2. Correct Cache-Control (and Vary) headers so caches know where and how long a response may be reused. ## Why GraphQL Needs Extra Care GraphQL usually exposes one endpoint, but each request can ask for different fields. Two requests to the same URL can therefore return very different response shapes and different data sensitivity. A single GraphQL response can also mix public data and user-specific data. Since HTTP cache headers apply to the full response, the server has to compute one safe final policy that represents everything selected in that operation. That means one response can include: - Public data, which is safe for shared caches. - User-specific data, which is not safe for shared caches. The GraphQL operation type matters as well. Query operations are the primary target for HTTP and CDN caching. Mutations change data and should not be cached as shared HTTP responses. Subscriptions are long-running streams and are not HTTP-cacheable in the same way. ## Deterministic GET Routes The GraphQL over HTTP specification allows query operations to be sent over HTTP GET. HTTP ``` GET /graphql?query=query GetProducts{products{nodes{name}}} ``` The same applies when variables are included. HTTP ``` GET /graphql?query=query GetProducts($first:Int!){products(first:$first){nodes{name}}}&variables={"first":5} ``` In real requests, these values are URL-encoded, and for larger operations the query string quickly becomes difficult to work with. This is where persisted operations help. ### Persisted Operation Routes In large first-party GraphQL APIs, a common approach used by companies such as Netflix, Meta, and X is to rely on trusted documents. Client operations are stored in an operation store, and clients send a stable operation identifier instead of the full query text. > You can read more in the [First-Party API guide](https://chillicream.com/docs/fusion/guides/first-party-api). With trusted documents in place, persisted-operation routes become short and stable. HTTP ``` GET /graphql/persisted/GetProducts/123456789 ``` Variables can then be sent as query parameters. HTTP ``` GET /graphql/persisted/GetProducts/123456789?variables={"first":5} ``` In order to use persisted-operation routes you need to add the middleware `MapGraphQLPersistedOperations`. C# ``` var app = builder.Build(); app.MapGraphQLPersistedOperations(); ``` ## `@cacheControl` in GraphQL A deterministic route alone is not enough. The gateway also needs policy metadata to decide whether a response is public or private, and how long it may be reused. GraphQL provides the `@cacheControl` directive for this purpose. You can place it on fields and types to describe cache intent. GraphQL ``` type Query { productById(id: ID!): Product @cacheControl(maxAge: 300, sharedMaxAge: 900) me: UserProfile @cacheControl(maxAge: 60, scope: PRIVATE, vary: ["Authorization"]) } ``` In Hot Chocolate you can express `@cacheControl` directive with the `[CacheControl]` attribute. C# ``` using HotChocolate.Caching; [QueryType] public static class Query { [CacheControl(300, SharedMaxAge = 900)] public static Product? GetProductById(int id) => ProductRepository.GetById(id); [CacheControl(60, Scope = CacheControlScope.Private, Vary = ["Authorization"])] public static UserProfile GetMe() => UserProfileRepository.GetCurrent(); } ``` ## How Hot Chocolate Assembles the Final Headers Hot Chocolate computes one effective response policy by traversing the selected query fields. It reads `@cacheControl` metadata on each field, falls back to the field return type when values are missing, and continues recursively through child selections. All collected constraints are merged into one final policy. The merge is conservative: `max-age` and `s-maxage` take the lowest value, scope resolves to the strictest value (`private` over `public`), and `vary` values are merged, normalized, and deduplicated. Hot Chocolate computes cache constraints only for query operations. Introspection requests and operations for which no selected field contributes `maxAge` or `sharedMaxAge` do not produce a cache policy. `UseQueryCache()` writes the final headers only when the executed result has no GraphQL errors and the request has not opted out of cache-control header generation. ## Enable Cache Control in Hot Chocolate Install the package first: Bash ``` dotnet add package HotChocolate.Caching ``` Then configure the server to register the directive and write the final headers: C# ``` using HotChocolate.Caching; builder .AddGraphQL() .AddQueryType() .UseQueryCache() .AddCacheControl() .ModifyCacheControlOptions(o => { o.ApplyDefaults = false; }); ``` `AddCacheControl()` registers the `@cacheControl` directive and the schema and execution components needed to compute cache constraints. `UseQueryCache()` writes the final `Cache-Control` and `Vary` values so the HTTP response formatter can emit them as HTTP headers. ## Cache-Control Options `ModifyCacheControlOptions` configures default behavior: C# ``` using HotChocolate.Caching; builder .AddGraphQL() .AddQueryType() .UseQueryCache() .AddCacheControl() .ModifyCacheControlOptions(o => { o.Enable = true; o.DefaultMaxAge = 60; o.DefaultScope = CacheControlScope.Public; o.ApplyDefaults = true; }); ``` | Option | Type | Default | Description | | ------------- | ----------------- | ------- | ------------------------------------------------------------------- | | Enable | bool | true | Enables or disables cache-control header generation. | | DefaultMaxAge | int | 0 | Default max-age when ApplyDefaults is enabled. | | DefaultScope | CacheControlScope | Public | Default cache scope when ApplyDefaults is enabled. | | ApplyDefaults | bool | true | Applies defaults to eligible fields without explicit @cacheControl. | With the default settings, eligible query fields that do not declare explicit cache metadata still contribute `max-age=0`. If you want headers only when fields opt in explicitly, set `ApplyDefaults = false`. ## How HotChocolate Assembles the Final Headers Hot Chocolate computes one effective response policy by traversing the planned operation tree. It starts at the selected root fields, reads `@cacheControl` metadata on each field, falls back to the field return type when values are missing, and continues recursively through child selections, including interfaces and unions. All collected constraints are merged into one final policy. The merge is conservative: `max-age` and `s-maxage` take the lowest value, scope resolves to the strictest value (`private` over `public`), and `vary` values are merged, normalized, and deduplicated. Hot Chocolate computes these cache constraints for query operations. Mutation requests, Subscription request, introspection requests, and operations with no cache constraints do not get cache-control headers. ## Skip Cache Control for a Specific Request Use `SkipQueryCaching()` on `OperationRequestBuilder` to bypass cache-control for a specific request. C# ``` using HotChocolate.AspNetCore; using HotChocolate.Execution; public sealed class NoCacheHeaderInterceptor : DefaultHttpRequestInterceptor { public override ValueTask OnCreateAsync( HttpContext context, IRequestExecutor requestExecutor, OperationRequestBuilder requestBuilder, CancellationToken cancellationToken) { if (context.Request.Headers.ContainsKey("X-Skip-Cache-Control")) { requestBuilder.SkipQueryCaching(); } return base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken); } } ``` Register the interceptor: C# ``` builder .AddGraphQL() .AddHttpRequestInterceptor(); ``` Note You can only register a single HttpRequestInterceptor per schema. ## Putting It Together In day-to-day terms, the flow is simple: - Subgraphs declare cache intent. - Fusion composes that metadata. - The gateway calculates one safe policy for each query result. - HTTP caches enforce the resulting headers. ## Next Steps - [HTTP Transport](https://chillicream.com/docs/hotchocolate/server/http-transport) for request and response behavior. - [Interceptors](https://chillicream.com/docs/hotchocolate/server/interceptors) for request-level control. - [Configuration Options](https://chillicream.com/docs/hotchocolate/server/options) for the full options reference. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/server/cache-control.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Command Line - Hot Chocolate > Manage GraphQL schemas from the CLI with the HotChocolate.AspNetCore.CommandLine package: run schema export for CI/CD pipelines and schema registry workflows. Canonical source: https://chillicream.com/docs/hotchocolate/server/command-line The `HotChocolate.AspNetCore.CommandLine` package extends the `IHostBuilder` interface with a command-line interface for managing GraphQL schemas. This extension lets you export schemas directly from the command line, which is useful for CI/CD pipelines and schema registry workflows. ## Setup Here is an example of using the `HotChocolate.AspNetCore.CommandLine` package with a minimal API: C# ``` var builder = WebApplication.CreateBuilder(args); builder.AddGraphQL().AddQueryType(); var app = builder.Build(); app.MapGraphQL(); return await app.RunWithGraphQLCommandsAsync(args); ``` `RunWithGraphQLCommandsAsync` returns a `Task` (and the synchronous `RunWithGraphQLCommands` returns `int`). Return this exit code from your `Program.cs` so that command failures signal an error to shell scripts, CI/CD pipelines, and other tools. ## Commands ### Schema Export Command The `schema export` command exports the GraphQL schema. By default, the schema is printed to the console. You can specify an output file using the `--output` option. ``` dotnet run -- schema export --output schema.graphql ``` **Options** - `--output`: The path to the file where the schema is exported. If no output path is specified, the schema prints to the console. - `--schema-name`: The name of the schema to export. If no schema name is specified, the default schema is exported. - `--semantic-non-null`: Rewrites the exported schema to strip non-null wrappers from output fields and apply the `@semanticNonNull` directive instead. Useful for clients that still rely on `@semanticNonNull` annotations. ## Next Steps - [Warmup](https://chillicream.com/docs/hotchocolate/server/warmup) for details on startup behavior and schema initialization. - [Migrate from v15 to v16](https://chillicream.com/docs/hotchocolate/migrating/migrate-from-15-to-16#noteworthy-changes) for the full list of changes to `RunWithGraphQLCommandsAsync`. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/server/command-line.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Endpoints - Hot Chocolate > Map Hot Chocolate endpoints in ASP.NET Core: MapGraphQL for HTTP and WebSockets, MapGraphQLSchema for SDL downloads, and MapNitroApp for the Nitro GraphQL IDE. Canonical source: https://chillicream.com/docs/hotchocolate/server/endpoints Hot Chocolate provides a set of ASP.NET Core middleware for making the GraphQL server available via HTTP and WebSockets. There are also middleware for hosting the [Nitro](https://chillicream.com/products/nitro) GraphQL IDE and an endpoint for downloading the schema in its SDL representation. ## MapGraphQL Call `MapGraphQL()` on the `IEndpointRouteBuilder` to register all of the middleware a standard GraphQL server requires. C# ``` app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGraphQL(); }); ``` With .NET 6+ Minimal APIs, you can call `MapGraphQL()` on the `app` builder directly since it implements `IEndpointRouteBuilder`: C# ``` var builder = WebApplication.CreateBuilder(args); // Omitted code for brevity var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` The middleware registered by `MapGraphQL` makes the GraphQL server available at `/graphql` by default. You can customize the endpoint: C# ``` endpoints.MapGraphQL("/my/graphql/endpoint"); ``` Calling `MapGraphQL()` enables the following functionality on the specified endpoint: - HTTP GET and HTTP POST GraphQL requests are handled (Multipart included) - WebSocket GraphQL requests are handled (if the ASP.NET Core WebSocket middleware has been registered) - Including the query string `?sdl` after the endpoint downloads the GraphQL schema - Accessing the endpoint from a browser loads the [Nitro](https://chillicream.com/products/nitro) GraphQL IDE You can customize the combined middleware using `GraphQLServerOptions` as shown below, or include only the parts of the middleware you need and configure them individually. The following middleware are available: - [MapNitroApp](#mapnitroapp) - [MapGraphQLHttp](#mapgraphqlhttp) - [MapGraphQLWebsocket](#mapgraphqlwebsocket) - [MapGraphQLSchema](#mapgraphqlschema) - [MapGraphQLPersistedOperations](#mapgraphqlpersistedoperations) ### GraphQLServerOptions You can influence the behavior of the middleware registered by `MapGraphQL` using `GraphQLServerOptions`. #### EnableSchemaRequests C# ``` endpoints.MapGraphQL().WithOptions(o => o.EnableSchemaRequests = false); ``` This setting controls whether the schema of the GraphQL server can be downloaded by appending `?sdl` to the endpoint. #### EnableGetRequests C# ``` endpoints.MapGraphQL().WithOptions(o => o.EnableGetRequests = false); ``` This setting controls whether the GraphQL server handles GraphQL operations sent via the query string in an HTTP GET request. #### AllowedGetOperations C# ``` endpoints.MapGraphQL().WithOptions(o => o.AllowedGetOperations = AllowedGetOperations.Query); ``` If [EnableGetRequests](#enablegetrequests) is `true`, you can control the allowed operations for HTTP GET requests using the `AllowedGetOperations` setting. By default, only queries are accepted via HTTP GET. You can also allow mutations by setting `AllowedGetOperations` to `AllowedGetOperations.QueryAndMutation`. #### EnableMultipartRequests C# ``` endpoints.MapGraphQL().WithOptions(o => o.EnableMultipartRequests = false); ``` This setting controls whether the GraphQL server handles HTTP multipart forms (file uploads). [Learn more about uploading files](https://chillicream.com/docs/hotchocolate/server/files#upload-scalar) #### Tool You can specify options for Nitro using the `Tool` property. For example, you could enable Nitro only during development: C# ``` app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGraphQL().WithOptions(o => o.Tool.Enable = env.IsDevelopment()); }); ``` [Learn more about possible NitroAppOptions](#nitroappoptions) ## MapNitroApp Call `MapNitroApp()` on the `IEndpointRouteBuilder` to serve [Nitro](https://chillicream.com/products/nitro) on a different endpoint than the actual GraphQL endpoint. C# ``` app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapNitroApp("/graphql/ui"); }); ``` This makes Nitro accessible via a web browser at the `/graphql/ui` endpoint. ### NitroAppOptions You can configure Nitro using `NitroAppOptions`. #### Enable C# ``` endpoints.MapNitroApp("/ui").WithOptions(o => o.Enable = false); ``` This setting controls whether Nitro is served. #### GraphQLEndpoint C# ``` endpoints.MapNitroApp("/ui").WithOptions(o => o.GraphQLEndpoint = "/my/graphql/endpoint"); ``` This setting sets the GraphQL endpoint to use when creating new documents within Nitro. #### UseBrowserUrlAsGraphQLEndpoint C# ``` endpoints.MapNitroApp("/ui").WithOptions(o => o.UseBrowserUrlAsGraphQLEndpoint = true); ``` If set to `true`, the current browser URL is treated as the GraphQL endpoint when creating new documents within Nitro. Warning [GraphQLEndpoint](#graphqlendpoint) takes precedence over this setting. #### Document C# ``` endpoints.MapNitroApp("/ui").WithOptions(o => o.Document = "{ __typename }"); ``` This setting lets you set a default GraphQL document that serves as a placeholder for each new document created using Nitro. #### UseGet C# ``` endpoints.MapNitroApp("/ui").WithOptions(o => o.UseGet = true); ``` This setting controls the default HTTP method used to execute GraphQL operations when creating new documents within Nitro. When set to `true`, HTTP GET is used instead of the default HTTP POST. #### HttpHeaders C# ``` endpoints.MapNitroApp("/ui").WithOptions(o => { o.HttpHeaders = new HeaderDictionary { { "Content-Type", "application/json" } }; }); ``` This setting lets you specify default HTTP headers that are added to each new document created using Nitro. #### IncludeCookies C# ``` endpoints.MapNitroApp("/ui").WithOptions(o => o.IncludeCookies = true); ``` This setting specifies the default for including cookies in cross-origin requests when creating new documents within Nitro. #### Title C# ``` endpoints.MapNitroApp("/ui").WithOptions(o => o.Title = "My GraphQL explorer"); ``` This setting controls the tab name when Nitro is opened inside a web browser. #### DisableTelemetry C# ``` endpoints.MapNitroApp("/ui").WithOptions(o => o.DisableTelemetry = true); ``` This setting lets you disable telemetry events. #### GaTrackingId C# ``` endpoints.MapNitroApp("/ui").WithOptions(o => o.GaTrackingId = "google-analytics-id"); ``` This setting lets you set a custom Google Analytics ID, which allows you to gain insights into the usage of Nitro hosted as part of your GraphQL server. The following information is collected: | Name | Description | | ------------------ | ------------------------------------------------------------- | | deviceId | Random string generated on a per-device basis | | operatingSystem | Name of the operating system: Windows, macOS, Linux & Unknown | | userAgent | User-Agent header | | applicationType | The type of application: app (Electron) or middleware | | applicationVersion | Version of Nitro | ## MapGraphQLHttp Call `MapGraphQLHttp()` on the `IEndpointRouteBuilder` to make your GraphQL server available via HTTP at a specific endpoint. C# ``` app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGraphQLHttp("/graphql/http"); }); ``` With the above configuration, you can issue HTTP GET/POST requests against the `/graphql/http` endpoint. ### GraphQLServerOptions The HTTP endpoint can also be configured with per-endpoint overrides using `WithOptions`: C# ``` endpoints.MapGraphQLHttp("/graphql/http").WithOptions(o => o.EnableGetRequests = false); ``` The same `GraphQLServerOptions` properties available on `MapGraphQL` can be overridden here, except for `Tool` and `EnableSchemaRequests` which are not applicable to standalone HTTP endpoints. [Learn more about GraphQLServerOptions](#graphqlserveroptions) ## MapGraphQLWebsocket Call `MapGraphQLWebSocket()` on the `IEndpointRouteBuilder` to make your GraphQL server available via WebSockets at a specific endpoint. C# ``` app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGraphQLWebSocket("/graphql/ws"); }); ``` With the above configuration, you can issue GraphQL subscription requests via WebSocket against the `/graphql/ws` endpoint. ## MapGraphQLSchema Call `MapGraphQLSchema()` on the `IEndpointRouteBuilder` to make your GraphQL schema available at a specific endpoint. C# ``` app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGraphQLSchema("/graphql/schema"); }); ``` 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. C# ``` var builder = WebApplication.CreateBuilder(args); builder .AddGraphQL() .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: C# ``` 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`: C# ``` 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](https://chillicream.com/docs/hotchocolate/performance/trusted-documents). ## AddGraphQL Parameters The `AddGraphQL()` method on `WebApplicationBuilder` accepts parameters that control request parsing and default security behavior. C# ``` builder.AddGraphQL( 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: C# ``` builder.AddGraphQL( maxAllowedRequestSize: 1 * 1000 * 1024); // ~1 MB ``` ### disableDefaultSecurity When `false` (the default), `AddGraphQL()` 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: C# ``` builder .AddGraphQL(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 C# ``` app.MapGraphQL().WithOptions(o => { o.EnableGetRequests = false; o.AllowedGetOperations = AllowedGetOperations.Query; o.Tool.Enable = false; }); ``` ### MapGraphQLHttp C# ``` app.MapGraphQLHttp("/graphql/http").WithOptions(o => { o.EnableMultipartRequests = false; o.EnforceGetRequestsPreflightHeader = true; }); ``` ### MapGraphQLWebSocket The WebSocket endpoint accepts a delegate over `GraphQLSocketOptions` directly: C# ``` 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: C# ``` builder .AddGraphQL() .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. ## Next Steps - [HTTP Transport](https://chillicream.com/docs/hotchocolate/server/http-transport) for details on request formats, response formats, WebSocket transport, and SSE. - [Interceptors](https://chillicream.com/docs/hotchocolate/server/interceptors) for hooking into request processing. - [Trusted Documents](https://chillicream.com/docs/hotchocolate/performance/trusted-documents) for the full persisted operations workflow. - [Cost Analysis](https://chillicream.com/docs/hotchocolate/security/cost-analysis) for understanding the default security cost analyzer. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/server/endpoints.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Files - Hot Chocolate > Approaches for handling file uploads and downloads in Hot Chocolate: the Upload scalar with multipart requests, presigned upload URLs, and serving files. Canonical source: https://chillicream.com/docs/hotchocolate/server/files Handling files is traditionally not a concern of a GraphQL server, which is also why the [GraphQL over HTTP](https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md) specification does not mention it. That said, at some point in the development of a new application you will likely have to deal with files in some way. This page gives you guidance on the available approaches. ## Uploading Files When it comes to uploading files, you have several options. ### Completely Decoupled You can handle file uploads completely decoupled from your GraphQL server, for example using a dedicated web application that offers an HTTP endpoint for uploads. This has a couple of downsides: - Authentication and authorization need to be handled by the dedicated endpoint as well. - The process of uploading a file needs to be documented outside of your GraphQL schema. ### Upload Scalar Hot Chocolate implements the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) which adds a new `Upload` scalar and lets your GraphQL server handle file upload streams. Warning Files cannot yet be uploaded through a gateway to stitched services using the `Upload` scalar. #### Usage Register the `Upload` scalar to use file upload streams in your input types or as an argument: C# ``` builder .AddGraphQL() .AddType(); ``` Note The `Upload` scalar can only be used as an input type and does not work on output types. Use the `Upload` scalar as an argument: C# ``` public class Mutation { public async Task UploadFileAsync(IFile file) { var fileName = file.Name; var fileSize = file.Length; await using Stream stream = file.OpenReadStream(); // You can now work with standard stream functionality of .NET // to handle the file. } } ``` [Learn more about arguments](https://chillicream.com/docs/hotchocolate/defining-a-schema/arguments) In input object types you can use it as follows: C# ``` public class ExampleInput { [GraphQLType(typeof(NonNullType))] public IFile File { get; set; } } ``` [Learn more about input object types](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types) If you need to upload a list of files, use a `List` or `ListType`. [Learn more about lists](https://chillicream.com/docs/hotchocolate/defining-a-schema/lists) #### UploadValueNode The upload literal node is called `UploadValueNode`. If you reference this type in custom scalar logic or tests, use the following pattern: C# ``` if (valueLiteral is UploadValueNode uploadValue) { var file = uploadValue.File; var key = uploadValue.Key; } ``` When constructing upload value nodes manually, the constructor now also requires the multipart key: C# ``` var valueNode = new UploadValueNode("0", file); ``` #### Client Usage When performing a mutation with the `Upload` scalar, you need to use variables. An example mutation: GraphQL ``` mutation ($file: Upload!) { uploadFile(file: $file) { success } } ``` Send this request to your GraphQL server using HTTP multipart: Bash ``` curl localhost:5000/graphql \ -H "GraphQL-preflight: 1" \ -F operations='{ "query": "mutation ($file: Upload!) { uploadFile(file: $file) { success } }", "variables": { "file": null } }' \ -F map='{ "0": ["variables.file"] }' \ -F 0=@file.txt ``` Note The `$file` variable is intentionally `null`. Hot Chocolate fills it in on the server. Note The `GraphQL-preflight: 1` HTTP header is required since version 13.2 for security reasons. [More examples can be found here](https://github.com/jaydenseric/graphql-multipart-request-spec#examples) You can check if your GraphQL client supports the specification [here](https://github.com/jaydenseric/graphql-multipart-request-spec#client). Both Relay and Apollo support this specification through community packages: - [react-relay-network-modern](https://github.com/relay-tools/react-relay-network-modern) using the `uploadMiddleware` - [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client) #### Options If you need to upload larger files or set custom upload size limits, configure [FormOptions](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.formoptions): C# ``` builder.Services.Configure(options => { // Set the limit to 256 MB options.MultipartBodyLengthLimit = 268435456; }); ``` Depending on your web server, you might need to configure these limits elsewhere as well. [Kestrel](https://docs.microsoft.com/aspnet/core/mvc/models/file-uploads#kestrel-maximum-request-body-size) and [IIS](https://docs.microsoft.com/aspnet/core/mvc/models/file-uploads#iis) are covered in the ASP.NET Core documentation. ### Presigned Upload URLs The best solution for uploading files is a hybrid approach. Your GraphQL server provides a mutation for uploading files, **but** the mutation only sets up the file upload. The actual file upload happens through a dedicated endpoint. You accomplish this by returning _presigned upload URLs_ from your mutations. These are URLs that point to an endpoint through which files can be uploaded. Files can only be uploaded to this endpoint if the URL contains a valid token. Your mutation generates the token, appends it to the upload URL, and returns the presigned URL to the client. Here is an example mutation resolver: C# ``` public record ProfilePictureUploadPayload(string UploadUrl); public class Mutation { [Authorize] public ProfilePictureUploadPayload UploadProfilePicture() { var baseUrl = "https://blob.chillicream.com/upload"; // Handle authorization logic here // If the user is allowed to upload, generate the token var token = "myUploadToken"; var uploadUrl = QueryHelpers.AddQueryString(baseUrl, "token", token); return new(uploadUrl); } } ``` If you are using a major cloud provider for storing your BLOBs, they likely support presigned upload URLs: - [Azure Storage shared access signatures](https://docs.microsoft.com/azure/storage/common/storage-sas-overview) - [AWS presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html) - [GCP signed URLs](https://cloud.google.com/storage/docs/access-control/signed-urls) Here is how a client would upload a new profile picture: **Request** GraphQL ``` mutation { uploadProfilePicture { uploadUrl } } ``` **Response** JSON ``` { "data": { "uploadProfilePicture": { "uploadUrl": "https://blob.chillicream.com/upload?token=myUploadToken" } } } ``` Given the `uploadUrl`, the client can HTTP POST the file to this endpoint to upload the profile picture. This solution offers the following benefits: - Uploading files is treated as a separate concern and your GraphQL server stays focused on GraphQL. - The GraphQL server maintains control over authorization and all business logic regarding granting a file upload stays in one place. - The action of uploading a profile picture is described by the schema and therefore more discoverable for developers. There is still some uncertainty about how the actual file upload happens, such as which HTTP verb to use or which headers to send with the `uploadUrl`. These additional parameters can be documented separately or made queryable through your mutation. ## Serving Files Imagine you want to expose the file you uploaded as the user's profile picture. How do you query for this file? You _could_ make the profile picture a queryable field that returns the Base64-encoded image. While this _can_ work, it has several downsides: - Since the image is part of the JSON serialized GraphQL response, caching is very difficult. - A query for the user's name might take a few milliseconds. Adding the image data might increase the response time by seconds. - Streaming (for example, video playback) would not work. The recommended solution is to serve files through a different HTTP endpoint and reference that endpoint in your GraphQL response. Instead of querying for the profile picture data, query for a URL that points to the profile picture. **Request** GraphQL ``` { user { name imageUrl } } ``` **Response** JSON ``` { "data": { "user": { "name": "John Doe", "imageUrl": "https://blob.chillicream.com/john-doe.png" } } } ``` Serving the file through a dedicated HTTP endpoint makes caching much easier and supports features like streaming. It gives the client control over how a resource is handled given its URL. In a web application, you pass the `imageUrl` as `src` to an HTML `img` element and let the browser handle fetching and caching. If you are using a cloud provider for file storage, you are likely already accessing files using a URL and can expose this URL as a `String` field in your graph. If infrastructure for serving files is not in place, you can set up file serving with ASP.NET Core or a dedicated web server like nginx. ## Next Steps - [Arguments](https://chillicream.com/docs/hotchocolate/defining-a-schema/arguments) for details on defining input arguments. - [Input Object Types](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types) for defining complex input types. - [Migrate from v15 to v16](https://chillicream.com/docs/hotchocolate/migrating/migrate-from-15-to-16#filevaluenode-renamed-to-uploadvaluenode) for the `FileValueNode` rename details. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/server/files.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Global State - Hot Chocolate > Share per-request data across all Hot Chocolate resolvers and middleware with Global State, initialized at request start and accessed via [GlobalState]. Canonical source: https://chillicream.com/docs/hotchocolate/server/global-state Global State lets you define properties on a per-request basis and makes them available to all resolvers and middleware. ## Initializing Global State Add Global State using the `SetProperty` method on the `OperationRequestBuilder`. This method takes a `key` and a `value` as arguments. The `key` must be a `string`, and the value can be of any type. Using an interceptor lets you initialize Global State before the request is executed. C# ``` public class HttpRequestInterceptor : DefaultHttpRequestInterceptor { public override ValueTask OnCreateAsync(HttpContext context, IRequestExecutor requestExecutor, OperationRequestBuilder requestBuilder, CancellationToken cancellationToken) { string userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; requestBuilder.SetProperty("UserId", userId); // requestBuilder.SetProperty("IntegerValue", int.Parse(userId)); // requestBuilder.SetProperty("ObjectValue", new User { Id = userId }); return base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken); } } ``` [Learn more about interceptors](https://chillicream.com/docs/hotchocolate/server/interceptors) ## Accessing Global State Access Global State in your resolvers as follows. C# ``` public class Query { public string Example1([GlobalState("UserId")] string userId) { // Omitted code for brevity } public string Example2([GlobalState("ObjectValue")] User user) { // Omitted code for brevity } } ``` The `GlobalStateAttribute` accepts the `key` of the Global State `value` as an argument. An exception is thrown if no Global State value exists for the specified `key` or if the `value` cannot be coerced to the type of the argument. Creating a custom attribute that inherits from `GlobalStateAttribute` is a good practice: C# ``` public class UserIdAttribute : GlobalStateAttribute { public UserIdAttribute() : base("UserId") { } } public class Query { public string Example([UserId] string userId) { // Omitted code for brevity } } ``` ## Next Steps - [Interceptors](https://chillicream.com/docs/hotchocolate/server/interceptors) for initializing state before request execution. - [Dependency Injection](https://chillicream.com/docs/hotchocolate/resolvers/dependency-injection) for injecting services into resolvers. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/hotchocolate/server/global-state.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # HTTP Transport - Hot Chocolate > How Hot Chocolate implements the GraphQL over HTTP specification: content negotiation, status codes, incremental delivery, and streaming transports like SSE. Canonical source: https://chillicream.com/docs/hotchocolate/server/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. ### POST Requests The GraphQL HTTP POST request is the most commonly used variant for GraphQL requests over HTTP and is specified [here](https://github.com/graphql/graphql-over-http/blob/master/spec/GraphQLOverHTTP.md#post). **request:** HTTP ``` POST /graphql HOST: foo.example Content-Type: application/json { "query": "query($id: ID!){user(id:$id){name}}", "variables": { "id": "QVBJcy5ndXJ1" } } ``` **response:** HTTP ``` HTTP/1.1 200 OK Content-Type: application/json { "data": { "user": { "name": "Jon Doe" } } } ``` ### GET Requests GraphQL can also be served through an HTTP GET request. You have the same options as the HTTP POST request, but the request properties are provided as query parameters. GraphQL HTTP GET requests can be a good choice when you want to cache GraphQL requests. For example, if you wanted to execute the following GraphQL query: GraphQL ``` query ($id: ID!) { user(id: $id) { name } } ``` With the following query variables: JSON ``` { "id": "QVBJcy5ndXJ1" } ``` This request could be sent via an HTTP GET as follows: **request:** HTTP ``` GET /graphql?query=query(%24id%3A%20ID!)%7Buser(id%3A%24id)%7Bname%7D%7D&variables=%7B%22id%22%3A%22QVBJcy5ndXJ1%22%7D` HOST: foo.example ``` **response:** HTTP ``` HTTP/1.1 200 OK Content-Type: application/json { "data": { "user": { "name": "Jon Doe" } } } ``` Note {query} and {operationName} parameters are encoded as raw strings in the query component. Therefore if the query string contained operationName=null then it should be interpreted as the {operationName} being the string "null". If a literal null is desired, the parameter (e.g. {operationName}) should be omitted. The GraphQL HTTP GET request is specified [here](https://github.com/graphql/graphql-over-http/blob/master/spec/GraphQLOverHTTP.md#get). ## DefaultHttpResponseFormatter The `DefaultHttpResponseFormatter` abstracts how responses are delivered over HTTP. You can override certain aspects of the formatter by creating your own formatter that inherits from `DefaultHttpResponseFormatter`: C# ``` public class CustomHttpResponseFormatter : DefaultHttpResponseFormatter { // ... } ``` Register the formatter: C# ``` builder.Services.AddHttpResponseFormatter(); ``` If you want to pass `HttpResponseFormatterOptions` to a custom formatter, make the following adjustments: C# ``` var options = new HttpResponseFormatterOptions(); builder.Services.AddHttpResponseFormatter(_ => new CustomHttpResponseFormatter(options)); public class CustomHttpResponseFormatter : DefaultHttpResponseFormatter { public CustomHttpResponseFormatter(HttpResponseFormatterOptions options) : base(options) { } } ``` ### Customizing Status Codes You can use a custom formatter to alter the HTTP status code in certain conditions. Warning Altering status codes can break the assumptions of your server's clients and might lead to issues. Proceed with caution. C# ``` public class CustomHttpResponseFormatter : DefaultHttpResponseFormatter { protected override HttpStatusCode OnDetermineStatusCode( IOperationResult result, FormatInfo format, HttpStatusCode? proposedStatusCode) { if (result.Errors?.Count > 0 && result.Errors.Any(error => error.Code == "SOME_AUTH_ISSUE")) { return HttpStatusCode.Forbidden; } // In all other cases let Hot Chocolate figure out the // appropriate status code. return base.OnDetermineStatusCode(result, format, proposedStatusCode); } } ``` ## JSON Serialization You can alter some JSON serialization settings when configuring the `HttpResponseFormatter`. ### Stripping Nulls from Response By default, the JSON in your GraphQL responses contains `null`. If you want to reduce payload size and your clients can handle it, strip nulls from responses: C# ``` var options = new HttpResponseFormatterOptions { Json = new JsonResultFormatterOptions { NullIgnoreCondition = JsonNullIgnoreCondition.All } }; builder.Services.AddHttpResponseFormatter(options); ``` ### Indenting JSON in Response By default, the JSON in your GraphQL responses is not indented. If you want to indent your JSON: C# ``` builder.Services.AddHttpResponseFormatter(indented: true); ``` Be aware that indenting JSON results in a slightly larger response size. If you are defining other `HttpResponseFormatterOptions`, configure the indentation through the `Json` property: C# ``` var options = new HttpResponseFormatterOptions { Json = new JsonResultFormatterOptions { Indented = true } }; builder.Services.AddHttpResponseFormatter(options); ``` ## Incremental Delivery (`@defer` / `@stream`) When using `@defer` or `@stream`, Hot Chocolate streams results to the client using one of three transport formats, selected via the `Accept` header: | Accept header | Transport | Content-Type | | ----------------- | ---------- | ----------------- | | multipart/mixed | Multipart | multipart/mixed | | text/event-stream | SSE | text/event-stream | | application/jsonl | JSON Lines | application/jsonl | If no streaming `Accept` header is provided, the default is `multipart/mixed`. ### Incremental Delivery Wire Format There are two wire formats for how incremental results are represented in the response payload. **v0.2 (default)** uses `pending`, `incremental` with `id`, and `completed` to track deferred fragments: JSON ``` {"data":{"product":{"name":"Abc"}},"pending":[{"id":"2","path":["product"]}],"hasNext":true} {"incremental":[{"id":"2","data":{"description":"Abc desc"}}],"completed":[{"id":"2"}],"hasNext":false} ``` **v0.1 (legacy)** uses `path` and `label` directly on incremental entries: JSON ``` {"data":{"product":{"name":"Abc"}},"hasNext":true} {"incremental":[{"data":{"description":"Abc desc"},"path":["product"]}],"hasNext":false} ``` The default format is v0.2\. If your clients depend on the legacy format, you have two options: client-driven format selection or changing the server default. #### Client-Driven Format Selection Clients choose which format they want by adding the `incrementalSpec` parameter to the `Accept` header: ``` Accept: multipart/mixed; incrementalSpec=v0.1 Accept: text/event-stream; incrementalSpec=v0.2 Accept: application/jsonl; incrementalSpec=v0.1 ``` When the client does not specify `incrementalSpec`, the server default is used. #### Changing the Server Default The default incremental delivery format is v0.2\. To change it server-wide: C# ``` builder .AddGraphQL() .AddHttpResponseFormatter( incrementalDeliveryFormat: IncrementalDeliveryFormat.Version_0_1); ``` Or with the options overload: C# ``` builder .AddGraphQL() .AddHttpResponseFormatter( new HttpResponseFormatterOptions { /* ... */ }, incrementalDeliveryFormat: IncrementalDeliveryFormat.Version_0_1); ``` The server default is only used as a fallback. A client that sends `incrementalSpec=v0.1` or `incrementalSpec=v0.2` in the `Accept` header always gets the format it asked for, regardless of the server default. ## Streaming Transports Hot Chocolate supports three streaming transport formats for delivering result streams (incremental delivery, batching, and subscriptions). The client selects the format via the `Accept` header. ### Multipart (`multipart/mixed`) The default streaming transport. Each result is sent as a separate MIME part separated by a boundary string. This is the most widely supported format. ``` Accept: multipart/mixed ``` ### Server-Sent Events (`text/event-stream`) Results are delivered as SSE events. This transport works well with browser `EventSource` APIs and proxies that support SSE. ``` Accept: text/event-stream ``` Each result is sent as an `event: next` message with the JSON payload in the `data:` field. A final `event: complete` message signals the end of the stream. ### JSON Lines (`application/jsonl`) Each result is written as a single line of JSON, separated by newlines. This format is compact and straightforward to parse incrementally, making it well-suited for batch responses. ``` Accept: application/jsonl ``` ``` {"data":{"hero":{"name":"R2-D2"}}} {"data":{"hero":{"name":"Luke Skywalker"}}} ``` The server sends periodic keep-alive messages (a space followed by a newline) to prevent connection timeouts. ## Batching Hot Chocolate supports operation batching, request batching, and variable batching. These features let you send and execute multiple GraphQL operations in a single HTTP request, with results streamed back using one of the transport formats above. For full details on how to enable and use batching, see the [Batching](https://chillicream.com/docs/hotchocolate/server/batching) page. ## Supporting Legacy Clients Your clients might not yet support the [GraphQL over HTTP specification](https://github.com/graphql/graphql-over-http/blob/a1e6d8ca248c9a19eb59a2eedd988c204909ee3f/spec/GraphQLOverHTTP.md). This can be problematic if they cannot handle a different response `Content-Type` or HTTP status codes besides `200`. If you have control over the client, you can either: - Update the client to support the GraphQL over HTTP specification - Send the `Accept: application/json` request header in your HTTP requests, signaling that your client only understands the legacy format If you cannot update or change the `Accept` header your clients are sending, configure that a missing `Accept` header or a wildcard like `*/*` should be treated as `application/json`: C# ``` builder.Services.AddHttpResponseFormatter(new HttpResponseFormatterOptions { HttpTransportVersion = HttpTransportVersion.Legacy }); ``` 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 lists the sub-protocols it supports in the standard WebSocket `Sec-WebSocket-Protocol` header during the handshake, ordered by preference. Hot Chocolate accepts the first listed sub-protocol it supports. If none of the listed sub-protocols is supported, the server closes the connection with close code `1002` (protocol error). ### Enabling WebSocket Support You must register the ASP.NET Core WebSocket middleware before calling `MapGraphQL()`. Without this, WebSocket upgrade requests are not handled. C# ``` var builder = WebApplication.CreateBuilder(args); builder .AddGraphQL() .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`: C# ``` builder .AddGraphQL() .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`: C# ``` 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 lists the sub-protocols it supports. 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: ``` 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 ``` > The example imports `@modelcontextprotocol/ext-apps` from jsDelivr to keep the HTML self-contained and easy to read. For larger views you would typically bundle the SDK with a tool like Vite and emit a single HTML file as the build output. The script creates an `App` whose `name` matches the tool, registers handlers for incoming tool results, host theme changes, and teardown, then opens the bridge to the host with `app.connect()`. The `getHostContext()` replay at the end covers the case where the host's theme is already available before the handler is wired up. For the full handler list, lifecycle details, and how to call other tools from a view, see the [MCP Apps SDK API reference](https://apps.extensions.modelcontextprotocol.io/api/). ### Author a prompt Prompts are pure JSON. Each prompt has `arguments` (the inputs the user fills in) and `messages` (the templated conversation that gets handed to the model). `mcp/prompts/SearchProducts/SearchProducts.json`: JSON ``` { "title": "Search Products", "description": "Search for products in the catalog based on a search query.", "icons": [ { "source": "https://example.com/favicon-32x32.png", "sizes": ["32x32"], "mimeType": "image/png", "theme": "dark" } ], "arguments": [ { "name": "searchQuery", "title": "Search Query", "description": "The search query to find relevant products.", "required": true } ], "messages": [ { "role": "user", "content": { "type": "text", "text": "Find products related to \"{searchQuery}\" in the product catalog." } } ] } ``` The `{searchQuery}` placeholder in the message text is interpolated from the matching argument by name. Any argument you declare in `arguments` is available as `{name}` inside `messages`. ### Settings reference The full set of fields supported by the tool and prompt settings files. #### Tool settings `{tool-name}.json` is an object. Every property is optional. | Property | Type | Description | | ----------- | ----------- | ----------------------------------------------------------- | | title | string | Human-readable title shown in the host's tool picker. | | icons | Icon\[\] | Icons displayed alongside the title. See **Icon** below. | | annotations | Annotations | Behavior hints for the model. See **Annotations** below. | | view | View | Render configuration for the Apps view. See **View** below. | | visibility | string\[\] | Who can call the tool. See **Visibility** below. | **Icon** | Property | Type | Description | | -------- | ----------------- | ------------------------------------------------------------------------------------------------ | | source | string (required) | URI of the icon. HTTPS URL or data: URI. | | mimeType | string | MIME type, for example image/png, image/svg+xml. Overrides the server's MIME type when supplied. | | sizes | string\[\] | One or more size specifiers like "32x32" or "any". | | theme | "light" \| "dark" | The UI theme this icon is designed for. | **Annotations** All values are booleans. Each is a hint surfaced to the model. | Property | Description | | --------------- | --------------------------------------------------------------------- | | destructiveHint | The operation may cause destructive side effects. | | idempotentHint | The operation is safe to retry. | | openWorldHint | The operation interacts with an open-world system (no closed schema). | **View** | Property | Type | Description | | ------------- | ----------- | ----------------------------------------------------------------- | | prefersBorder | boolean | Render the iframe with a visible border. | | domain | string | Dedicated origin for the view sandbox. | | permissions | Permissions | Browser permissions the view requests. See **Permissions** below. | | csp | Csp | Content Security Policy domain allowlists. See **CSP** below. | **Permissions** All values are booleans. Each maps to a Permission Policy feature requested for the view's iframe. | Property | Description | | -------------- | ------------------------------- | | camera | Request camera access. | | microphone | Request microphone access. | | geolocation | Request geolocation access. | | clipboardWrite | Request clipboard write access. | **CSP** Each property is an array of origin strings. | Property | Description | | --------------- | -------------------------------------------------------- | | baseUriDomains | Allowed values for the base-uri directive. | | connectDomains | Origins for fetch, XHR, and WebSocket requests. | | frameDomains | Origins allowed in nested iframes (frame-src directive). | | resourceDomains | Origins for scripts, images, styles, and fonts. | **Visibility** A list of one or more values controlling who can call the tool. Use this to expose a helper tool only to other tools' Apps views without surfacing it to the agent. | Value | Description | | ------- | ------------------------------------------------------- | | "model" | The tool is visible to and callable by the agent. | | "app" | The tool is callable by Apps views on this server only. | #### Prompt settings `{prompt-name}.json` is an object. `messages` is required, everything else is optional. | Property | Type | Description | | ----------- | ---------------------- | ---------------------------------------------------------------------- | | messages | Message\[\] (required) | The templated conversation handed to the model. See **Message** below. | | title | string | Human-readable title shown in the host's prompt picker. | | description | string | Human-readable description. | | arguments | Argument\[\] | User-fillable arguments. See **Argument** below. | | icons | Icon\[\] | Same shape as the tool Icon above. | **Argument** | Property | Type | Description | | ----------- | ----------------- | ---------------------------------------------------------------- | | name | string (required) | Argument name. Referenced as {name} inside message text. | | title | string | Display title in the host's prompt form. | | description | string | Help text shown alongside the input. | | required | boolean | Whether the user must provide a value before running the prompt. | **Message** | Property | Type | Description | | -------- | -------------------------------- | ------------------------------------ | | role | "user" \| "assistant" (required) | Role attached to the message. | | content | Content (required) | Message body. See **Content** below. | **Content** Only text content is supported. | Property | Type | Description | | -------- | ----------------- | -------------------------------------------------------------------------------------- | | type | "text" (required) | Always "text". | | text | string (required) | The message text. {argName} placeholders are interpolated from the prompt's arguments. | ## Publish with the Nitro CLI The CLI does the heavy lifting: archive, validate, upload, publish. ### 1\. Log in ``` nitro login ``` You only need to do this once per machine. CI environments authenticate with `--api-key` instead. See [Global Options](https://chillicream.com/docs/nitro/cli/global-options). ### 2\. Create a feature collection A collection is a named container for your tools and prompts. Create one for your API. ``` nitro mcp create \ --name "" \ --api-id "" ``` Get the API ID from `nitro api list` or the Nitro UI. The command prints the new collection's ID. Save it. Every subsequent command needs it. See [nitro mcp create](https://chillicream.com/docs/nitro/cli/mcp#nitro-mcp-create) for the full reference. ### 3\. Upload a version Each upload is a complete snapshot tagged with a name (a release tag, a Git commit SHA, anything you want). ``` nitro mcp upload \ --mcp-feature-collection-id "" \ --tag "v1" \ --tool-pattern "./mcp/tools/**/*.graphql" \ --prompt-pattern "./mcp/prompts/**/*.json" ``` The CLI walks the glob patterns, finds the sibling `.json` and `.html` files automatically, packages everything into a ZIP archive, and uploads it. Nitro validates the archive on the server before storing it. See [nitro mcp upload](https://chillicream.com/docs/nitro/cli/mcp#nitro-mcp-upload) for all options. ### 4\. Publish to a stage Uploading does not expose anything to clients. You must explicitly publish a tagged version to a stage. ``` nitro mcp publish \ --mcp-feature-collection-id "" \ --tag "v1" \ --stage "dev" ``` Stages are independent: publishing to `dev` does not touch `production`. To roll back, publish an earlier tag to the same stage. See [nitro mcp publish](https://chillicream.com/docs/nitro/cli/mcp#nitro-mcp-publish) for gated stages and approval flows. ### Optional: validate in CI If you want to gate a deploy step in a separate pipeline job, run validation explicitly: ``` nitro mcp validate \ --mcp-feature-collection-id "" \ --stage "dev" \ --tool-pattern "./mcp/tools/**/*.graphql" \ --prompt-pattern "./mcp/prompts/**/*.json" ``` > Validation also runs automatically inside `nitro mcp publish`. Use the standalone command only when CI needs a separate gate. For the full reference, see [Nitro CLI MCP commands](https://chillicream.com/docs/nitro/cli/mcp). ### Find your server URL The MCP endpoint is hosted by your HotChocolate server, not by Nitro. Once a version is published, the runtime picks it up and serves it at the adapter's default route: ``` https:///graphql/mcp ``` Replace `` with the public URL of your HotChocolate server or Fusion gateway. The path is configurable, but `/graphql/mcp` is the default and what most deployments use. Give that URL to your MCP clients. ### Test with MCP Inspector Before wiring the server into a chat client, smoke-test it with [MCP Inspector](https://github.com/modelcontextprotocol/inspector), the official MCP debugging tool. It connects to your server URL, lists every tool and prompt, and lets you invoke them with arbitrary arguments. ``` npx @modelcontextprotocol/inspector ``` Open the printed URL in a browser, point it at `https:///graphql/mcp`, and exercise your tools. Use this whenever you change a tool or prompt to confirm the server returns what you expect, without round-tripping through a chat host. ## Connect from an MCP client Any MCP client can connect to the URL from the previous section. The exact UI for adding a remote MCP server differs per client and the menus tend to shift over time. The links below point at each vendor's current setup documentation. - **ChatGPT**: [Connect from ChatGPT (Apps SDK)](https://developers.openai.com/apps-sdk/deploy/connect-chatgpt) - **Claude**: [Get started with custom connectors using remote MCP](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp) - **VS Code**: [Add and manage MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers) - **Other clients**: [MCP clients directory](https://modelcontextprotocol.io/clients) In every client the inputs are the same: a name, a description, the MCP server URL, and authentication settings. Tools that ship an HTML resource render as interactive MCP Apps views in clients that support the extension. Plain-text clients render the tool result as text and ignore the view. ## Storage and telemetry Nitro provides operational infrastructure around the collection so you do not have to build it yourself. **Storage.** Each feature collection is workspace-scoped and tied to one API. Versions are immutable, tagged snapshots: every upload creates a new version, and new versions replace rather than merge with the previous one. Rollback is a republish of an earlier tag. **Telemetry.** Every published tool tracks request count, error count, mean duration, P95 and P99 latency, operations per minute, error rate, and an impact score. Distributed traces are emitted so you can correlate tool calls with the rest of your GraphQL server. Error insights aggregate per error type with errors-per-minute and last-seen timestamps. All metrics are visible per stage in the Nitro UI. **Validation.** Tool GraphQL documents are validated against your schema. Prompt JSON is validated for structure. The validator also checks for conflicts with other tools already published to the target stage. Failures block the publish. **Stages and permissions.** Stages are independent: there is no automatic promotion from `dev` to `production`. Permissions are stage-scoped, so a user who can publish to `dev` cannot publish to `production` without the matching permission there. ## Next steps - Full CLI reference: [Nitro CLI MCP commands](https://chillicream.com/docs/nitro/cli/mcp). - The MCP specification: [modelcontextprotocol.io](https://modelcontextprotocol.io/). - The MCP Apps SDK: [API reference](https://apps.extensions.modelcontextprotocol.io/api/). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/adapters/mcp.md) Maintained by ChilliCream. Last updated on **August 12, 2026** by **Glen** --- # Analyzers - Nitro > The `ChilliCream.Nitro` Roslyn source generator emits an `AddDefaults()` method that wires up Nitro integration and warns when required packages are missing. Canonical source: https://chillicream.com/docs/nitro/analyzers The `ChilliCream.Nitro` meta-package ships with a Roslyn source generator that simplifies Nitro integration setup. It provides compile-time warnings when required packages are missing and generates an `AddDefaults()` extension method that wires everything up in a single call. ## Compile-time warnings If your project references HotChocolate or Fusion but is missing the corresponding Nitro integration package, you get a compile-time warning: | Warning | Trigger | | ---------- | --------------------------------------------------------------- | | **NS0001** | HotChocolate referenced without ChilliCream.Nitro.HotChocolate | | **NS0002** | HotChocolate.Fusion referenced without ChilliCream.Nitro.Fusion | ## Generated `AddDefaults()` extension method When the correct integration package is referenced, the generator emits an `AddDefaults()` extension method on `INitroBuilder`. This method wires up the default Nitro integration with the GraphQL pipeline in a single call. C# ``` builder.Services .AddNitro(options => { options.ApiId = "my-api"; options.ApiKey = "my-key"; options.Stage = "production"; }) .AddDefaults(); ``` You can still customize per-schema options by calling `ModifyNitroOptions()` on the builder afterwards: C# ``` builder.Services .AddNitro(options => { options.ApiId = "my-api"; options.ApiKey = "my-key"; }) .AddDefaults(); builder.Services .AddGraphQLServer() .AddQueryType() .ModifyNitroOptions(options => { options.PersistedOperations.Enabled = true; options.Metrics.Enabled = true; }); ``` ## Troubleshooting If `AddDefaults()` does not appear in IntelliSense: 1. Verify that `ChilliCream.Nitro` is referenced (this is the meta-package containing the source generator). 2. Verify that the matching integration package is referenced (`ChilliCream.Nitro.HotChocolate` for HotChocolate projects, `ChilliCream.Nitro.Fusion` for Fusion projects). 3. Rebuild the project so the source generator can run. If you cannot use the source generator, call the explicit method instead: `.AddHotChocolate()` or `.AddFusion()` on the `INitroBuilder`. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/analyzers.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Client Registry - Nitro > Manage GraphQL clients and persisted operations with the Nitro client registry: validate operations against the schema and distribute them to your server. Canonical source: https://chillicream.com/docs/nitro/apis/client-registry ![Image](https://chillicream.com/images/nitro-docs/apis/client-registry-0.webp) The client registries is an important tool for managing your GraphQL Clients. It provides a centralized location for clients and queries. You can use the client registry to manage your clients and their queries. It allows you to validate your queries against the schema, ensuring that all the operations defined by a client are compatible with the current schema. This validation step is critical to prevent the execution of invalid queries that might result in runtime errors. ## Understanding Clients A client, in the context of a GraphQL API, is an entity that interacts with the API by defining and executing GraphQL operations. These operations are stored on the API as persisted operations. ### What is a Persisted Operation? A persisted operation is a GraphQL operation that has been sent to the server, stored, and assigned a unique identifier (hash). Instead of sending the full text of a GraphQL operation to the server for execution, clients can send the hash of the operation, reducing the amount of data transmitted over the network. This practice is particularly beneficial for mobile clients operating in environments with limited network capacity. Persisted operations also add an extra layer of security as the server can be configured to only execute operations that have been previously stored, which prevents malicious queries. This is the cheapest and most effective way to secure your GraphQL API from potential attacks. ![Image](https://chillicream.com/images/nitro-docs/apis/client-registry-1.webp) Persisted operations can be inspected in the `Operations` tab. ### The Role of the Client Registry The client registry plays a crucial role in managing these persisted operations. It is used to validate the operations against the schema, ensuring that all the operations defined by a client are compatible with the current schema. This validation step is critical to prevent the execution of invalid operations that might result in runtime errors. Additionally, the client registry is responsible for distributing the operations to the GraphQL server. It maintains a mapping of hashes to operation keys, informing the server which hash corresponds to which operation. This allows the server to efficiently look up and execute the appropriate operation when it receives a request from a client. ### Client Versions A client can have multiple versions, with each version containing a different set of persisted operations. This versioning system allows for incremental updates and changes to the client's operations without disrupting the existing functionality. As new versions are released, they can be validated and registered with the client registry, ensuring that they are compatible with the current schema and can be executed by the server. By managing client versions and persisted operations, the client registry helps maintain the integrity and smooth operation of your GraphQL API. It ensures that your clients and API can evolve together without breaking, contributing to a more robust and reliable system. The number of active client versions can vary depending on the nature of the client. For instance, a website usually has one active client version per stage. However, during deployment, you might temporarily have two active versions as the new version is phased in and the old version is phased out. On the other hand, for mobile clients, you often have multiple versions active simultaneously. This is because users may be using different versions of the app, and not all users update their apps at the same time. Once a client version is no longer in use, it reaches its end of life. At this point, you can unpublish the client version from the client registry. This will remove its persisted operations from distribution, and they will no longer be validated against the schema. ### The Operations File In the context of GraphQL, the operations file is a structured file that holds a collection of persisted operations for a client. This file serves as a reference for the client to manage and execute specific operations against a GraphQL API. #### Understanding the Format and Structure The operations file typically adopts the JSON format as used by Relay. It comprises key-value pairs, with each pair representing a unique persisted operation. The key corresponds to a hash identifier for the operation, and the value is the GraphQL operation string. Below is an illustrative example of an operations file (`operations.json`): JSON ``` { "913abc361487c481cf6015841c0eca22": "{ me { username } }", "0e7cf2125e8eb711b470cc72c73ca77e": "{ me { id } }" ... } ``` #### Compatibility with GraphQL Clients Several GraphQL clients have built-in support for this Relay-style operations file format. This compatibility allows for a standardized way of handling persisted operations across different clients. For more details on how various clients implement and work with persisted operations, consider referring to their respective documentation: - [StrawberryShake](https://chillicream.com/docs/strawberryshake/performance/persisted-operations) - [URQL](https://nearform.com/open-source/urql/docs/advanced/persistence-and-uploads/) - [Relay](https://relay.dev/docs/guides/persisted-queries/) ## Setting Up a Client Registry To set up a client registry, first, visit `nitro.chillicream.com` and sign up for an account. Next, you'll need to download and install Nitro CLI, the .NET tool used to manage your client registry. You can find more information about Nitro CLI in the [Nitro CLI Documentation](https://chillicream.com/docs/nitro/cli/installation). After installing Nitro CLI, create a new API either through the Nitro App or the CLI. In the app, simply right-click the document explorer and select "New API." If you prefer using the CLI, ensure you're logged in with the command `nitro login`, then create a new API with the command `nitro api create`. With these steps complete, you are ready to start using the client registry. To get the id of your API, use the command `nitro api list`. This command will list all of your APIs, their names, and their ids. You will need the id of your API to perform most operations on the schema registry. ## Using Persisted Operations To use persisted operations, the server needs to know how to translate the hash into the corresponding GraphQL operation. This is where the client registry comes in. The client registry maintains a mapping of hashes to operation keys, informing the server which hash corresponds to which operation. This allows the server to efficiently look up and execute the appropriate operation when it receives a request from a client. To connect your HotChocolate server to the client registry, you need the `ChilliCream.Nitro` NuGet package. This package contains the `AddNitro()` extension method, which can be used to configure the client registry. To install the Nitro services, run the following command in your project's root directory: Bash ``` dotnet add package ChilliCream.Nitro ``` After installing the package, you need to configure the services in your startup class. Below is a sample implementation in C#: C# ``` public void ConfigureServices(IServiceCollection services) { services .AddGraphQL() .AddQueryType() .AddNitro(x => // Connect to the client registry { x.ApiKey = "<>"; x.ApiId = "QXBpCmc5NGYwZTIzNDZhZjQ0NjBmYTljNDNhZDA2ZmRkZDA2Ng=="; x.Stage = "dev"; }) .UsePersistedOperationPipeline(); // Enable the persisted operation pipeline } ``` Tip **Using Environment Variables** Alternatively, you can set the required values using environment variables. This method allows you to call `AddNitro` without explicitly passing parameters. - `NITRO_API_KEY` maps to `ApiKey` - `NITRO_API_ID` maps to `ApiId` - `NITRO_STAGE` maps to `Stage` C# ``` public void ConfigureServices(IServiceCollection services) { services .AddGraphQL() .AddQueryType() .AddNitro() // Connect to the client registry .UsePersistedOperationPipeline(); // Enable the persisted operation pipeline } ``` In this setup, the API key, ID, and stage are set through environment variables. ### Block Ad-Hoc Queries While you want to allow ad-hoc queries during development, you might want to disable them in production. This can be done by setting the `PersistedOperations.OnlyAllowPersistedDocuments` option to `true` in the `ModifyRequestOptions` method. C# ``` public void ConfigureServices(IServiceCollection services) { services .AddGraphQL() .AddQueryType() .AddNitro() // Connect to the client registry .ModifyRequestOptions(x => x.PersistedOperations.OnlyAllowPersistedDocuments = true) .UsePersistedOperationPipeline(); // Enable the persisted operation pipeline } ``` You can also customize the error message that is returned when an ad-hoc operation is sent to the server. C# ``` public void ConfigureServices(IServiceCollection services) { services .AddGraphQL() .AddQueryType() .AddNitro() // Connect to the client registry .ModifyRequestOptions(x => { x.PersistedOperations.OnlyAllowPersistedDocuments = true; x.PersistedOperations.OperationNotAllowedError = ErrorBuilder.New() .SetMessage("Only persisted operations are allowed.") .Build(); }) .UsePersistedOperationPipeline(); // Enable the persisted operation pipeline } ``` ## Setup the cache You can setup a second level cache for persisted operations for improving your system's resilience and performance. Find out more about the cache here [Caching](https://chillicream.com/docs/nitro/apis/fusion). ## Integrating with Continuous Integration Integrating the client registry into your Continuous Integration/Continuous Deployment (CI/CD) pipeline maximizes their benefits. It ensures that the clients in your API are always up-to-date and tested against potential breaking changes. The schema and client registries work hand-in-hand to ensure the smooth functioning of your API. As you make changes to your schema, the schema registry helps manage these changes, preventing inadvertent breaking changes and preserving a history of your schemas. As you validate, upload, and publish new schemas, the client registry ensures that your clients remain compatible with these changes. As you release new versions of your clients, the client registry helps manage these versions and the operation documents associated with them. By working together, the schema and client registries help maintain the integrity of your API and the services that rely on it, ensuring that they can evolve together without breaking. ### Understanding the Flow The general flow for the client registry involves three main steps: validating the client, uploading it to the registry, and publishing it. 1. **Validate the Client**: The first step takes place during your Pull Request (PR) build. Here, you validate the client against the API using `nitro client validate` command. This ensures that the client is compatible with the API and will not break existing functionality. 2. **Upload the Client**: The second step takes place during your release build. Here, you upload the client to the registry using the `nitro client upload` command. This command requires the `--tag` and `--api-id` options. The `--tag` option specifies the tag for the client, and the `--api-id` option specifies the ID of the API to which you are uploading. This command create a new version of the client with the specified tag. The tag is a string that can be used to identify the client. It can be any string, but it is recommended to use a version number, such as `v1` or `v2`; or a commit hash, such as `a1b2c3d4e5f6g7h8i9j0k1l2m3n`. The tag is used to identify the client when publishing it. 3. **Publish the Client**: The third step takes place just before the release. Here, you publish the client using the `nitro client publish` commands. This command requires the `--tag` and `--api-id` options. The `--tag` option specifies the tag for the client, and the `--api-id` option specifies the ID of the API to which you are uploading. This command publishes the client with the specified tag, making it the active version for the specified API. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/apis/client-registry.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Deployment - Nitro > Track schema, client, and Fusion configuration deployments in the Nitro Deployments tab and gate risky changes with the `--wait-for-approval` flag. Canonical source: https://chillicream.com/docs/nitro/apis/deployments ![Deployment](https://chillicream.com/images/nitro-docs/apis/deployments-0.webp) Deploying a service typically involves publishing a client, schema, or fusion configuration to a stage. This process is an integral part of your service's deployment, where artifacts from your CI/CD pipeline are pushed to the platform and then published to a designated stage prior to the actual deployment of your service. Whenever you initiate a deployment of a client, schema, or fusion configuration through Nitro CLI, it logs an entry in the "Deployments" tab. This tab provides a chronological overview of all deployments executed on the stage, offering visibility into the deployment history and status. ## Setup Approvals In development environments, it's not uncommon for GraphQL changes to introduce breaking changes. To mitigate the risk of manually pushing such changes to a stage, Nitro CLI offers the `--wait-for-approval` flag. This option can be utilized when publishing a schema, client, or fusion configuration and serves as a time saver by allowing you to review and approve deployments directly from the "Deployments" tab. Deployments flagged with `--wait-for-approval` are held in a pending state, awaiting approval. They remain in this state until explicitly approved or automatically timed out after 10 minutes. This mechanism allows for a controlled deployment process, where potentially breaking changes can be reviewed and either approved or rejected directly from the "Deployments" tab. ![Deployment](https://chillicream.com/images/nitro-docs/apis/deployments-1.webp) [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/apis/deployments.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Fusion - Nitro > Use Nitro as the control plane for your Fusion gateway: monitor topology and subgraph telemetry, and let the gateway pull its latest configuration automatically. Canonical source: https://chillicream.com/docs/nitro/apis/fusion ![Image](https://chillicream.com/images/nitro-docs/apis/fusion-0.webp) Nitro can be used as your orchestrator for your Fusion gateway. It deeply integrates with your development workflow and allows you to publish, validate, consume and monitor your Fusion gateway. On the Fusion dashboard you can now see the tracing information of your gateway and your subgraphs. ## Dashboard The Fusion dashboard gives you a quick overview of your gateway and subgraphs. It shows you the status of your gateway and the status of your subgraphs. You can also see the latest telemetry data insights of your gateway and subgraphs. ### Topology ![Image](https://chillicream.com/images/nitro-docs/apis/fusion-1.webp) The topology view shows you the connections between your gateway and your subgraphs. You can also see which clients are connected to your gateway and how many operations they are executing. ### Status ![Image](https://chillicream.com/images/nitro-docs/apis/fusion-2.webp)The status view shows you a quick overview of the status of your gateway. With the indicators for latency, throughput and errors you see how your gateway statistics developed between the previous and the current time range. You also see the essential information about your gateway, such as the version, the stage, how many subgraphs are connected and how many clients are connected. ### Subgraphs ![Image](https://chillicream.com/images/nitro-docs/apis/fusion-3.webp)The subgraphs view shows you a quick overview over your connected subgraphs. You can see the latency, throughput and error rate of each subgraph. ## Gateway Management With Fusion you compose your gateway configuration locally when you deploy a subgraph. This means that you somehow need to inform your gateway that there is a new configuration available. With Nitro you can automate this process. You can configure your gateway to automatically pull the latest configuration from Nitro. This way you can be sure that your gateway always has the latest configuration. You can also validate your configuration against the schema and client registry to make sure that your change does not break any clients. ### Configure your gateway To configure your Fusion gateway to pull the configuration from Nitro, you need to install the `ChilliCream.Nitro` and `ChilliCream.Nitro.Fusion` packages. You can do this by running the following commands in your project's root directory: Bash ``` dotnet add package ChilliCream.Nitro dotnet add package ChilliCream.Nitro.Fusion ``` After installing the package, you need to configure the services in your startup class. Below is a sample implementation in C#: C# ``` builder.Services .AddNitro(x => { x.ApiKey = "<>"; x.ApiId = "QXBpCmc5NGYwZTIzNDZhZjQ0NjBmYTljNDNhZDA2ZmRkZDA2Ng=="; x.Stage = "dev"; }) .AddDefaults(); ``` Note `AddDefaults()` is generated by a source generator in the `ChilliCream.Nitro` package. It requires the `ChilliCream.Nitro.Fusion` package to be referenced in your project. If `AddDefaults()` is not available, verify your package references or use the explicit `.AddFusion()` method instead. See the [Analyzers](https://chillicream.com/docs/nitro/analyzers) page for details. Tip **Using Environment Variables** Alternatively, you can set the required values using environment variables. This method allows you to call `AddNitro` without explicitly passing parameters. - `NITRO_API_KEY` maps to `ApiKey` - `NITRO_API_ID` maps to `ApiId` - `NITRO_STAGE` maps to `Stage` C# ``` builder.Services .AddNitro() .AddDefaults(); ``` In this setup, the API key, ID, and stage are set through environment variables. Now your gateway will be notified whenever there is a new configuration available and will automatically pull it. ### Configure Your Subgraphs To set up your subgraphs to be linked with your gateway, you need to follow these steps: #### Step 1: Install ChilliCream.Nitro Package First, ensure that the `ChilliCream.Nitro` and `ChilliCream.Nitro.HotChocolate` packages are installed in your subgraph projects. If not, you can install them by running the following commands in the root directory of each subgraph project: Bash ``` dotnet add package ChilliCream.Nitro dotnet add package ChilliCream.Nitro.HotChocolate ``` #### Step 2: Configure Services in Startup After installing the package, configure the Nitro Services on your schema. Here is an example of how you can do this: C# ``` services .AddNitro(x => { x.ApiKey = "<>"; x.ApiId = "<>"; x.Stage = "dev"; }) .AddOpenTelemetry(); services .AddGraphQL() .AddQueryType() .AddInstrumentation(); // Enable GraphQL telemetry services .AddOpenTelemetry() .WithTracing(x => { x.AddHttpClientInstrumentation(); x.AddAspNetCoreInstrumentation(); // Register more instrumentation providers such as Entity Framework Core, HttpClient, etc. }); ``` Tip **Using Environment Variables** Alternatively, you can also set the required values using environment variables. This configuration enables your subgraph to interact with the Nitro services, including telemetry and instrumentation. #### Step 3: Create a Subgraph Configuration File Each subgraph requires a specific configuration file named `subgraph-config.json`. This file should be placed in the root directory of the subgraph project, next to the `.csproj` file. Here’s an example of what the `subgraph-config.json` file should look like: JSON ``` { "subgraph": "Order", // Name of the subgraph "http": { "baseAddress": "http://localhost:59093/graphql" }, // Default HTTP settings "extensions": { "nitro": { "apiId": "<>" } } } ``` This file is required for the topology to recognize and display your subgraph correctly. #### Step 4: Pack your subgraph and compose your Gateway After configuring your subgraph you have to `pack` your subgraph and `compose` your gateway. This process links your subgraph with the gateway, ensuring a cohesive GraphQL architecture. ### Integration into your CI/CD pipeline The deployment of a subgraph is a multi step process. To integrate Nitro into this process you need to install the Nitro CLI. You can find more information about Nitro CLI in the [Nitro CLI Documentation](https://chillicream.com/docs/nitro/cli/installation). Bash ``` dotnet new tool-manifest dotnet tool install ChilliCream.Nitro.CLI ``` You will also need the [Command Line Tools](https://www.nuget.org/packages/HotChocolate.Fusion.CommandLine) for packing and composing your subgraph. Bash ``` dotnet tool install HotChocolate.Fusion.CommandLine ``` #### 1\. Pack the subgraph All changes to the gateway originate from a subgraph. Once the subgraph is ready to be deployed, you need to pack it. Packing a subgraph will create a subgraph package file that contains the schema, the extensions and the configuration of the subgraph. To easily access the newest schema and extensions, you can use the `schema export` command from the [Command Line Extension](https://chillicream.com/docs/hotchocolate/server/command-line). This command exports your current schema into a specified output file. Bash ``` dotnet run -- schema export --output schema.graphql dotnet fusion subgraph pack ``` This step is usually done in a separate build step in your CI/CD pipeline where you build and test your project before you go into the deployment phase. #### 2\. Wait for a deployment slot Once your changes are ready to be deployed, you need to wait for a deployment slot. There can only ever be one deployment at the time. If there is already a deployment in progress, you need to wait until it is finished. Nitro helps you coordinate your subgraph deployments. You register for a deployment by calling: Bash ``` dotnet nitro fusion-configuration publish begin \ --stage <> \ --tag <> \ --api-id <> \ --subgraph-name <> \ --api-key <> ``` This command will complete once your turn has come and you can start deploying your subgraph. #### 3\. Start the deployment Once you have a deployment slot, you need to notify Nitro that you are still interested in the slot. You do this by calling: Bash ``` dotnet nitro fusion-configuration publish start --api-key <> ``` #### 4\. Configure the subgraph As most likely, your connection information is different from environment to environment, you need to configure the url of your subgraph. You can do this by calling: Bash ``` dotnet fusion subgraph config set http \ --url <> -c path/to/your/subgraph/config.fsp ``` #### 5\. Compose the subgraph To compose the subgraph, you first need to fetch the latest configuration from Nitro. You can do this by calling: Bash ``` dotnet nitro fusion-configuration download \ --api-id <> \ --stage <> \ --output-file ./gateway.fgp \ --api-key <> ``` This will download the latest configuration from Nitro and save it to the specified file (`gateway.fgp`). Now you can compose the subgraph by calling: Bash ``` dotnet fusion compose -p ./gateway.fgp -s path/to/your/subgraph/config.fsp ``` #### 6\. Validate the subgraph (optional) If you want to make sure that your subgraph is compatible with the schema and client registry, you can validate it by calling: Bash ``` dotnet nitro fusion-configuration publish validate --configuration ./gateway.fgp --api-key <> ``` In case the validation fails, you will get an error message. You have to cancel the deployment manually though. You can add deployment step to your CI/CD pipeline which will cancel the deployment if the validation fails by calling: Bash ``` dotnet nitro fusion-configuration publish cancel --api-key <> ``` #### 7\. Deploy the subgraph Now it's time to deploy your subgraph to your infrastructure #### 8\. Commit the deployment To complete the deployment, you need to commit the deployment. This will notify Nitro that you are done with the deployment and that the next deployment can start. Nitro will also notify your gateway that the deployment is finished and that it can pull the latest configuration. You can commit the deployment by calling: Bash ``` dotnet nitro fusion-configuration publish commit --configuration ./gateway.fgp --api-key <> ``` ## Distributed Telemetry ![Image](https://chillicream.com/images/nitro-docs/apis/fusion-4.webp)Nitro provides a distributed telemetry solution for your Fusion Gateway. It allows you to monitor your gateway and all your subgraphs in one place. You can inspect the traces of your operations on the gateway and see how they are executed on the subgraphs. To enable telemetry for your gateway and subgraphs, all of them need to be configured to send telemetry data to Nitro. Your subgraphs can be configured to send telemetry data by using the [ChilliCream.Nitro](https://www.nuget.org/packages/ChilliCream.Nitro/) package. You can find more information about how to configure your subgraphs in the [Open Telemetry](https://chillicream.com/docs/nitro/open-telemetry/operation-monitoring) guide. To send telemetry data from the gateway you need to add the instrumentation and the exporter to your gateway. C# ``` builder.Services .AddNitro() .AddOpenTelemetry(); builder .AddGraphQLGateway() .CoreBuilder .AddInstrumentation(); builder.Services .AddOpenTelemetry() .WithTracing(b => b .AddHttpClientInstrumentation() .AddAspNetCoreInstrumentation()); ``` Now your gateway will send the telemetry data to Nitro. To connect your subgraphs to the gateway, you need to add an extension to your `subgraph-config.json`. You need to specify the `apiId` of the subgraph JSON ``` { "subgraph": "Order", "http": { "baseAddress": "http://localhost:59093/graphql" }, "extensions": { "nitro": { "apiId": "QXBpCmc4ZjdhZTUxYjE5YTY0ZjFiYjcwNTc3NjJkMDkzOTg2Nw==" } } } ``` ## Cache The `ChilliCream.Nitro` package provides caching for persisted operations and fusion configurations, improving your system's resilience and performance. By first accessing a local cache for configurations before querying the server, your infrastructure becomes more robust, minimizing dependency on real-time server communications. This approach not only speeds up access to necessary configurations but also ensures your system remains stable and responsive, even during network fluctuations. We offer two types of caches: `FileSystemCache` for storing data on your local file system, and `BlobStorageCache` for storing data in Azure Blob Storage. Here’s how you add caching to your service: For GraphQL services: C# ``` services .AddNitro() .AddAssetCache(); services .AddGraphQL(); ``` For fusion services: C# ``` services .AddNitro() .AddAssetCache(); services .AddGraphQLGateway(); ``` ### `FileSystemCache` This default cache stores data in the `assets` folder of your project. You can change the folder like this: C# ``` services .AddNitro() .AddFileSystemAssetCache(x => { x.CacheDirectory = "cache"; // Your cache folder }); services .AddGraphQL(); ``` ### `BlobStorageCache` This cache stores your data in Azure Blob Storage. You need to install the `ChilliCream.Nitro.Azure` package: Bash ``` dotnet add package ChilliCream.Nitro.Azure ``` Set it up with: C# ``` services .AddNitro() .AddBlobStorageAssetCache(x => { x.ContainerName = "your-container-name"; x.Client = new BlobServiceClient( new Uri("https://yourblobstorage.blob.core.windows.net/"), new DefaultAzureCredential()); }); services .AddGraphQL(); ``` ### Custom `IAssetCache` If you need a specific cache setup, you can make your own by implementing the `IAssetCache`interface. This lets you decide how queries and configurations are cached according to your needs. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/apis/fusion.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Operation Reporting - Nitro > Nitro Operation Reporting gives insight into the GraphQL operations executed on your server; enable it with the `ChilliCream.Nitro` package and `AddNitro()`. Canonical source: https://chillicream.com/docs/nitro/apis/operation-reporting ![Image](https://chillicream.com/images/nitro-docs/apis/operation-reporting-0.webp) Nitro's Operation Reporting feature provides comprehensive insights into the GraphQL operations executed on your server. This functionality is essential for maintaining visibility over server activities, including both persisted and executed operations. By leveraging Operation Reporting, developers and system administrators can gain a clearer understanding of what operations are executed and available on the server. ## Enabling Operation Reporting Operation Reporting is an integrated feature in Nitro and is enabled by default when using Nitro services. To integrate these services into your project, the [ChilliCream.Nitro](https://www.nuget.org/packages/ChilliCream.Nitro/) NuGet package must be added. To install the Nitro services, run the following command in your project's root directory: Bash ``` dotnet add package ChilliCream.Nitro dotnet add package ChilliCream.Nitro.HotChocolate ``` After installing the package, you need to configure the services in your startup class. Below is a sample implementation in C#: C# ``` public void ConfigureServices(IServiceCollection services) { services .AddNitro(x => { x.ApiKey = "<>"; x.ApiId = "QXBpCmc5NGYwZTIzNDZhZjQ0NjBmYTljNDNhZDA2ZmRkZDA2Ng=="; x.Stage = "dev"; }) .AddDefaults(); services .AddGraphQL() .AddQueryType(); } ``` Tip **Using Environment Variables** Alternatively, you can set the required values using environment variables. This method allows you to call `AddNitro` without explicitly passing parameters. - `NITRO_API_KEY` maps to `ApiKey` - `NITRO_API_ID` maps to `ApiId` - `NITRO_STAGE` maps to `Stage` C# ``` public void ConfigureServices(IServiceCollection services) { services .AddNitro() .AddDefaults(); services .AddGraphQL() .AddQueryType(); } ``` In this setup, the API key, ID, and stage are set through environment variables. ## Viewing Reported Operations Once Operation Reporting is enabled and configured, all GraphQL operations processed by your server will be reported to Nitro. These operations can be viewed and analyzed in the `Operations` tab within the Nitro interface. ![Image](https://chillicream.com/images/nitro-docs/apis/operation-reporting-1.webp) 1. Click the `Operations` tab in Nitro to view the list of reported operations. 2. The name of the executed operation. Click to view the operation details. 3. The document ID of the persisted operation. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/apis/operation-reporting.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # APIs - Nitro > APIs in Nitro represent your GraphQL servers and group documents, settings, registries, and telemetry. Compare Collection, Service, and Gateway API types. Canonical source: https://chillicream.com/docs/nitro/apis/overview ![Image](https://chillicream.com/images/nitro-docs/apis/apis-0.webp)An API within the context of Nitro, refers to a representation of your GraphQL Servers. This representation is more than a mere conceptual framework — it serves as a practical tool that allows you to group your documents and share common settings like connection and authorization parameters among them. Additionally, an API forms the foundation for your client registry, schema registry setup and the telemetry. For more detailed information on these features, refer to the [Schema Registry](https://chillicream.com/docs/nitro/apis/schema-registry) guide the [Client Registry](https://chillicream.com/docs/nitro/apis/client-registry) and the [Telemetry](https://chillicream.com/docs/nitro/open-telemetry/operation-monitoring) guide. ## API Types ![Image](https://chillicream.com/images/nitro-docs/apis/apis-1.webp) ### API Collection ![Image](https://chillicream.com/images/nitro-docs/apis/apis-2.webp) A compilation of GraphQL Documents with shared connection settings, enabling the grouping of documents for sharing with your team. ### API Service ![Image](https://chillicream.com/images/nitro-docs/apis/apis-3.webp)Incorporates all features of an API Collection and adds the capability to register your schema and clients in the schema registry. It also includes the use of telemetry for service monitoring. This type is ideal for representing a single deployment service or a subgraph. ### API Gateway ![Image](https://chillicream.com/images/nitro-docs/apis/apis-4.webp)Encompasses all the features of the API Service, along with the ability to publish and manage fusion configuration. Additionally, it supports distributed telemetry for comprehensive monitoring of your Gateway. ## Creating an API ![Image](https://chillicream.com/images/nitro-docs/apis/apis-5.webp) Creating an API in Nitro is a user-friendly process. There are three methods available: 1. Click on the `New API` button located at the top of the document explorer toolbar. 2. Right-click within the document explorer and select `New API` from the context menu. This creates a new API within the currently selected folder. Note: APIs can be organized in folders but cannot be nested within each other. 3. Choose the API Type. Options include `API Collection`, `API Service`, or `API Gateway`. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/apis/overview.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Schema & Client Registry - Nitro > Store, version, and distribute GraphQL schemas with the Nitro schema registry, validating changes against clients to catch breaking changes in CI/CD. Canonical source: https://chillicream.com/docs/nitro/apis/schema-registry ![Image](https://chillicream.com/images/nitro-docs/apis/schema-registry-0.webp)The schema registries is an essential tool for managing your GraphQL APIs. It provides a centralized location for storing, managing, and distributing your GraphQL schema definitions. With the schema registry, you can upload and store the schema of your API, making it accessible to your development team and other services. ![Image](https://chillicream.com/images/nitro-docs/apis/schema-registry-1.webp)The schema registry enables you to validate your schemas and clients against previous versions, ensuring that changes to your service do not break existing functionality, deeply integrated into your CI/CD pipeline. ![Image](https://chillicream.com/images/nitro-docs/apis/schema-registry-2.webp)They also maintain a version history, allowing you to track changes over time and revert to previous versions if necessary. Together with the client registry, you can maintain the integrity of your API and the services that rely on it, ensuring that they can evolve together without breaking. ## Understanding Schemas In the context of GraphQL APIs, a schema is the blueprint that defines the shape of your data and specifies the capabilities of the API. It outlines the types of queries and mutations that can be executed against your API. A schema is more than just a technical specification; it is a contract between your API and your clients. By understanding and managing schema changes, you can ensure that this contract remains valid and that your API and clients can evolve together without breaking. Each stage of your API can have one active schema. This active schema is the one against which all requests are validated. ### Schema Changes Changes to the schema can be categorized into three levels of severity based on their potential impact on the clients: safe, dangerous, and breaking. 1. **Safe**: These changes don't affect the existing functionality. Examples include changes to descriptions or adding a new optional field to a type. Safe changes are generally backward compatible and don't require modifications to existing clients. Examples are: - Adding a new field to an object type - Adding a new optional argument to a field or directive - Adding a new type 1. **Dangerous**: These changes could potentially break existing functionality, depending on how your consumers interact with your API. An example of a dangerous change is adding a new member to an enum. If the client is not prepared to handle the new member, it might result in unexpected behavior. Examples are: - Adding a new member to an enum - Adding a new implementation to an interface 1. **Breaking**: These changes will break existing functionality if the affected parts of the schema are being used by clients. Examples include changing the type of a field, adding a required field to an input type, removing a field, or adding a new required argument to a field or directive. Examples are: - Removing a field from an object type - Changing the type of a field - Change a non-null field to a nullable field Breaking changes need to be managed with care to avoid disruptions to the service. It's important to ensure that all clients can handle these changes before they are introduced. This can be accomplished by versioning your clients and managing the lifecycle of client versions, as described in the section [Understanding Clients](https://chillicream.com/docs/nitro/apis/client-registry#understanding-clients)\]. ### Extracting the Schema Extracting your GraphQL API's schema can be beneficial for various purposes, such as documentation, testing, and version control. Here are some methods to extract the schema: #### Using Schema Export Command One of the simplest ways to extract the schema is by using the `schema export` command. This command exports your current schema into a specified output file. ``` dotnet run -- schema export --output schema.graphql ``` For more details about this command and how to setup the command line extension, please refer to the [Command Line Extension documentation](https://chillicream.com/docs/hotchocolate/server/command-line). #### Utilizing Snapshot Testing If you have already established snapshot testing in your workflow, you can use it to extract the schema. Snapshot tests compare the current schema against a previously saved one. If the schemas differ, the test fails, ensuring unintentional schema changes are detected. Additionally, keeping a snapshot test in the repository aids in visualizing schema changes in pull requests. Here is a sample snapshot test using [Snapshooter](https://github.com/SwissLife-OSS/snapshooter): C# ``` [Fact] public async Task Schema_Should_Not_Change() { // Arrange var executor = await new ServiceCollection() .AddGraphQL() .AddYourSchema() .BuildRequestExecutorAsync(); // Act var schema = executor.Schema.Print(); // Assert schema.MatchSnapshot(); } ``` ## Setting Up a Schema Registry To set up a schema registry, first, visit `nitro.chillicream.com` and sign up for an account. Next, you'll need to download and install Nitro CLI, the .NET tool used to manage your schema registry. You can find more information about Nitro CLI in the [Nitro CLI Documentation](https://chillicream.com/docs/nitro/cli/installation). After installing Nitro CLI, create a new API either through the Nitro App or the CLI. In the app, simply right-click the document explorer and select "New API." If you prefer using the CLI, ensure you're logged in with the command `nitro login`, then create a new API with the command `nitro api create`. With these steps complete, you are ready to start using the schema registry. To get the id of your API, use the command `nitro api list`. This command will list all of your APIs, their names, and their ids. You will need the id of your API to perform most operations on the schema registry. ## Integrating with Continuous Integration Integrating the schema registry and into your Continuous Integration/Continuous Deployment (CI/CD) pipeline maximizes their benefits. It ensures that the schemas in your API are always up-to-date and tested against potential breaking changes. To interact with the schema registry from the pipeline, you will need an API key. You can generate an api key directly with Nitro CLI using the command `nitro api-key create`. Make sure to copy the key and store it in a secure location. The key will not be displayed again. You can then use the key to authenticate with the schema registry using the `--api-key` option. The schema and client registries work hand-in-hand to ensure the smooth functioning of your API. As you make changes to your schema, the schema registry helps manage these changes, preventing inadvertent breaking changes and preserving a history of your schemas. As you validate, upload, and publish new schemas, the client registry ensures that your clients remain compatible with these changes. ### Understanding the Flow The general flow for the schema registry involves three main steps: validating the schema, uploading it to the registry, and publishing it. 1. **Validate the Schema**: The first step takes place during your Pull Request (PR) build. Here, you validate the schema against the API using the `nitro schema validate` command. This ensures that the schema is compatible with the API and will not break existing functionality. 2. **Upload the Schema**: The second step takes place during your release build. Here, you upload the schema to the registry using the `nitro schema upload` command. This command requires the `--tag` and `--api-id` options. The `--tag` option specifies the tag for the schema, and the `--api-id` option specifies the ID of the API to which you are uploading. This command create a new version of the schema with the specified tag. The tag is a string that can be used to identify the schema. It can be any string, but it is recommended to use a version number, such as `v1` or `v2`; or a commit hash, such as `a1b2c3d4e5f6g7h8i9j0k1l2m3n`. The tag is used to identify the schema when publishing it. 3. **Publish the Schema**: The third step takes place just before the release. Here, you publish the schema using the `nitro schema publish` command. This command requires the `--tag` and `--api-id` options. The `--tag` option specifies the tag for the schema, and the `--api-id` option specifies the ID of the API to which you are uploading. This command publishes the schema with the specified tag, making it the active version for the specified API. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/apis/schema-registry.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Stages - Nitro > Stages in Nitro model environments like dev, QA, and production: each holds an active schema and client versions, configured through a YAML stage editor. Canonical source: https://chillicream.com/docs/nitro/apis/stages ## Working with Stages ![Screenshot of the Stages overview](https://chillicream.com/images/nitro-docs/apis/stages-0.webp)A stage represents an environment of your service, such as development, staging, or production. Each stage can have an active schema and multiple active client versions. Stages are integral to the lifecycle management of your GraphQL APIs. They enable you to manage different environments of your service, such as development, staging, or production. Each stage can have an active schema, multiple active client versions and telemetry reports. The active schema and client versions for a stage represent the current state of your API for that environment. Stages in your development workflow can be arranged sequentially to represent progression of changes. For instance, in a simple flow like Development (Dev) - Quality Assurance (QA) - Production (Prod), each stage comes "after" the preceding one. This signifies that changes propagate from "Dev" to "QA", and finally to "Prod" ### Managing Stages If you do not have stages yet, you can go the the `Stages` tab and click on `Use Default Setup`. This will add a stage `Development` and a stage `Production` to your service. ![Screenshot of the Stages overview](https://chillicream.com/images/nitro-docs/apis/stages-1.webp)You can always edit these stages or add new ones by clicking the `Edit Stages` button. ![Screenshot of the Stages overview](https://chillicream.com/images/nitro-docs/apis/stages-2.webp) The stages dialog allows you to add and edit stages. You can also delete stages, but only if they are not used by any client or schema. Stages are edited in a yaml editor. The default configuration looks like this: YAML ``` dev: # define a stage by adding the identifier as a root node displayName: Development # add a display name to a stage prod: displayName: Production conditions: - after: dev # this defines the connection to other stages. production comes after development ``` You can easily create more complex stage configurations. For example, if you have two different QA stages, you can define them like this: YAML ``` dev: displayName: Development qa1: displayName: QA 1 conditions: - after: dev qa2: displayName: QA 2 conditions: - after: dev prod: displayName: Production conditions: - after: qa1 - after: qa2 ``` This configuration defines two QA stages, `QA 1` and `QA 2`. Both of them come after the `Development` stage. The `Production` stage comes after both QA stages. It will result in the following stage order: ![Image](https://chillicream.com/images/nitro-docs/apis/stages-3.webp) [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/apis/stages.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # api Command - Nitro > Manage Nitro APIs from the CLI with the `nitro api` commands: create service, gateway, or collection APIs in a workspace, list them, and inspect details. Canonical source: https://chillicream.com/docs/nitro/cli/api The `nitro api` commands manage APIs in a workspace. Each API has a kind that determines how it behaves: `service` for a single GraphQL service, `gateway` for a federated gateway, or `collection` for grouping related APIs together. All `api` commands require authentication. Run `nitro login` first or pass `--api-key` (see [Global Options](https://chillicream.com/docs/nitro/cli/global-options)). ## `nitro api create` Create a new API in a workspace. The path must start with `/` and uniquely identifies the API within its workspace. ``` nitro api create \ --name "" \ --path "" ``` ### Options | Option | Env | Description | | ------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------- | | \--name | NITRO\_API\_NAME | The name of the API. Required. | | \--path | NITRO\_API\_PATH | The path to the API. Must start with /. Required. | | \--workspace-id | NITRO\_WORKSPACE\_ID | ID of the workspace to create the API in. Falls back to the workspace from the current session. | | \--kind | NITRO\_API\_KIND | The kind of the API. One of collection, gateway, service. | ### Examples Create an API in the workspace from the current session: ``` nitro api create --name "" --path "/products" ``` Create an API in an explicit workspace: ``` nitro api create \ --name "" \ --path "/products" \ --workspace-id "" ``` Create a gateway API: ``` nitro api create \ --name "" \ --path "/products/catalog" \ --kind gateway ``` ## `nitro api list` List all APIs in a workspace. Results are paginated, use the returned cursor to fetch the next page. ``` nitro api list ``` ### Options | Option | Env | Description | | ------------------------------ | -------------------- | -------------------------------------------------------------------------- | | \--workspace-id | NITRO\_WORKSPACE\_ID | ID of the workspace. Falls back to the workspace from the current session. | | \--cursor | NITRO\_CURSOR | Pagination cursor to resume from. Useful for non-interactive paging. | ## `nitro api show` Show the details of a single API by its ID. ``` nitro api show "" ``` ### Arguments | Argument | Description | | -------- | -------------------------------- | | | ID of the API to show. Required. | ## `nitro api set-settings` Update the schema registry settings of an API. These settings control how breaking and dangerous schema changes are evaluated when publishing new schema versions. ``` nitro api set-settings "" \ --treat-dangerous-as-breaking \ --allow-breaking-schema-changes ``` ### Arguments | Argument | Description | | -------- | ---------------------------------- | | | ID of the API to update. Required. | ### Options | Option | Env | Description | | -------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------- | | \--treat-dangerous-as-breaking | NITRO\_TREAT\_DANGEROUS\_AS\_BREAKING | Treat dangerous schema changes as breaking. Required when running non-interactively. | | \--allow-breaking-schema-changes | NITRO\_ALLOW\_BREAKING\_SCHEMA\_CHANGES | Allow breaking schema changes when no published client breaks. Required when running non-interactively. | When run interactively, the CLI prompts for any setting you omit. ### Examples Treat dangerous changes as breaking and reject any breaking change: ``` nitro api set-settings "" \ --treat-dangerous-as-breaking true \ --allow-breaking-schema-changes false ``` Allow breaking changes when no client is affected: ``` nitro api set-settings "" \ --treat-dangerous-as-breaking true \ --allow-breaking-schema-changes true ``` ## `nitro api delete` Delete an API by its ID. This removes the API and all of its schema versions, clients, and stages. ``` nitro api delete "" ``` ### Arguments | Argument | Description | | -------- | ---------------------------------- | | | ID of the API to delete. Required. | ### Options | Option | Description | | -------- | -------------------------------------------------------------------------------------------------------------------------- | | \--force | Skip the confirmation prompt. Required when running non-interactively (for example in CI) or together with \--output json. | [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/api.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # api-key Command - Nitro > Create, list, and delete API keys with the `nitro api-key` commands: API- or workspace-scoped credentials for CI/CD pipelines and telemetry reporting. Canonical source: https://chillicream.com/docs/nitro/cli/api-key The `nitro api-key` commands manage API keys. API keys authenticate non-interactive use of the CLI and the Nitro server, and are intended for CI/CD pipelines, deployments, and telemetry reporting from your GraphQL server. A key is scoped either to a single API (via `--api-id`) or to an entire workspace (via `--workspace-id`). API-scoped keys can only operate on the API they were created for, workspace-scoped keys can operate on every API in the workspace. Optionally, an API key can additionally be restricted to a single stage with the `--stage-condition` option. This lets you issue, for example, a `dev`\-only key that cannot publish to `prod`. > If you need broader, user-level access (for example to automate workspace administration), use a [Personal Access Token](https://chillicream.com/docs/nitro/cli/pat) instead. All `api-key` commands require authentication. Run `nitro login` first or pass `--api-key` (see [Global Options](https://chillicream.com/docs/nitro/cli/global-options)). ## `nitro api-key create` Create a new API key. The secret is returned only once at creation time, store it in a secure location (for example a secret manager or a CI secret) before closing the terminal. ``` nitro api-key create \ --name "" \ --api-id "" ``` ### Options | Option | Env | Description | | ------------------------------------ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | \--name | NITRO\_API\_KEY\_NAME | Display name for the API key, used for later reference. Required. | | \--api-id | NITRO\_API\_ID | ID of the API to scope the key to. Required unless \--workspace-id is set. Get the ID from nitro api list or the API overview page in the Nitro UI. | | \--workspace-id | NITRO\_WORKSPACE\_ID | ID of the workspace to scope the key to. Falls back to the workspace from the current session. Required unless \--api-id is set. | | \--stage-condition | | _(Preview)_ Restrict the key to a single stage by name. If omitted, the key is valid for all stages. | When run interactively without `--api-id` or `--workspace-id`, the CLI prompts you to pick between an API-scoped or workspace-scoped key. ### Examples Create an API-scoped key: ``` nitro api-key create --name "" --api-id "" ``` Create a workspace-scoped key with an explicit workspace: ``` nitro api-key create --name "" --workspace-id "" ``` Restrict a key to a single stage: ``` nitro api-key create \ --name "" \ --api-id "" \ --stage-condition "" ``` Capture the secret in a script: ``` SECRET=$(nitro api-key create \ --name "" \ --api-id "" \ --output json | jq -r '.secret') ``` ## `nitro api-key list` List the API keys in a workspace. Results are paginated, use the returned cursor to fetch the next page. ``` nitro api-key list ``` ### Options | Option | Env | Description | | ------------------------------ | -------------------- | -------------------------------------------------------------------------- | | \--workspace-id | NITRO\_WORKSPACE\_ID | ID of the workspace. Falls back to the workspace from the current session. | | \--cursor | NITRO\_CURSOR | Pagination cursor to resume from. Useful for non-interactive paging. | ## `nitro api-key delete` Delete an API key by its ID. Once deleted, any client using the key loses access immediately. ``` nitro api-key delete "" ``` ### Arguments | Argument | Description | | -------- | -------------------------------------- | | | ID of the API key to delete. Required. | ### Options | Option | Description | | -------- | -------------------------------------------------------------------------------------------------------------------------- | | \--force | Skip the confirmation prompt. Required when running non-interactively (for example in CI) or together with \--output json. | [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/api-key.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # client Command - Nitro > Reference for the `nitro client` commands: register GraphQL clients, upload versions with their persisted operations, and publish them to a stage. Canonical source: https://chillicream.com/docs/nitro/cli/client The `nitro client` commands manage clients of an API. A client is a registered consumer of a GraphQL API (for example a web app, a mobile app, or another service) along with the set of operations it sends. A client owns a sequence of versions, each identified by a tag and containing a set of persisted operations. Versions are published to a stage to mark them as live. All `client` commands require authentication. Run `nitro login` first or pass `--api-key` (see [Global Options](https://chillicream.com/docs/nitro/cli/global-options)). ## `nitro client create` Create a new client under an API. ``` nitro client create \ --name "" \ --api-id "" ``` ### Options | Option | Env | Description | | ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | \--name | NITRO\_CLIENT\_NAME | Display name of the client. Required. | | \--api-id | NITRO\_API\_ID | ID of the API the client belongs to. Required when no workspace is set in the session. Get the ID from nitro api list or the Nitro UI. | ### Examples Create a client interactively (prompts for missing values): ``` nitro client create ``` Create a client non-interactively: ``` nitro client create --name "" --api-id "" ``` ## `nitro client upload` Upload a new client version with the operations the client sends. The version is identified by a tag and is not yet published to any stage. ``` nitro client upload \ --client-id "" \ --tag "" \ --operations-file ``` ### Options | Option | Env | Description | | ------------------------------------ | ----------------------- | ------------------------------------------------------------------------ | | \--client-id | NITRO\_CLIENT\_ID | ID of the client. Required. | | \--tag | NITRO\_TAG | Tag of the new client version, for example v1 or a Git commit. Required. | | \--operations-file | NITRO\_OPERATIONS\_FILE | Path to the JSON file with the persisted operations. Required. | ### Examples Upload a client version: ``` nitro client upload \ --client-id "" \ --tag "v1" \ --operations-file ./operations.json ``` ## `nitro client publish` Publish a previously uploaded client version to a stage. The version is identified by its tag. ``` nitro client publish \ --client-id "" \ --tag "" \ --stage "" ``` ### Options | Option | Env | Description | | ------------------------ | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | \--client-id | NITRO\_CLIENT\_ID | ID of the client. Required. | | \--tag | NITRO\_TAG | Tag of the client version to publish. Required. | | \--stage | NITRO\_STAGE | Name of the stage to publish to. Required. | | \--force | | Skip confirmation prompts and publish even when the version contains breaking operations. Mutually exclusive with \--wait-for-approval. | | \--wait-for-approval | NITRO\_WAIT\_FOR\_APPROVAL | Block the command until a reviewer approves the deployment. Mutually exclusive with \--force. Required when the stage gates deployments. | ### Examples Publish to `dev`: ``` nitro client publish \ --client-id "" \ --tag "v1" \ --stage "dev" ``` Publish to a gated stage and wait for approval: ``` nitro client publish \ --client-id "" \ --tag "v1" \ --stage "production" \ --wait-for-approval ``` ## `nitro client validate` Validate a new client version against a stage without publishing it. Run this in your pull request validation workflow to catch breaking operations before they are merged. ``` nitro client validate \ --client-id "" \ --stage "" \ --operations-file ``` ### Options | Option | Env | Description | | ------------------------------------ | ----------------------- | -------------------------------------------------------------- | | \--client-id | NITRO\_CLIENT\_ID | ID of the client. Required. | | \--stage | NITRO\_STAGE | Name of the stage to validate against. Required. | | \--operations-file | NITRO\_OPERATIONS\_FILE | Path to the JSON file with the persisted operations. Required. | ### Examples Validate against the `dev` stage: ``` nitro client validate \ --client-id "" \ --stage "dev" \ --operations-file ./operations.json ``` ## `nitro client unpublish` Unpublish one or more client version tags from a stage. The version is not deleted, only removed from the stage. ``` nitro client unpublish \ --client-id "" \ --stage "" \ --tag "" ``` ### Options | Option | Env | Description | | ------------------------ | ----------------- | ------------------------------------------------------------------------------------------------ | | \--client-id | NITRO\_CLIENT\_ID | ID of the client. Required. | | \--stage | NITRO\_STAGE | Name of the stage to unpublish from. Required. | | \--tag | NITRO\_TAG | Tag of the client version to unpublish. Pass multiple times to unpublish several tags. Required. | ### Examples Unpublish a single tag: ``` nitro client unpublish \ --client-id "" \ --stage "dev" \ --tag "" ``` Unpublish multiple tags in one call: ``` nitro client unpublish \ --client-id "" \ --stage "dev" \ --tag "v1" \ --tag "v2" ``` ## `nitro client download` Download all persisted operations of the client currently published to a stage. Writes either a single JSON file (Relay-style) or a directory with one `.graphql` file per operation. ``` nitro client download \ --api-id "" \ --stage "" \ --path ``` ### Options | Option | Env | Description | | --------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API. Required. | | \--stage | NITRO\_STAGE | Name of the stage to download from. Required. | | \--path | | Path to write the operations to. A file path for relay, a directory for folder. Required. | | \--format | | Output format. relay writes a single JSON map of id -> operation, folder writes one file per operation. Defaults to relay. | ### Examples Download Relay-style persisted operations: ``` nitro client download \ --api-id "" \ --stage "dev" \ --path ./operations.json ``` Download as a folder of `.graphql` files: ``` nitro client download \ --api-id "" \ --stage "dev" \ --path ./operations \ --format folder ``` ## `nitro client list` List all clients of an API. Results are paginated, use the returned cursor to fetch the next page. ``` nitro client list --api-id "" ``` ### Options | Option | Env | Description | | ------------------ | -------------- | -------------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API. Required when running non-interactively. | | \--cursor | NITRO\_CURSOR | Pagination cursor to resume from. Useful for non-interactive paging. | ## `nitro client list versions` List all versions of a client, including ones that have never been published to a stage. ``` nitro client list versions --client-id "" ``` ### Options | Option | Env | Description | | ------------------------ | ----------------- | -------------------------------------------------------------------- | | \--client-id | NITRO\_CLIENT\_ID | ID of the client. Required when running non-interactively. | | \--cursor | NITRO\_CURSOR | Pagination cursor to resume from. Useful for non-interactive paging. | ## `nitro client list published-versions` List the versions of a client that are currently published to a given stage. ``` nitro client list published-versions \ --client-id "" \ --stage "dev" ``` ### Options | Option | Env | Description | | ------------------------ | ----------------- | -------------------------------------------------------------------- | | \--client-id | NITRO\_CLIENT\_ID | ID of the client. Required when running non-interactively. | | \--stage | NITRO\_STAGE | Name of the stage to list published versions for. Required. | | \--cursor | NITRO\_CURSOR | Pagination cursor to resume from. Useful for non-interactive paging. | ## `nitro client show` Show the details of a single client by its ID. ``` nitro client show "" ``` ### Arguments | Argument | Description | | -------- | ----------------------------------- | | | ID of the client to show. Required. | ## `nitro client delete` Delete a client by its ID. This removes the client and all of its versions. ``` nitro client delete "" ``` ### Arguments | Argument | Description | | -------- | ---------------------------------------------------------------------------------------------------------------- | | | ID of the client to delete. Required when running non-interactively. Interactive runs prompt to select a client. | ### Options | Option | Description | | -------- | -------------------------------------------------------------------------------------------------------------------------- | | \--force | Skip the confirmation prompt. Required when running non-interactively (for example in CI) or together with \--output json. | [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/client.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # environment Command - Nitro > Manage workspace environments with the `nitro environment` commands: create, list, show, and delete variable groups reused across documents in the Nitro UI. Canonical source: https://chillicream.com/docs/nitro/cli/environment The `nitro environment` commands manage environments. An environment is a workspace-level grouping that holds a list of variables that can be reused across documents in the Nitro UI. All `environment` commands require authentication. Run `nitro login` first or pass `--api-key` (see [Global Options](https://chillicream.com/docs/nitro/cli/global-options)). ## `nitro environment create` Create a new environment in a workspace. ``` nitro environment create --name "" ``` ### Options | Option | Env | Description | | ------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------- | | \-n, --name | | Display name of the environment (for example dev, staging, production). Required. | | \--workspace-id | NITRO\_WORKSPACE\_ID | ID of the workspace to create the environment in. Falls back to the workspace from the current session. | When run interactively without `--name`, the CLI prompts for it. ### Examples Create an environment in the current workspace: ``` nitro environment create --name "" ``` Create an environment in a specific workspace: ``` nitro environment create \ --name "" \ --workspace-id "" ``` ## `nitro environment list` List all environments in a workspace. Results are paginated, use the returned cursor to fetch the next page. ``` nitro environment list ``` ### Options | Option | Env | Description | | ------------------------------ | -------------------- | -------------------------------------------------------------------------- | | \--workspace-id | NITRO\_WORKSPACE\_ID | ID of the workspace. Falls back to the workspace from the current session. | | \--cursor | NITRO\_CURSOR | Pagination cursor to resume from. Useful for non-interactive paging. | ## `nitro environment show` Show the details of an environment by its ID. ``` nitro environment show "" ``` ### Arguments | Argument | Description | | -------- | ---------------------------------------- | | | ID of the environment to show. Required. | [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/environment.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # fusion Command - Nitro > Reference for the `nitro fusion` commands: upload source schemas, publish composed Fusion gateway configurations to a stage, and validate them in CI/CD. Canonical source: https://chillicream.com/docs/nitro/cli/fusion The `nitro fusion` commands manage [Fusion](https://chillicream.com/docs/fusion) configurations. A Fusion configuration is the composed gateway artifact built from one or more source schemas. Once published to a stage, the gateway loads it and starts serving the federated graph. Note For using these commands in CI/CD pipelines (uploading source schemas, publishing configurations, validating pull requests), see [Deployment and CI/CD](https://chillicream.com/docs/fusion/deployment-and-ci-cd). ## `nitro fusion upload` Upload a source schema for a later composition. The schema is stored on the Nitro backend under the given API and tag and can be referenced by name from a subsequent `nitro fusion publish` call (via `--source-schema`). ``` nitro fusion upload \ --api-id "" \ --tag "" \ --source-schema-file "" ``` ### Options | Option | Env | Description | | ---------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API. Required. | | \--tag | NITRO\_TAG | Tag of the schema version being uploaded (for example a Git commit SHA or release tag). Required. | | \-f, --source-schema-file | | Path to a source schema file (.graphqls) or to a directory that contains one. Required. | | \-w, --working-directory | | Working directory for the command. Used for relative paths. | > `--source-schema-file` accepts either a schema file or a directory. In both cases, a `schema-settings.json` file is expected to sit next to the schema file (when a directory is given, both files must be inside that directory). ### Examples Upload a single source schema: ``` nitro fusion upload \ --api-id "" \ --tag "v1" \ --source-schema-file ./products/schema.graphqls ``` ## `nitro fusion publish`16.6.0+Nitro 10.3.0+ Publish a Fusion configuration to a stage. ``` nitro fusion publish \ --api-id "" \ --stage "" \ --tag "" \ --source-schema products ``` ### Options | Option | Env | Description | | ---------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API. Required. | | \--tag | NITRO\_TAG | Tag of the schema version to deploy (for example a Git commit SHA or release tag). Required. | | \--stage | NITRO\_STAGE | Name of the stage to publish to. Required. | | \-s, --source-schema | | One or more source schemas to include in the composition. Each value is either a name (example) or a name plus version (example@1.0.0). When the version is omitted, the value of \--tag is used. | | \-f, --source-schema-file | | One or more paths to a source schema file (.graphqls) or to a directory that contains one. | | \-a, --archive | NITRO\_FUSION\_CONFIG\_FILE | Path to a Fusion archive file. The \--configuration alias is deprecated. | | \--legacy-v1-archive | | Path to a Fusion v1 archive file. Only intended for use during the migration from Fusion v1 to Fusion v2+. | | \--force | | Skip confirmation prompts for deletes and overwrites. | | \--wait-for-approval | NITRO\_WAIT\_FOR\_APPROVAL | Block the command until a reviewer approves the deployment. Required when the stage gates deployments. | | \-w, --working-directory | | Working directory for the command. Used for resolving relative paths and auto-discovering source schema files. | > `--source-schema-file` accepts either a schema file or a directory. In both cases, a `schema-settings.json` file is expected to sit next to the schema file (when a directory is given, both files must be inside that directory). ### Examples Compose and publish from previously uploaded source schemas: ``` nitro fusion publish \ --api-id "" \ --stage "dev" \ --tag "v1" \ --source-schema products \ --source-schema reviews ``` Compose and publish from local source schema files in one step: ``` nitro fusion publish \ --api-id "" \ --stage "dev" \ --tag "v1" \ --source-schema-file ./products/schema.graphqls \ --source-schema-file ./reviews/schema.graphqls ``` Publish a pre-composed archive: ``` nitro fusion publish \ --api-id "" \ --stage "dev" \ --tag "v1" \ --archive ./gateway.far ``` ## Advanced: multi-step publish > Reach for these commands only when `nitro fusion publish` cannot model your pipeline, for example when validation must run in one CI job and the deploy must run in a separate, manually approved job. For everything else, prefer `nitro fusion publish`. The subcommands below split the same flow into individual steps, which is more error-prone and harder to monitor. A multi-step publish is driven by a single request ID. `begin` allocates a deployment slot and prints a request ID, every following step references that ID (either explicitly via `--request-id` or implicitly via local state that the CLI caches between commands in the same job). The standard order is `begin` → `start` → `validate` → `commit`. `cancel` releases the slot at any time before `commit`. ### `nitro fusion publish begin` Begin a Fusion configuration publish by requesting a deployment slot for a stage. The returned request ID identifies the publish for every subsequent step. ``` nitro fusion publish begin \ --api-id "" \ --tag "" \ --stage "" ``` | Option | Env | Description | | -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------ | | \--api-id | NITRO\_API\_ID | ID of the API. Required. | | \--tag | NITRO\_TAG | Tag of the schema version to deploy. Required. | | \--stage | NITRO\_STAGE | Name of the stage to publish to. Required. | | \--wait-for-approval | NITRO\_WAIT\_FOR\_APPROVAL | Block the command until a reviewer approves the deployment. Required when the stage gates deployments. | ### `nitro fusion publish start` Mark the publish as started. After this step the deployment is in flight and the configuration is being applied to the gateway. ``` nitro fusion publish start --request-id "" ``` | Option | Env | Description | | -------------------------- | ------------------ | --------------------------------------------------------------------------------------------------- | | \--request-id | NITRO\_REQUEST\_ID | Request ID returned by begin. Falls back to the cached ID from the previous step in the same shell. | ### `nitro fusion publish validate` Validate a composed Fusion archive against everything currently published to the stage targeted by the request. ``` nitro fusion publish validate \ --request-id "" \ --archive "" ``` | Option | Env | Description | | -------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------- | | \--request-id | NITRO\_REQUEST\_ID | Request ID returned by begin. Falls back to the cached ID from the previous step in the same shell. | | \-a, --archive | NITRO\_FUSION\_CONFIG\_FILE | Path to the Fusion archive to validate. Required. The \--configuration alias is deprecated. | ### `nitro fusion publish commit` Commit the Fusion configuration, finalizing the publish. After this step the new configuration is live on the stage. ``` nitro fusion publish commit \ --request-id "" \ --archive "" ``` | Option | Env | Description | | -------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------- | | \--request-id | NITRO\_REQUEST\_ID | Request ID returned by begin. Falls back to the cached ID from the previous step in the same shell. | | \-a, --archive | NITRO\_FUSION\_CONFIG\_FILE | Path to the Fusion archive to commit. Required. The \--configuration alias is deprecated. | ### `nitro fusion publish cancel` Cancel an in-progress publish and release the deployment slot. Run this from the failure branch of any job between `begin` and `commit`. ``` nitro fusion publish cancel --request-id "" ``` | Option | Env | Description | | -------------------------- | ------------------ | --------------------------------------------------------------------------------------------------- | | \--request-id | NITRO\_REQUEST\_ID | Request ID returned by begin. Falls back to the cached ID from the previous step in the same shell. | ## `nitro fusion validate` Validate a Fusion configuration against a stage. Composes the supplied source schemas (or uses a pre-composed archive) and runs the same checks as `publish` without requesting a deployment slot. ``` nitro fusion validate \ --api-id "" \ --stage "" \ --archive "" ``` ### Options | Option | Env | Description | | ---------------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API. Required. | | \--stage | NITRO\_STAGE | Name of the stage to validate against. Required. | | \-a, --archive | NITRO\_FUSION\_CONFIG\_FILE | Path to a pre-composed Fusion archive file. The \--configuration alias is deprecated. | | \--legacy-v1-archive | | Path to a Fusion v1 archive file. Only intended for use during the migration from Fusion v1 to Fusion v2+. | | \-f, --source-schema-file | | One or more paths to a source schema file (.graphqls) or to a directory that contains one. | > `--source-schema-file` accepts either a schema file or a directory. In both cases, a `schema-settings.json` file is expected to sit next to the schema file (when a directory is given, both files must be inside that directory). ### Examples Validate by composing source schemas on the fly: ``` nitro fusion validate \ --api-id "" \ --stage "dev" \ --source-schema-file ./products/schema.graphqls \ --source-schema-file ./reviews/schema.graphqls ``` Validate a pre-composed archive: ``` nitro fusion validate \ --api-id "" \ --stage "dev" \ --archive ./gateway.far ``` ## `nitro fusion download` Download the most recent gateway configuration of a stage to a local archive file. ``` nitro fusion download \ --api-id "" \ --stage "" \ --output-file "" ``` ### Options | Option | Env | Description | | ---------------------------- | ------------------- | ----------------------------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API. Required. | | \--stage | NITRO\_STAGE | Name of the stage to download from. Required. | | \--version | | Version of the archive format to request. Defaults to the latest archive version. | | \--output-file | NITRO\_OUTPUT\_FILE | File path to write the archive to. When omitted, the archive is streamed to stdout. | ### Examples Download the live `dev` configuration: ``` nitro fusion download \ --api-id "" \ --stage "dev" \ --output-file ./gateway.far ``` ## `nitro fusion compose` Compose multiple source schemas into a single composite schema and write the result to a Fusion archive. When `--archive` points at an existing archive, `compose` works incrementally: source schemas already in the archive are carried forward, a `--source-schema-file` whose name matches an existing source schema overrides it, and `--remove-source-schema` drops one. So removing a source schema is `--remove-source-schema `, and replacing or renaming one is `--remove-source-schema ` together with `--source-schema-file `. ``` nitro fusion compose \ --source-schema-file "" \ --archive "" ``` ### Options | Option | Env | Description | | ---------------------------------------------------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \-f, --source-schema-file | | One or more paths to a source schema file (.graphqls) or to a directory that contains one. When omitted, source schemas are auto-discovered from the working directory. | | \--source-schema-url | | URL from which to download a source schema. Repeat once per remote source. | | \--source-schema-settings-file | | Settings file paired by occurrence with \--source-schema-url. Repeat once per remote source. | | \-a, --archive | NITRO\_FUSION\_CONFIG\_FILE | Path to the output Fusion archive file. The \--configuration alias is deprecated. | | \-e, --env, --environment | | Name of the environment used for value substitution in schema-settings.json files. | | \--cache-control-merge-behavior | | Choose how @cacheControl directives are merged. | | \--enable-global-object-identification | | Add the Query.node field for global object identification. | | \--enum-values-merge-behavior | | Choose how enum values are merged across source schemas. | | \--node-resolution | | Choose whether Query.node identifiers are resolved by the gateway or a source schema. | | \--tag-merge-behavior | | Choose how @tag directives are merged. | | \--shareable-field-runtime-type-routing | | Choose how runtime types are routed for Apollo Federation shareable abstract fields. | | \--allow-non-resolvable-interface-objects | | Allow Apollo Federation interface objects without a resolvable key. | | \--include-satisfiability-paths | | Include paths in satisfiability error messages to make composition errors easier to diagnose. | | \--remove-source-schema | | One or more source schemas to remove from the archive before composing. Cannot be combined with \--watch. | | \--watch | | Watch source files for changes and recompose automatically. | | \-w, --working-directory | | Working directory for the command. Used for relative paths and source schema auto-discovery. | | \--exclude-by-tag | | One or more tags to exclude from the composition. | > `--source-schema-file` accepts either a schema file or a directory. In both cases, a `schema-settings.json` file is expected to sit next to the schema file (when a directory is given, both files must be inside that directory). Local schema files do not use `--source-schema-settings-file`. For a local file, Nitro derives the companion settings path from the schema file. For example, `./inventory/schema.graphqls` uses `./inventory/schema-settings.json`. For remote sources, repeat `--source-schema-url` and `--source-schema-settings-file` the same number of times. Nitro pairs them by occurrence: the first URL uses the first settings file, the second URL uses the second settings file, and so on. Keep each pair adjacent so the relationship remains visible in scripts. The paired settings file selects the acquisition protocol. An absent `apolloFederationSupport` marker makes Nitro GET raw SDL from the exact URL. Exact `"1.0"` and `"2.0"` markers make Nitro POST an Apollo `_service { sdl }` query. See [Getting the Subgraph Schema](https://chillicream.com/docs/fusion/connectors/apollofederation#getting-the-subgraph-schema) for the settings shape and protocol details. ### Examples Compose a gateway from two source schemas: ``` nitro fusion compose \ --source-schema-file ./products/schema.graphqls \ --source-schema-file ./reviews/schema.graphqls \ --archive ./gateway.far \ --env "dev" ``` Compose two remote schemas and one local schema: ``` nitro fusion compose \ --source-schema-url https://products.example.com/graphql \ --source-schema-settings-file ./products/schema-settings.json \ --source-schema-url https://reviews.example.com/graphql \ --source-schema-settings-file ./reviews/schema-settings.json \ --source-schema-file ./inventory/schema.graphqls \ --archive ./gateway.far ``` After a successful composition, Nitro prints: ``` ✅ Composite schema written to '/absolute/path/to/gateway.far'. ``` Auto-discover source schemas from a working directory: ``` nitro fusion compose \ --working-directory ./subgraphs \ --archive ./gateway.far ``` Remove a source schema and recompose: ``` nitro fusion compose \ --archive ./gateway.far \ --remove-source-schema reviews ``` Replace or rename a source schema (drop the old, add the new): ``` nitro fusion compose \ --archive ./gateway.far \ --remove-source-schema reviews \ --source-schema-file ./reviews-v2/schema.graphqls ``` In watch mode, Nitro observes local schema directories and paired remote settings files. A watched change triggers recomposition and fetches the remote schemas again. Nitro does not poll remote URLs. ## `nitro fusion settings set` Set a Fusion composition setting on a Fusion archive. Use this to flip composition-level toggles after a composition has been produced, without recomposing from sources. ``` nitro fusion settings set \ --archive "" ``` ### Arguments | Argument | Description | | ---------------- | -------------------------------------------- | | | Name of a setting listed in the table below. | | | New value for the setting. Required. | ### Options | Option | Env | Description | | --------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------- | | \-a, --archive | NITRO\_FUSION\_CONFIG\_FILE | Path to the Fusion archive file to update. Required. The \--configuration alias is deprecated. | | \-e, --env, --environment | | Name of the environment used for value substitution in schema-settings.json files. | ### Available Settings | Setting | Values | Description | | -------------------------------------- | ---------------------------------- | ------------------------------------------------------------------- | | allow-non-resolvable-interface-objects | true, false | Allow Apollo interface objects without a resolvable key. | | cache-control-merge-behavior | ignore, include, include-private | Choose how @cacheControl directives are merged. | | enum-values-merge-behavior | auto, strict, union | Choose how enum values are merged across source schemas. | | exclude-by-tag | Comma-separated tags | Exclude fields and types by tag. | | global-object-identification | true, false | Enable global object identification through Query.node. | | include-satisfiability-paths | true, false | Include paths in satisfiability diagnostics. | | node-resolution | gateway, source-schema | Choose who resolves Query.node identifiers. | | shareable-field-runtime-type-routing | source-local, common-runtime-types | Choose routing for type-conditioned selections on shareable fields. | | tag-merge-behavior | ignore, include, include-private | Choose how @tag directives are merged. | ### Examples Enable global object identification on an archive: ``` nitro fusion settings set global-object-identification "true" \ --archive ./gateway.far \ --env "dev" ``` After a successful update, Nitro prints: ``` Composed new configuration. ``` For examples of node resolution, shareable runtime type routing, and tag exclusion, see [Fusion CLI](https://chillicream.com/docs/fusion/cli#nitro-fusion-settings-set). ## `nitro fusion source-schema init` Create the `schema-settings.json` file that a source schema needs for composition. The settings file is written next to the schema file it belongs to, under the name composition looks for, so `nitro fusion compose` picks it up without further configuration. Running the command against an existing settings file updates the values you pass and preserves every other setting in the file, which makes it safe to re-run from a pipeline. ``` nitro fusion source-schema init [options] ``` ### Options | Option | Env | Description | | -------------------------------- | -------------- | ------------------------------------------------------------------------------------------- | | \--name | | Name that identifies the source schema in the composite schema. Required for a new file. | | \-f, --source-schema-file | | Source schema file (.graphqls), or a directory containing one, that the settings belong to. | | \--settings-file | | Write the settings to this path instead of deriving it from the schema file. | | \--url | | URL the gateway uses to reach the source schema. Required for a new file. | | \--dev-url | | URL a local development environment uses to reach the source schema. | | \--client-name | | Name of the HTTP client the gateway uses to reach the source schema. | | \--api-id | NITRO\_API\_ID | Nitro Cloud API identifier, written to extensions.nitro.apiId. | | \--schema-type | | graphql-federation, apollo-federation-1, or apollo-federation-2. | | \--variable-batching | | Whether the source schema supports variable batching. Defaults to false for a new file. | | \--request-batching | | Whether the source schema supports request batching. Defaults to false for a new file. | | \--alias-batching | | Whether the source schema supports alias batching. Defaults to true for a new file. | | \--batching-format ... | | One or more response media types supported for batching, such as application/jsonl. | | \-w, --working-directory | | Working directory for the command. | `--name` and `--url` are only required when the settings file does not exist yet, since an existing file already carries both. On an interactive terminal the command asks for whichever of the two you did not pass. Everywhere else, including CI, omitting one fails rather than guessing a value. New settings default to GraphQL Federation. An interactive terminal asks for the schema type and preselects GraphQL Federation. It also asks whether variable, request, and alias batching are supported. Existing settings keep their schema type unless `--schema-type` is passed. Both `--url` and `--dev-url` accept `{{VARIABLE_NAME}}` placeholders, which composition resolves against the [environments](https://chillicream.com/docs/fusion/cli#environments) section of the settings file. ### Source schema types `--schema-type` describes the specification implemented by the source schema. | Type | What it declares | | ------------------- | ---------------------------------------------------------------------------------------------------------- | | graphql-federation | GraphQL Federation semantics from the Composite Schemas Specification. This is the default used by Fusion. | | apollo-federation-1 | Apollo Federation v1 semantics through extensions.chillicream.apolloFederationSupport.version. | | apollo-federation-2 | Apollo Federation v2 semantics through extensions.chillicream.apolloFederationSupport.version. | Selecting an Apollo Federation type writes the corresponding `1.0` or `2.0` marker. Selecting `graphql-federation` removes that marker. Schema-type changes do not modify transport capabilities. ### Batching capabilities New settings declare the following batching capabilities: JSON ``` { "variableBatching": false, "requestBatching": false, "aliasBatching": true } ``` Pass the corresponding batching option with `true` or `false` to override a value. Pass one or more media types to `--batching-format` to write the `formats` array. When updating an existing file, omitted batching options preserve their current values. In interactive mode, pressing Enter accepts `false` for variable batching, `false` for request batching, and `true` for alias batching. Batching response formats can only be configured explicitly with `--batching-format`. > A source schema configured with an Apollo Federation type that composition fetches over HTTP (via `--source-schema-url` on `nitro fusion compose`) is retrieved through Apollo Federation's `_service` field. A source schema read from a local file is read as it is on disk, and the marker only affects directive semantics. ### Where the file is written The target path is resolved in this order: 1. `--settings-file`, when given. 2. Next to `--source-schema-file` as `-settings.json`. A path that is not named like a schema file counts as a directory, whether or not it exists yet: the schema file inside it determines the name, and `schema-settings.json` is used when it holds none. 3. `schema-settings.json` in the working directory. ### Examples Create settings for a subgraph next to its schema file: ``` nitro fusion source-schema init \ --name "products" \ --source-schema-file ./products/schema.graphqls \ --url "https://products.example.com/graphql" ``` This writes `./products/schema-settings.json`: JSON ``` { "name": "products", "transports": { "http": { "url": "https://products.example.com/graphql", "capabilities": { "batching": { "variableBatching": false, "requestBatching": false, "aliasBatching": true } } } } } ``` Create settings for an Apollo Federation subgraph: ``` nitro fusion source-schema init \ --name "reviews" \ --source-schema-file ./reviews/schema.graphqls \ --url "https://reviews.example.com/graphql" \ --schema-type apollo-federation-2 ``` Point an existing settings file at a new URL without touching its other settings: ``` nitro fusion source-schema init \ --source-schema-file ./products/schema.graphqls \ --url "https://products.staging.example.com/graphql" ``` ## `nitro fusion run` Start a Fusion gateway locally with the specified archive. Useful for smoke-testing a composed archive before publishing. Only supports Fusion v2. ``` nitro fusion run "" ``` ### Arguments | Argument | Description | | --------------- | ------------------------------------------------- | | | Path to the Fusion archive file to run. Required. | ### Options | Option | Description | | ------------------ | ------------------------------------ | | \-p, --port | The port the gateway will listen on. | ### Examples Run a gateway on port 5000: ``` nitro fusion run ./gateway.far --port 5000 ``` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/fusion.md) Maintained by ChilliCream. Last updated on **August 24, 2026** by **Michael Staib** --- # Global Options - Nitro > Global options every Nitro CLI command accepts: `--api-key` for non-interactive auth, `--output json` for scripting, and `--cloud-url` for self-hosted setups. Canonical source: https://chillicream.com/docs/nitro/cli/global-options ## `-?, -h, --help` Show help and usage information for the command. Use this on any subcommand to see its options, environment variables, and an example invocation. ## `--api-key ` API key or Personal Access Token used to authenticate non-interactive CLI calls. Pass either an API key created via [nitro api-key create](https://chillicream.com/docs/nitro/cli/api-key) or a PAT created via [nitro pat create](https://chillicream.com/docs/nitro/cli/pat). Set via the `NITRO_API_KEY` environment variable. For interactive use, prefer `nitro login` over passing this flag. ## `--output ` Switches the CLI's output format. The only supported value is `json`, which renders a structured JSON document instead of the default human-readable output. Setting `--output json` also enables non-interactive mode: prompts are disabled and any missing required input results in an error instead of an interactive question. Use this in CI, scripts, and any pipeline that needs to parse CLI output. Set via the `NITRO_OUTPUT_FORMAT` environment variable. ## `--cloud-url ` URL of the Nitro backend the CLI talks to. Only needed for self-hosted or dedicated deployments, the public ChilliCream Cloud is the default. Set via the `NITRO_CLOUD_URL` environment variable. Defaults to `api.chillicream.com`. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/global-options.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Installation - Nitro > Install the Nitro CLI as a .NET tool, via npm with `@chillicream/nitro`, through Homebrew, or as pre-built binaries from the GitHub releases page. Canonical source: https://chillicream.com/docs/nitro/cli/installation The Nitro CLI ships in several flavors so you can pick whatever fits your environment best. ## .NET tool If you have the .NET SDK installed, you can install the CLI as a [.NET tool](https://learn.microsoft.com/dotnet/core/tools/global-tools). Install as a local tool, scoped to a repository via a tool manifest. From the repository root: ``` dotnet new tool-manifest dotnet tool install ChilliCream.Nitro.CommandLine ``` Local tools are restored with `dotnet tool restore` and invoked through `dotnet tool run nitro` (or `dotnet nitro`). Check the manifest (`./.config/dotnet-tools.json`) into source control so every collaborator uses the same version. Or install globally: ``` dotnet tool install -g ChilliCream.Nitro.CommandLine ``` ## npm The CLI is published to npm as [@chillicream/nitro](https://www.npmjs.com/package/@chillicream/nitro). For one-off invocations run it with `npx`. The `@latest` tag opts out of npm's local cache so each run pulls the newest release: ``` npx @chillicream/nitro@latest --version ``` ## Homebrew (macOS and Linux) The CLI is available through the [chillicream/tools](https://github.com/ChilliCream/homebrew-tools) tap: ``` brew tap chillicream/tools brew install nitro-cli ``` To upgrade later: ``` brew update brew upgrade nitro-cli ``` ## Pre-built binaries Pre-built binaries for every supported OS and architecture are attached to each [GitHub release](https://github.com/ChilliCream/graphql-platform/releases). | Platform | Asset | | --------------------------- | --------------------------- | | Linux x64 | nitro-linux-x64.tar.gz | | Linux x64 (musl, Alpine) | nitro-linux-musl-x64.tar.gz | | Linux arm64 | nitro-linux-arm64.tar.gz | | macOS x64 (Intel) | nitro-osx-x64.zip | | macOS arm64 (Apple Silicon) | nitro-osx-arm64.zip | | Windows x64 | nitro-win-x64.zip | | Windows x86 | nitro-win-x86.zip | Extract the archive and place the `nitro` binary somewhere on your `PATH`. The binaries are self-contained, no .NET SDK or runtime is required on the target machine. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/installation.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # launch Command - Nitro > The `nitro launch` command opens the Nitro web UI in your default browser, giving you quick access to the control plane straight from your terminal. Canonical source: https://chillicream.com/docs/nitro/cli/launch The `nitro launch` command opens the Nitro web UI in your default browser. ``` nitro launch ``` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/launch.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # login Command - Nitro > Authenticate the Nitro CLI interactively with `nitro login`: sign in through the browser, pick a default workspace, and persist the session locally. Canonical source: https://chillicream.com/docs/nitro/cli/login The `nitro login` command logs you in interactively through your default browser. After authenticating, the CLI prompts you to select a default workspace (skipped when you only have one) and persists the session locally so subsequent commands don't need `--api-key`. For non-interactive environments such as CI/CD, skip `nitro login` entirely and authenticate per-invocation with `--api-key` instead (see [Global Options](https://chillicream.com/docs/nitro/cli/global-options)). ``` nitro login ``` ## Arguments | Argument | Description | | -------- | ------------------------------------------------------------------------------- | | | URL of the Nitro backend. Only needed for self-hosted or dedicated deployments. | ## Examples Log in against the default Nitro Cloud: ``` nitro login ``` Log in against a self-hosted or dedicated deployment using the positional argument: ``` nitro login "" ``` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/login.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # logout Command - Nitro > The `nitro logout` command ends your Nitro CLI session and removes locally stored credentials; afterwards commands need `nitro login` or an `--api-key`. Canonical source: https://chillicream.com/docs/nitro/cli/logout The `nitro logout` command logs you out and removes the locally stored session information. After logout, subsequent commands either need a fresh [nitro login](https://chillicream.com/docs/nitro/cli/login) or an explicit `--api-key`. ``` nitro logout ``` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/logout.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # mcp Command - Nitro > Manage MCP feature collections with the `nitro mcp` commands: create a collection, upload versioned prompts and tools, validate, and publish to a stage. Canonical source: https://chillicream.com/docs/nitro/cli/mcp The `nitro mcp` commands manage MCP feature collections. An MCP feature collection bundles a versioned set of prompt and tool definitions that HotChocolate (Fusion) serves to MCP clients on a given stage. A typical workflow is: `create` a collection on an API, `upload` a new version of its prompts and tools, optionally `validate` that version against a stage, then `publish` it. > Validation runs automatically as part of `nitro mcp publish`. Use `nitro mcp validate` only when you need to gate a deploy step in a separate pipeline job. All `mcp` commands require authentication. Run `nitro login` first or pass `--api-key` (see [Global Options](https://chillicream.com/docs/nitro/cli/global-options)). ## `nitro mcp create` Create a new MCP feature collection on an API. ``` nitro mcp create \ --name "" \ --api-id "" ``` ### Options | Option | Env | Description | | ------------------ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | \--name | NITRO\_MCP\_FEATURE\_COLLECTION\_NAME | Display name of the MCP feature collection. Required. | | \--api-id | NITRO\_API\_ID | ID of the API the collection belongs to. Required when no workspace is set in the session. Get the ID from nitro api list or the Nitro UI. | ### Examples Create a collection interactively (prompts for missing values): ``` nitro mcp create ``` Create a collection non-interactively: ``` nitro mcp create --name "" --api-id "" ``` ## `nitro mcp upload` Upload a new version of an MCP feature collection. Prompt and tool definition files are picked up via glob patterns. ``` nitro mcp upload \ --mcp-feature-collection-id "" \ --tag "" \ --prompt-pattern "" \ --tool-pattern "" ``` ### Options | Option | Env | Description | | -------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------ | | \--mcp-feature-collection-id | NITRO\_MCP\_FEATURE\_COLLECTION\_ID | ID of the MCP feature collection. Required. | | \--tag | NITRO\_TAG | Tag of the version being uploaded (for example a Git commit SHA or release tag). Required. | | \-p, --prompt-pattern | | One or more glob patterns for locating MCP prompt definition files (\*.json). | | \-t, --tool-pattern | | One or more glob patterns for locating MCP tool definition files (\*.graphql). | ### Examples Upload prompts and tools from the default folders: ``` nitro mcp upload \ --mcp-feature-collection-id "" \ --tag "v1" \ --prompt-pattern "./prompts/**/*.json" \ --tool-pattern "./tools/**/*.graphql" ``` ## `nitro mcp publish` Publish a previously uploaded MCP feature collection version to a stage. The version is identified by its tag. ``` nitro mcp publish \ --mcp-feature-collection-id "" \ --tag "" \ --stage "" ``` ### Options | Option | Env | Description | | -------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | \--mcp-feature-collection-id | NITRO\_MCP\_FEATURE\_COLLECTION\_ID | ID of the MCP feature collection. Required. | | \--tag | NITRO\_TAG | Tag of the version to publish. Required. | | \--stage | NITRO\_STAGE | Name of the stage to publish to. Required. | | \--force | | Skip confirmation prompts for deletes and overwrites. Mutually exclusive with \--wait-for-approval. | | \--wait-for-approval | NITRO\_WAIT\_FOR\_APPROVAL | Block the command until a reviewer approves the deployment. Mutually exclusive with \--force. Required when the stage gates deployments. | ### Examples Publish to `dev`: ``` nitro mcp publish \ --mcp-feature-collection-id "" \ --stage "dev" \ --tag "v1" ``` Publish to a gated stage and wait for approval: ``` nitro mcp publish \ --mcp-feature-collection-id "" \ --stage "production" \ --tag "v1" \ --wait-for-approval ``` ## `nitro mcp validate` Validate a new MCP feature collection version against a stage without publishing it. Run this in your pull request validation workflow to catch breaking changes before they are merged. ``` nitro mcp validate \ --mcp-feature-collection-id "" \ --stage "" \ --prompt-pattern "" \ --tool-pattern "" ``` ### Options | Option | Env | Description | | -------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------ | | \--mcp-feature-collection-id | NITRO\_MCP\_FEATURE\_COLLECTION\_ID | ID of the MCP feature collection. Required. | | \--stage | NITRO\_STAGE | Name of the stage to validate against. Required. | | \-p, --prompt-pattern | | One or more glob patterns for locating MCP prompt definition files (\*.json). | | \-t, --tool-pattern | | One or more glob patterns for locating MCP tool definition files (\*.graphql). | ### Examples Validate against the `dev` stage: ``` nitro mcp validate \ --mcp-feature-collection-id "" \ --stage "dev" \ --prompt-pattern "./prompts/**/*.json" \ --tool-pattern "./tools/**/*.graphql" ``` ## `nitro mcp list` List all MCP feature collections of an API. Results are paginated, use the returned cursor to fetch the next page. ``` nitro mcp list --api-id "" ``` ### Options | Option | Env | Description | | ------------------ | -------------- | -------------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API. Falls back to interactive selection when omitted. | | \--cursor | NITRO\_CURSOR | Pagination cursor to resume from. Useful for non-interactive paging. | ## `nitro mcp delete` Delete an MCP feature collection by its ID. Once deleted, the collection and all its versions are no longer accessible to MCP clients. ``` nitro mcp delete "" ``` ### Arguments | Argument | Description | | -------- | ----------------------------------------------------- | | | ID of the MCP feature collection to delete. Required. | ### Options | Option | Description | | -------- | -------------------------------------------------------------------------------------------------------------------------- | | \--force | Skip the confirmation prompt. Required when running non-interactively (for example in CI) or together with \--output json. | [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/mcp.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # openapi Command - Nitro > Manage OpenAPI collections with the `nitro openapi` commands: upload versioned HTTP endpoint definitions, validate them, and publish them to a stage. Canonical source: https://chillicream.com/docs/nitro/cli/openapi The `nitro openapi` commands manage OpenAPI collections. An OpenAPI collection bundles a versioned set of HTTP endpoint definitions and/or models that HotChocolate (Fusion) uses to expose HTTP endpoints as a GraphQL schema on a given stage. A typical workflow is: `create` a collection on an API, `upload` a new version of its HTTP endpoint definitions and/or models, optionally `validate` that version against a stage, then `publish` it. > Validation runs automatically as part of `nitro openapi publish`. Use `nitro openapi validate` only when you need to gate a deploy step in a separate pipeline job. All `openapi` commands require authentication. Run `nitro login` first or pass `--api-key` (see [Global Options](https://chillicream.com/docs/nitro/cli/global-options)). ## `nitro openapi create` Create a new OpenAPI collection on an API. ``` nitro openapi create \ --name "" \ --api-id "" ``` ### Options | Option | Env | Description | | ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | \--name | NITRO\_OPENAPI\_COLLECTION\_NAME | Display name of the OpenAPI collection. Required. | | \--api-id | NITRO\_API\_ID | ID of the API the collection belongs to. Required when no workspace is set in the session. Get the ID from nitro api list or the Nitro UI. | ### Examples Create a collection interactively (prompts for missing values): ``` nitro openapi create ``` Create a collection non-interactively: ``` nitro openapi create --name "" --api-id "" ``` ## `nitro openapi upload` Upload a new version of an OpenAPI collection. ``` nitro openapi upload \ --openapi-collection-id "" \ --tag "" \ --pattern "" ``` ### Options | Option | Env | Description | | ------------------------------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------ | | \--openapi-collection-id | NITRO\_OPENAPI\_COLLECTION\_ID | ID of the OpenAPI collection. Required. | | \--tag | NITRO\_TAG | Tag of the version being uploaded (for example a Git commit SHA or release tag). Required. | | \-p, --pattern | | One or more glob patterns selecting \*.graphql files defining HTTP endpoints / models definitions. Required. | ### Examples Upload all OpenAPI documents matching a pattern: ``` nitro openapi upload \ --openapi-collection-id "" \ --tag "v1" \ --pattern "./**/*.graphql" ``` ## `nitro openapi publish` Publish a previously uploaded OpenAPI collection version to a stage. The version is identified by its tag. ``` nitro openapi publish \ --openapi-collection-id "" \ --tag "" \ --stage "" ``` ### Options | Option | Env | Description | | ------------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | \--openapi-collection-id | NITRO\_OPENAPI\_COLLECTION\_ID | ID of the OpenAPI collection. Required. | | \--tag | NITRO\_TAG | Tag of the version to publish. Required. | | \--stage | NITRO\_STAGE | Name of the stage to publish to. Required. | | \--force | | Skip confirmation prompts for deletes and overwrites. Mutually exclusive with \--wait-for-approval. | | \--wait-for-approval | NITRO\_WAIT\_FOR\_APPROVAL | Block the command until a reviewer approves the deployment. Mutually exclusive with \--force. Required when the stage gates deployments. | ### Examples Publish to `dev`: ``` nitro openapi publish \ --openapi-collection-id "" \ --stage "dev" \ --tag "v1" ``` Publish to a gated stage and wait for approval: ``` nitro openapi publish \ --openapi-collection-id "" \ --stage "production" \ --tag "v1" \ --wait-for-approval ``` ## `nitro openapi validate` Validate a new OpenAPI collection version against a stage without publishing it. Run this in your pull request validation workflow to catch breaking changes before they are merged. ``` nitro openapi validate \ --openapi-collection-id "" \ --stage "" \ --pattern "" ``` ### Options | Option | Env | Description | | ------------------------------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------ | | \--openapi-collection-id | NITRO\_OPENAPI\_COLLECTION\_ID | ID of the OpenAPI collection. Required. | | \--stage | NITRO\_STAGE | Name of the stage to validate against. Required. | | \-p, --pattern | | One or more glob patterns selecting \*.graphql files defining HTTP endpoints / models definitions. Required. | ### Examples Validate against the `dev` stage: ``` nitro openapi validate \ --openapi-collection-id "" \ --stage "dev" \ --pattern "./**/*.graphql" ``` ## `nitro openapi list` List all OpenAPI collections of an API. Results are paginated, use the returned cursor to fetch the next page. ``` nitro openapi list --api-id "" ``` ### Options | Option | Env | Description | | ------------------ | -------------- | -------------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API. Falls back to interactive selection when omitted. | | \--cursor | NITRO\_CURSOR | Pagination cursor to resume from. Useful for non-interactive paging. | ## `nitro openapi delete` Delete an OpenAPI collection by its ID. Once deleted, the collection and all its versions are no longer accessible. ``` nitro openapi delete "" ``` ### Arguments | Argument | Description | | -------- | ------------------------------------------------- | | | ID of the OpenAPI collection to delete. Required. | ### Options | Option | Description | | -------- | -------------------------------------------------------------------------------------------------------------------------- | | \--force | Skip the confirmation prompt. Required when running non-interactively (for example in CI) or together with \--output json. | [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/openapi.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # pat Command - Nitro > Create and manage Personal Access Tokens with the `nitro pat` commands: user-bound credentials for personal automation and bootstrapping Nitro workspaces. Canonical source: https://chillicream.com/docs/nitro/cli/pat The `nitro pat` commands manage Personal Access Tokens (PATs). A PAT is bound to your user account and acts on your behalf, so it inherits your access across every workspace and API you are a member of. PATs are intended for personal automation, scripts on your machine, and bootstrapping operations that need broader permissions than an [API key](https://chillicream.com/docs/nitro/cli/api-key) provides (for example creating workspaces or managing members). For narrower, non-user-bound automation (CI/CD, deploy pipelines, telemetry from your GraphQL server), prefer an [API key](https://chillicream.com/docs/nitro/cli/api-key) scoped to a single API or workspace. To use a PAT for non-interactive CLI calls, pass it via `--api-key` (or `NITRO_API_KEY`). The Nitro server accepts both PATs and API keys through the same option. > Treat a PAT like a password. It can do anything you can do, store the secret in a secret manager and revoke it as soon as you no longer need it. All `pat` commands require authentication. Run `nitro login` first or pass `--api-key` (see [Global Options](https://chillicream.com/docs/nitro/cli/global-options)). ## `nitro pat create` Create a new personal access token. The secret is returned only once at creation time, store it in a secure location (for example a secret manager) before closing the terminal. ``` nitro pat create \ --description "" \ --expires "" ``` ### Options | Option | Env | Description | | ---------------------------- | ------------------ | ---------------------------------------------------------------------- | | \--description | NITRO\_DESCRIPTION | Human-readable description used to identify the token later. Required. | | \--expires | NITRO\_EXPIRES | Expiration time of the token in days. Default: 180. | ### Examples Create a token with the default 180-day expiration: ``` nitro pat create --description "" ``` Create a short-lived token: ``` nitro pat create --description "" --expires "30" ``` Capture the secret in a script: ``` SECRET=$(nitro pat create \ --description "" \ --output json | jq -r '.secret') ``` Use the captured secret to authenticate subsequent CLI calls: ``` nitro workspace list --api-key "$SECRET" ``` ## `nitro pat list` List the personal access tokens on your user account. Results are paginated, use the returned cursor to fetch the next page. Secrets are not returned, only metadata. ``` nitro pat list ``` ### Options | Option | Env | Description | | ------------------ | ------------- | -------------------------------------------------------------------- | | \--cursor | NITRO\_CURSOR | Pagination cursor to resume from. Useful for non-interactive paging. | ## `nitro pat revoke` Revoke a personal access token by its ID. Once revoked, any client using the token loses access immediately. ``` nitro pat revoke "" ``` ### Arguments | Argument | Description | | -------- | ---------------------------------------------------- | | | ID of the personal access token to revoke. Required. | ### Options | Option | Description | | -------- | -------------------------------------------------------------------------------------------------------------------------- | | \--force | Skip the confirmation prompt. Required when running non-interactively (for example in CI) or together with \--output json. | [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/pat.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # schema Command - Nitro > Manage the GraphQL schema of an API with the `nitro schema` commands: upload versions, validate a stage for breaking changes, publish, and download. Canonical source: https://chillicream.com/docs/nitro/cli/schema The `nitro schema` commands manage the GraphQL schema (SDL) of an API. The typical flow is: `upload` a new version, `validate` it against the target stage to detect breaking changes, then `publish` it once the changes are safe. `download` retrieves the schema currently published to a stage, which is useful for code generation and local tooling. > For HotChocolate Fusion gateways, use the [nitro fusion](https://chillicream.com/docs/nitro/cli/fusion) commands instead. All `schema` commands require authentication. Run `nitro login` first or pass `--api-key` (see [Global Options](https://chillicream.com/docs/nitro/cli/global-options)). ## `nitro schema upload` Upload a new schema version to an API. The version is identified by a tag and is not yet published to any stage. ``` nitro schema upload \ --api-id "" \ --tag "" \ --schema-file ``` ### Options | Option | Env | Description | | ---------------------------- | ------------------- | ------------------------------------------------------------------------ | | \--api-id | NITRO\_API\_ID | ID of the API to upload to. Required. | | \--tag | NITRO\_TAG | Tag of the new schema version, for example v1 or a Git commit. Required. | | \--schema-file | NITRO\_SCHEMA\_FILE | Path to the GraphQL file with the schema definition. Required. | ### Examples Upload a schema version: ``` nitro schema upload \ --api-id "" \ --tag "v1" \ --schema-file ./schema.graphqls ``` ## `nitro schema publish` Publish a previously uploaded schema version to a stage. The version is identified by its tag. ``` nitro schema publish \ --api-id "" \ --tag "" \ --stage "" ``` ### Options | Option | Env | Description | | -------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API. Required. | | \--tag | NITRO\_TAG | Tag of the schema version to publish. Required. | | \--stage | NITRO\_STAGE | Name of the stage to publish to. Required. | | \--force | | Skip confirmation prompts and publish even when the version contains breaking changes. Mutually exclusive with \--wait-for-approval. | | \--wait-for-approval | NITRO\_WAIT\_FOR\_APPROVAL | Block the command until a reviewer approves the deployment. Mutually exclusive with \--force. Required when the stage gates deployments. | ### Examples Publish to `dev`: ``` nitro schema publish \ --api-id "" \ --tag "v1" \ --stage "dev" ``` Publish to a gated stage and wait for approval: ``` nitro schema publish \ --api-id "" \ --tag "v1" \ --stage "production" \ --wait-for-approval ``` ## `nitro schema validate` Validate a new schema version against a stage without publishing it. Run this in your pull request validation workflow to catch breaking changes before they are merged. ``` nitro schema validate \ --api-id "" \ --stage "" \ --schema-file ``` ### Options | Option | Env | Description | | ---------------------------- | ------------------- | -------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API. Required. | | \--stage | NITRO\_STAGE | Name of the stage to validate against. Required. | | \--schema-file | NITRO\_SCHEMA\_FILE | Path to the GraphQL file with the schema definition. Required. | ### Examples Validate against the `dev` stage: ``` nitro schema validate \ --api-id "" \ --stage "dev" \ --schema-file ./schema.graphqls ``` ## `nitro schema download` Download the schema currently published to a stage and write it to a file. ``` nitro schema download \ --api-id "" \ --stage "" \ --output-file ``` ### Options | Option | Env | Description | | ---------------------------- | ------------------- | ------------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API. Required. | | \--stage | NITRO\_STAGE | Name of the stage to download from. Required. | | \--output-file | NITRO\_OUTPUT\_FILE | Path to write the schema to. If the file exists, it is overwritten. | ### Examples Download the `dev` schema to a local file: ``` nitro schema download \ --api-id "" \ --stage "dev" \ --output-file ./schema.graphqls ``` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/schema.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # stage Command - Nitro > Manage the deployment stages of a Nitro API with the `nitro stage` commands: declare stages like dev and production via `nitro stage edit` and list them. Canonical source: https://chillicream.com/docs/nitro/cli/stage The `nitro stage` commands manage the stages of an API. Stages represent deployment targets (for example dev, staging, production) that artifacts like schemas, clients, and fusion configurations are published to. Stages are not created with a dedicated `create` command. Instead, the full set of stages for an API is declared together with `nitro stage edit`, either interactively or by passing a JSON `--configuration`. Conditions on a stage (such as `afterStage`) do not have any effect besides how the UI for the stages is being rendered. All `stage` commands require authentication. Run `nitro login` first or pass `--api-key` (see [Global Options](https://chillicream.com/docs/nitro/cli/global-options)). ## `nitro stage edit` Edit the stages of an API. When `--configuration` is omitted, an interactive editor lets you add, rename, reorder, and delete stages. In non-interactive mode the configuration must be supplied as JSON. ``` nitro stage edit \ --configuration "[{\"name\":\"dev\",\"displayName\":\"Dev\",\"conditions\":[]}]" \ --api-id "" ``` ### Options | Option | Env | Description | | -------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API whose stages you are editing. Required. | | \--configuration | | Stage configuration as a JSON array. Each entry has name, displayName, and a conditions array (for example \[{"afterStage":"dev"}\]). If omitted, the CLI opens an interactive editor. | ### Examples Open the interactive editor for an API: ``` nitro stage edit --api-id "" ``` Replace the stages of an API with a single `dev` stage: ``` nitro stage edit \ --api-id "" \ --configuration "[{\"name\":\"dev\",\"displayName\":\"Dev\",\"conditions\":[]}]" ``` Define a `dev` to `prod` promotion chain: ``` nitro stage edit \ --api-id "" \ --configuration "[{\"name\":\"dev\",\"displayName\":\"Dev\",\"conditions\":[]},{\"name\":\"prod\",\"displayName\":\"Production\",\"conditions\":[{\"afterStage\":\"dev\"}]}]" ``` ## `nitro stage list` List all stages of an API, including their conditions. ``` nitro stage list --api-id "" ``` ### Options | Option | Env | Description | | ------------------ | -------------- | ------------------------ | | \--api-id | NITRO\_API\_ID | ID of the API. Required. | ## `nitro stage delete` Delete a single stage by name. Removing a stage that other parts of your workflow depend on may fail, resolve those dependencies first. ``` nitro stage delete \ --stage "" \ --api-id "" ``` ### Options | Option | Env | Description | | ------------------ | -------------- | -------------------------------------------------------------------------------------------------------------------------- | | \--api-id | NITRO\_API\_ID | ID of the API the stage belongs to. Required. | | \--stage | NITRO\_STAGE | Name of the stage to delete. Required. | | \--force | | Skip the confirmation prompt. Required when running non-interactively (for example in CI) or together with \--output json. | [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/stage.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # status Command - Nitro > The `nitro status` command shows the current Nitro CLI session: the logged-in user, the active workspace, and the backend URL when it is not the default. Canonical source: https://chillicream.com/docs/nitro/cli/status The `nitro status` command displays the current session status, including the logged-in user, the active workspace (if one is selected), and the backend URL when it differs from the default. The `status` command requires authentication. Run [nitro login](https://chillicream.com/docs/nitro/cli/login) first or pass `--api-key`. ``` nitro status ``` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/status.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # workspace Command - Nitro > Manage Nitro workspaces with the `nitro workspace` commands: create workspaces, list them, set a session default, and check the current selection. Canonical source: https://chillicream.com/docs/nitro/cli/workspace The `nitro workspace` commands manage workspaces. A workspace is the top-level container in Nitro, every API, environment, and API key belongs to exactly one workspace. The CLI tracks a default workspace per session so most other commands can omit `--workspace-id`. Use `nitro workspace set-default` to change it and `nitro workspace current` to see what is selected. All `workspace` commands require authentication. Run `nitro login` first or pass `--api-key` (see [Global Options](https://chillicream.com/docs/nitro/cli/global-options)). ## `nitro workspace create` Create a new workspace. ``` nitro workspace create --name "" ``` ### Options | Option | Description | | -------------- | -------------------------------------------------------------------------------------------------------------- | | \--name | Display name of the workspace. Required. | | \--default | Set the created workspace as the default for the current session. Pass \--default false to opt out explicitly. | When run interactively without `--name`, the CLI prompts for it. ## `nitro workspace list` List the workspaces you have access to. Results are paginated, use the returned cursor to fetch the next page. ``` nitro workspace list ``` ### Options | Option | Env | Description | | ------------------ | ------------- | -------------------------------------------------------------------- | | \--cursor | NITRO\_CURSOR | Pagination cursor to resume from. Useful for non-interactive paging. | ## `nitro workspace show` Show the details of a workspace by its ID. ``` nitro workspace show "" ``` ### Arguments | Argument | Description | | -------- | -------------------------------------- | | | ID of the workspace to show. Required. | ## `nitro workspace current` Show the name of the currently selected workspace. ``` nitro workspace current ``` ## `nitro workspace set-default` Set the default workspace for the current session. In interactive mode the CLI shows a picker, in non-interactive mode pass `--workspace-id`. ``` nitro workspace set-default ``` ### Options | Option | Env | Description | | ------------------------------ | -------------------- | ---------------------------------------------------------------------------- | | \--workspace-id | NITRO\_WORKSPACE\_ID | ID of the workspace to set as the default. Required in non-interactive mode. | [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/cli/workspace.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Documents - Nitro > Tour of the Document View in Nitro, the workspace for running GraphQL queries, mutations, and subscriptions and exploring the schema reference and SDL. Canonical source: https://chillicream.com/docs/nitro/documents The Document View is a feature that allows users to work with documents and API documents within the application. It provides functionality for executing queries, mutations, and subscriptions. This section describes the various components and functionalities available in the Document View. ![Image](https://chillicream.com/images/nitro-docs/documents/document-0.webp) ## 1\. Operation The Operation section is where you can write and execute your queries, mutations, and subscriptions. It provides a convenient interface for interacting with the GraphQL API. ## 2\. Schema Reference The Schema Reference section allows you to explore the schema in a tree view or the explorer view. It provides detailed information about the available schema and its components. For more information on using the Schema Reference, refer to the [Schema Reference](https://chillicream.com/docs/nitro/documents/schema-reference) guide. ## 3\. Schema Definition The Schema Definition section displays the schema definition in SDL (Schema Definition Language) format. It allows you to inspect the schema structure and understand its various types and fields. Refer to the [Schema Definition](https://chillicream.com/docs/nitro/documents/schema-definition) guide for further details. ## 4\. Document Status Indicator The Document Status Indicator visually represents whether the current document is saved or not. A white dot indicates that the document is not saved, while a different icon may indicate a saved or modified state. ## 5\. Query Editor The Query Editor provides a dedicated space for writing your queries, mutations, and subscriptions. It offers features such as syntax highlighting, auto-completion, and error checking to assist in query composition. Learn more about using the query editor in the [Operations](https://chillicream.com/docs/nitro/documents/operations) guide. ## 6\. Response Pane The Response Pane displays the response of your queries, mutations, and subscriptions. It shows the data returned by the API and provides a structured view for easier analysis. Refer to the [Response Pane](https://chillicream.com/docs/nitro/documents/response) guide for additional information. ## 7\. Schema Indicator The Schema Indicator is a visual cue that indicates the connection status with the GraphQL server and the successful retrieval of the schema. A green circle represents a connected state. ## 8\. Connected Schema Endpoint This section displays the currently connected schema endpoint, providing information about the GraphQL server being accessed. ## 9\. Settings The Document Settings section allows you to configure various settings specific to the document. Refer to the [Connection Settings](https://chillicream.com/docs/nitro/documents/connection-settings) guide for detailed information about it. You can also configure the authentication settings for the document. Refer to the [Authentication Settings](https://chillicream.com/docs/nitro/documents/authentication) guide for more details. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/documents/index.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Authentication - Nitro > Configure Basic Authentication, Bearer Token, or OAuth 2.0 flows in Nitro to fetch tokens and send the Authorization header with your GraphQL requests. Canonical source: https://chillicream.com/docs/nitro/documents/authentication Nitro offers support for various authentication flows. The following guide details how to use these authentication flows to retrieve a token from an identity server and send the Authorization header to the server. ![Nitro Authentication Flow](https://chillicream.com/images/nitro-docs/documents/auth-0.webp) Accessing the authentication settings is straightforward: 1. Locate the cog icon in the navigation bar on the left side of the screen. 2. Click on the icon to open the authentication settings. Nitro supports three types of authentication flows: - [Basic Authentication](#basic-authentication) - [Bearer Token](#bearer-token) - [OAuth 2.0](#oauth-20) ## Basic Authentication Basic Authentication is a built-in authentication scheme of the HTTP protocol. It works by sending HTTP requests with an Authorization header. This header includes the word 'Basic' followed by a space and a base64-encoded string of the format 'username:password'. ![Basic Authentication Fields](https://chillicream.com/images/nitro-docs/documents/auth-1.webp) When setting up Basic Authentication, you will need to provide the following information: - **Username**: The username required for authentication. - **Password**: The corresponding password for the provided username. - **Authorization Header**: This is a preview of the header that will be used for authentication. This field is auto-generated based on the inputted username and password. Learn more about Basic Authentication [here](https://en.wikipedia.org/wiki/Basic%5Faccess%5Fauthentication). ## Bearer Token This method involves a client sending a token within the Authorization header. The token is typically generated by the server when a login request is processed. Subsequent requests to protected resources must include this token in the Authorization header. ![Bearer Token Fields](https://chillicream.com/images/nitro-docs/documents/auth-2.webp) You will need to provide at least the token. The following fields are available: - **Token**: The token that will be used for authentication. - **Prefix**: The prefix for the token. By default, this is set as 'Bearer'. - **Authorization Header**: A preview of the header that will be used for authentication. This field is auto-generated based on the provided token and prefix. Learn more about Bearer Token Authentication [here](https://swagger.io/docs/specification/authentication/bearer-authentication/). ## OAuth 2.0 OAuth 2.0 authentication flow is an industry-standard authorization framework. It allows third-party applications to gain limited access to a web service through a server that supports the OAuth 2.0 protocol. Most major identity providers support OAuth 2.0, including Auth0, Okta, AWS Cognito, Azure AD, and more. In .NET, OAuth 2.0 is implemented by [Duende IdentityServer](https://duendesoftware.com/products/identityserver) and [OpenIddict](https://documentation.openiddict.com/). Nitro supports several OAuth 2.0 flows, including: - [Authorization Code](https://auth0.com/docs/flows/authorization-code-flow) - [Client Credentials](https://auth0.com/docs/flows/client-credentials-flow) - [Resource Owner Password Credentials](https://auth0.com/docs/flows/resource-owner-password-flow) - [Implicit](https://auth0.com/docs/get-started/authentication-and-authorization-flow/implicit-flow-with-form-post) Depending on the selected OAuth 2.0 flow, the fields you need to fill in will vary. ![OAuth 2.0 Fields](https://chillicream.com/images/nitro-docs/documents/auth-3.webp) The following fields are available required: - **[Grant Type](https://datatracker.ietf.org/doc/html/rfc6749#section-1.3)**: This is the method an application uses to obtain an access token. Common values include 'authorization\_code', 'client\_credentials', 'password', and 'refresh\_token'. Each type serves a different use case, such as a web application, machine-to-machine, mobile apps, etc. - **Authorization URL**: This is the URL to which your application directs the user in the initial step of the authorization process. It usually looks something like ''. - **Access Token URL**: This is the URL your application uses to obtain the access token from the authorization server. It's typically of the form ''. - **[Client ID](https://datatracker.ietf.org/doc/html/rfc6749#section-2.2)**: This is a public identifier for your application, issued by the authorization server when you register your application. It's used to identify your application to the user during authorization. - **[Client Secret](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1)**: This is a confidential key held by the client application, used to authenticate to the authorization server when using 'client\_credentials' or 'authorization\_code' grant types. It should be kept confidential and never exposed publicly. - **[Use PKCE](https://datatracker.ietf.org/doc/html/rfc7636)**: PKCE (Proof Key for Code Exchange) is a mechanism designed to secure public clients that don't use a client secret. It's highly recommended for mobile and single-page apps. When enabled, it adds an extra step in the authorization process to prevent certain types of attacks. - **[Scope](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3)**: These are permissions that the application requests. The value is a list of space-delimited strings, such as 'read:messages write:messages'. 'offline\_access' can be requested to get a refresh token. - **[Redirect URL](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2)**: This is the URL to which the authorization server will redirect the user's browser after authorization has been granted. It must match one of the redirect URIs registered with the authorization server. - **[Code Challenge Method](https://datatracker.ietf.org/doc/html/rfc7636#section-4.2)**: This field is relevant if PKCE is used. It defines how the code verifier is transformed. 'PLAIN' or 'S256' (SHA256) are common options, with 'S256' being more secure. - **[State](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1)**: This is an opaque value that is used to maintain state between the request and the callback, mitigating CSRF attacks. It's a good practice to use a unique value for each authorization request. - **[Credentials](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3)**: Defines how client credentials are sent to the server. They can be sent as a Basic Auth Header or in the Request Body. - **Header Prefix**: This is the prefix that appears before the token in the Authorization header. The default is 'Bearer', as described in [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750), but it could also be 'Token' or other custom strings. - **Audience**: This is the intended audience of the token, typically the identifier of the resource server that should accept the token. - **Resource**: The target resource that the application wants to access. - **Origin**: This is used in browser-based applications to indicate the origin of the request and mitigate CSRF attacks. - **Username**: Required for the 'password' grant type. This is the resource owner's username. - **Password**: Also required for the 'password' grant type. This is the resource owner's password. - **[Response Type](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.1)**: Indicates what should be returned from the initial request. For 'authorization\_code' grant type, this should be 'code'. For the implicit grant type, this would typically be 'token'. This comprehensive set of options allows for fine-tuned control of OAuth 2.0 authentication flows in your application. Learn more about OAuth 2.0 [here](https://oauth.net/2/). ### Request a token In Nitro, you can fetch the authentication token using two different methods: 1. **Fetch Button in Authentication Settings**: Located at the bottom of the authentication settings is a button labeled `Fetch`. Clicking this will retrieve the authentication token. Once the token is fetched, you also have options to `Clear` it or `Refresh` it (if a refresh token was requested). In the desktop application, there's an additional feature to `Reset the session on identity server`. This is particularly useful because your authentication session on your identity server is persisted in the browser, meaning you don't need to sign in repeatedly. If you wish to log in as a different user, you can reset your session by clicking this button. ![Fetch Button](https://chillicream.com/images/nitro-docs/documents/auth-4.webp) 2. **Key Icon in Operations Pane**: At the top right of the operations pane, you'll notice a key icon. Clicking this icon initiates the authentication flow. ![Key Icon](https://chillicream.com/images/nitro-docs/documents/auth-5.webp) These two methods allow you to conveniently manage and initiate your authentication flows, providing you with flexibility to cater to different use-case scenarios. ### Redirect URL In the context of Nitro, the Redirect URL plays a crucial role, particularly when you're using Nitro within a web browser rather than the desktop application. The Redirect URL is where your browser is directed to after the authentication process. It must be configured to point back to the URL where Nitro is hosted. This is essential because Nitro needs to retrieve the authorization code from this URL to exchange it for a token. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/documents/authentication.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Connection Settings - Nitro > Configure Connection Settings in Nitro: the HTTP and subscription endpoints, subscription protocol, and other options for talking to your GraphQL server. Canonical source: https://chillicream.com/docs/nitro/documents/connection-settings The Connection Settings in Nitro allow you to configure various options related to connecting and communicating with your GraphQL server. This section describes the different settings available and their functionalities. ![Connection-Settings](https://chillicream.com/images/nitro-docs/documents/connection-settings-0.webp) ## HTTP Endpoint The HTTP Endpoint refers to the URL of your GraphQL server. It is the endpoint used to send queries and mutations. Configure this setting with the appropriate URL to establish a connection with your GraphQL server. ## Subscription Endpoint The Subscription Endpoint represents the URL used to send subscriptions to the GraphQL server. By default, it is inferred from the Schema Endpoint. Specify the subscription URL if it differs from the Schema Endpoint. ## SSE Subscription Endpoint If you utilize SSE (Server-Sent Events) subscriptions, the SSE Subscription Endpoint should be provided. This URL specifies the endpoint for SSE subscriptions. By default, it is inferred from the Schema Endpoint. ## Subscription Protocol The Subscription Protocol setting allows you to choose the protocol for handling subscriptions. Nitro supports the following options: - **Auto**: Nitro negotiates the protocol with the server automatically based on server capabilities. - **GraphQL Websocket**: Nitro uses the [graphql-ws](https://github.com/enisdenjo/graphql-ws) protocol for handling subscriptions. - **GraphQL SSE**: Nitro uses the [graphql-sse](https://github.com/enisdenjo/graphql-sse) protocol for handling subscriptions. - **Apollo Websocket**: Nitro uses the deprecated [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) protocol from Apollo. ## Use HTTP GET By enabling the Use HTTP GET option, Nitro will use HTTP GET instead of HTTP POST for executing queries and mutations. This can be useful in certain scenarios or for compatibility with specific GraphQL servers. ## Include Cookies (cross-origin) Enabling the Include Cookies (cross-origin) option ensures that cookies are included when sending queries and mutations to the GraphQL server. This is particularly relevant in cross-origin situations where cookies are required for authentication or session management. The Connection Settings provide you with the flexibility to customize the connection parameters and communication behavior with your GraphQL server. Configure these settings according to your server's requirements and the desired functionality of your application. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/documents/connection-settings.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Operation Pane - Nitro > Write and run GraphQL queries, mutations, and subscriptions in Nitro's Operation Pane, with a request editor, run buttons, formatting, and variables panel. Canonical source: https://chillicream.com/docs/nitro/documents/operations The Operation Pane provides a comprehensive interface for writing GraphQL queries, mutations, and subscriptions. It offers several features to enhance your development experience. ![Nitro - Operation Pane](https://chillicream.com/images/nitro-docs/documents/operation-0.webp) ## **1\. Request Editor** The Request Editor is a text editor within the Operation Pane where you can write your GraphQL queries, mutations, and subscriptions. It provides syntax highlighting, intellisense, and validation to assist you in writing accurate queries. ## **2\. Inline Run Button** The Inline Run Button allows you to execute your query directly from the Request Editor. By clicking this button, your query will be sent to the GraphQL server, and the response will be displayed in the Response Pane. ## **3\. Format Button** The Format Button helps you maintain proper query formatting according to common formatting rules. When clicked, it automatically formats your query and updates the content in the Request Editor. ## **4\. Run Button** The Run Button enables you to execute your query with a single click. Similar to the Inline Run Button, clicking this button will send your query to the GraphQL server, and the response will be shown in the Response Pane. ## **5\. Variables Panel** The Variables Panel allows you to define variables for your query. You can specify the variable name, type, and default value. These variables can then be utilized within your query, making it more dynamic and reusable. ## **6\. Headers Panel** The Headers Panel enables you to define headers for your query. You can specify the header name and value, which will be included when sending the query to the GraphQL server. This feature is useful for including authentication tokens or other custom headers required by your API. For authentication, you can also use the Authentication Settings feature, which is described in the [Authentication Settings](https://chillicream.com/docs/nitro/documents/authentication) section. ## **7\. File Upload Panel** The File Upload Panel provides functionality for uploading files to the GraphQL server. You can specify the file name and contents within this panel. This feature works in conjunction with the `Upload` scalar type, a special type in GraphQL that facilitates file uploads. It follows the multipart form request specification to send files to the GraphQL server. For more details, refer to the [documentation](https://chillicream.com/docs/hotchocolate/server/files) on file uploads in GraphQL. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/documents/operations.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Response Pane - Nitro > Inspect query results in Nitro's Response Pane: view the JSON response, browse the history of executed requests, and check status code, duration, and size. Canonical source: https://chillicream.com/docs/nitro/documents/response The Response Pane is a central feature that is located to the right of the request editor. Its primary function is to display the response of a query that has been executed, but it offers a range of additional features that help in analyzing these responses. ![Nitro - Response Pane](https://chillicream.com/images/nitro-docs/documents/response-0.webp)The pane is divided into two sections: 1. The upper section displays the JSON response. 2. The lower section maintains a record of all executed queries related to the current document. 3. Additionally, you can get a brief overview of the response status code, the duration, and the size of the last executed request on the top right of the pane. ## Response Section ![Nitro - Response Section](https://chillicream.com/images/nitro-docs/documents/response-1.webp)This part of the pane displays the JSON response of the executed query. In case you have deferred results, this view amalgamates all results into one JSON object. For those using subscriptions, this view will display the most recent result of the subscription. ## Responses Section ![Nitro - Responses Section](https://chillicream.com/images/nitro-docs/documents/response-2.webp) The Responses section keeps track of all the queries that have been executed in the current document. Clicking on a history entry will load the corresponding query into the editor, allowing you to review the response. Clicking the button labelled `1` above will open the corresponding log entry: ![Nitro - Request Log](https://chillicream.com/images/nitro-docs/documents/response-3.webp) [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/documents/response.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Schema Definition - Nitro > View and download the raw SDL of your GraphQL server with Nitro's Schema Definition feature, including an editor for examining types, fields, and structure. Canonical source: https://chillicream.com/docs/nitro/documents/schema-definition ![Nitro - Schema Definition](https://chillicream.com/images/nitro-docs/documents/definition-0.webp) The Schema Definition feature in Nitro provides you with access to the raw Schema Definition Language (SDL) of your GraphQL server. This section explains how to view and download the SDL using Nitro. 1. SDL Editor The SDL Editor allows you to explore and examine the SDL of your GraphQL server. You can view the SDL code directly in the editor and analyze the schema structure, types, and fields. 2. Download SDL To download the SDL, locate the download icon in the top right-hand corner of the "Schema Definition" area. Clicking on the download icon will initiate the download process, allowing you to save the SDL file locally on your device. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/documents/schema-definition.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Schema Reference - Nitro > Explore your GraphQL schema with Nitro's Schema Reference: browse types, fields, and directives in the Tree View or Explorer View and filter by name. Canonical source: https://chillicream.com/docs/nitro/documents/schema-reference The Schema Reference feature allows you to inspect and explore the schema of your GraphQL server. It provides valuable information about the available types, fields, and directives within the schema. This section describes the components and functionalities of the Schema Reference. ## Explorer View ![Explorer View](https://chillicream.com/images/nitro-docs/documents/reference-0.webp) ### 1\. View Switcher The View Switcher allows you to toggle between two different views: the Tree View and the Explorer View. Each view provides a unique way to navigate and explore the schema. You are currently viewing the Explorer View. ### 2\. Selected Type Details This section displays detailed information about the currently selected type, including its fields and arguments. It offers insights into the structure and properties of the selected type. ### 3\. Summary Information The Summary Information provides quick statistics about the number of types and directives present in the schema. It offers a high-level overview of the schema's composition. ### 4\. Filter Bar The Filter Bar allows you to search for specific types and fields within the schema. You can enter keywords or names to quickly locate relevant components. ## Column View ![Column View](https://chillicream.com/images/nitro-docs/documents/reference-1.webp) In the Column View, you start with the root types of the schema, such as Query, Mutation, and Subscription. Clicking on a root type expands it to reveal the available fields associated with that type. By clicking on any field, you can further explore the schema and its nested components. The right side of the interface provides detailed information about the selected field. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/documents/schema-reference.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Environments - Nitro > Define environment variables in Nitro to reuse URLs, tokens, and credentials across documents and switch between development, testing, and production values. Canonical source: https://chillicream.com/docs/nitro/environments Environment variable provide a mechanism for reusing specific values across multiple documents. These variables are defined as key-value pairs. Frequently used data, like URLs, tokens, and credentials, are commonly centrally managed as environment variable. By doing so, these values can be referenced in your documents and changed in a single location for all you documents. You can also quickly switch between different values for different environments. For instance, you might store your API key as an environment variable, allowing it to be used universally in all your documents. If the API key changes, you can just update the environment variable without having to manually alter each document. You can define multiple environments for a single workspace corresponding to various contexts or project stages (e.g., development, testing, production). The environments can be switched in the status bar. When you have different project stages such as such as development (`https://serivce1.dev.company.com`), testing (`https://serivce1.test.company.com`), and production (`https://serivce1.prod.company.com`), you can for example set up the dynamic part of the url as a variable URL and incorporate it in all your urls (`https://serivce1.{{subdomain}}.company.com`). By switching the active environment in Nitro, the base URL will update automatically, streamlining your workflow. Similarly, identifiers such as client ID and client secret can be defined as environment variable, which make it convenient to work with. ## Creating Environments ![Screenshot showing the environment](https://chillicream.com/images/nitro-docs/env-0.webp)To create a new environment, follow these steps: 1. Click on the environment icon on the left side of your screen. 2. Click the Plus icon to create a new environment. 3. Enter a name for the environment. ## Rename Environments ![Screenshot showing the environment](https://chillicream.com/images/nitro-docs/env-1.webp)You can rename by right-clicking on the environment and selecting the 'Rename' option. ## Specifying Variables ![Screenshot showing the environment](https://chillicream.com/images/nitro-docs/env-2.webp) To open the environment variable editor, click on the environment you would like to edit. 1. **Enabled**: If an environment variable is disabled, it will not be used in requests. This state is not synchronized with the server and is only local to the client. This way you can temporarily disable an environment variable without other users being affected. 2. **Name**: The name of the environment variable. This name will be used to reference the variable in your documents. e.g. `{{ClientId}}`, `{{ClientSecret}}` `{{Subdomain}}` 3. **Value**: The value of the environment variable. This value will be substituted in your documents when the environment variable is referenced. 4. **Secret**: If an environment variable is marked as secret, it will be shown as a password field within the Nitro interface. This feature is designed to help prevent accidental exposure of the value. Please note that marking a value as secret does not mean it is encrypted; it is simply visually obscured in the user interface. 5. **Save Tab**: Clicking the 'Save Tab' button will save the environment variables. ## Switching Environments ![Screenshot showing the environment](https://chillicream.com/images/nitro-docs/env-3.webp)To switch environments, click on the environment name in the status bar. A dropdown menu will appear, listing all available environments. After selecting an environment, the environment variables of the selected environment will be active and applied to all documents. ## Using Variables ![Screenshot showing the environment](https://chillicream.com/images/nitro-docs/env-4.webp)Once you have defined an environment variable, you can reference it in your documents by using the following syntax: `{{variable_name}}`. Environment variable can be employed in a variety of places within Nitro, such as URLs, GraphQL Variables, and Connection Settings. When a request is executed, any referenced environment variable will be substituted with their respective values. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/environments.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Explore the UI - Nitro > Overview of the Nitro user interface: sidebar, document tree, main area, environments, history, settings, and the status bar, with links to detailed guides. Canonical source: https://chillicream.com/docs/nitro/explore-the-ui ![Image](https://chillicream.com/images/nitro-docs/explore-the-ui/eti-01.webp) Let's explore the main components of the Nitro user interface: 1. **Sidebar**: The sidebar contains various icons representing different sections of the application. Interacting with these icons will alter the content displayed in the main area. For instance, in the current view, the highlighted icon indicates that we are in the **Documents** section. For detailed information, please refer to our guide on [Documents](https://chillicream.com/docs/nitro/documents). 2. **Environments**: This section is for managing environments. 3. **History**: This section displays the list of recently executed queries. 4. **Settings**: To modify application preferences, you can navigate to the Settings section. Learn more about it in our [Settings Guide](https://chillicream.com/docs/nitro/settings). 5. **User Menu**: This section allows users to sign in/out, manage their accounts, and handle billing. You can read more about account management and billing in the organization section [Organizations](https://chillicream.com/docs/nitro/organizations). 6. **Document Tree**: Here you can manage files, folders, documents, and APIs. Read more about it in the [Explorer](https://chillicream.com/docs/nitro/explore-the-ui/explorer). 7. **Main Area**: The main area showcases the content related to the selected section from the sidebar. In this instance, it displays the **Documents** section. 8. **Status Bar**: The status bar provides quick access to workspace, organization, and synchronization features. It also displays additional information. More details are provided in our [Status Bar Guide](https://chillicream.com/docs/nitro/explore-the-ui/status-bar). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/explore-the-ui/index.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Explorer - Nitro > Navigate your workspace tree with Nitro's Explorer: quick actions for creating documents, folders, and APIs, plus context menus for item-specific operations. Canonical source: https://chillicream.com/docs/nitro/explore-the-ui/explorer The Explorer is a versatile component utilized in various parts of the application. It serves as a navigation tool for traversing the workspace tree, enabling you to efficiently browse and manage their project's structure. Additionally, the Explorer uses icons to visually represent the type of each item within the workspace. ![Explorer](https://chillicream.com/images/nitro-docs/explore-the-ui/explorer-0.webp) ## 1\. Quick Actions The Explorer offers a set of quick actions that allow users to perform common tasks swiftly. In the provided example, the quick actions enable the creation of a document, folder, or API with just a few clicks. ## 2\. Folder Node Folder nodes within the Explorer can be expanded or collapsed by clicking on the arrow associated with them. The icon accompanying each folder node indicates its item type, aiding in quick identification and differentiation. ## 3\. Context Menu Right-clicking on an item within the Explorer opens the context menu, which presents a list of actions that can be performed on the selected item. The context menu provides a convenient way to access item-specific operations and functions relevant to the current workspace context. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/explore-the-ui/explorer.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Status Bar - Nitro > Reference for Nitro's Status Bar: connection status, signed-in user, organization and workspace switchers, environments, sync indicator, and log entries. Canonical source: https://chillicream.com/docs/nitro/explore-the-ui/status-bar The status bar is a visual component located at the bottom of the application interface. It is divided into several sections, each providing valuable information to the user. ![Status Bar](https://chillicream.com/images/nitro-docs/explore-the-ui/status-0.webp) 1. **Connection Status:** This section indicates whether you are currently connected to the internet or not. When online, it means that any changes you make are being synchronized with the cloud. 2. **User Info:** The section displays the email of the user who is currently signed in to the application. It helps you identify the user account associated with the active session. 3. **Selected Organization:** In this section, you can see the name of the organization that is currently selected. Clicking on this section allows you to change the organization or add a new one. ![Status Bar](https://chillicream.com/images/nitro-docs/explore-the-ui/status-1.webp) 4. **Selected Workspace:** This section shows the currently selected workspace within the chosen organization. By clicking here, you can easily switch to a different workspace, providing seamless navigation between different project environments. 5. **Environment:** This section displays the name of the currently selected environment. Clicking on this section allows you to switch to a different environment or create a new one. ![Status Bar](https://chillicream.com/images/nitro-docs/explore-the-ui/status-2.webp) 6. **Workspace Synchronization Indicator:** The indicator in this section informs you whether the workspace is currently synchronizing. If synchronization is required or you want to manually trigger the process, you can click here to initiate synchronization. 7. **Schema Available:** This section provides an indicator to show whether the schema, which defines the structure and organization of data within the application, is available. 8. **Log:** This section indicates the number of log entries for each severity (error, information, warning). Clicking on this section will open the `Log` panel. 9. **Keyboard Shortcuts Overlay:** By clicking on this section, you can display an overlay that shows a list of available keyboard shortcuts for efficient navigation and interaction with the application. ![Status Bar](https://chillicream.com/images/nitro-docs/explore-the-ui/status-3.webp) 10. **Version Information:** This section displays relevant information about the version of the application you are currently using. It helps you identify the installed version and keep track of updates or changes that may be available. ![Status Bar](https://chillicream.com/images/nitro-docs/explore-the-ui/status-4.webp) [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/explore-the-ui/status-bar.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Getting Started - Nitro > Set up Nitro as a web or desktop app and run your first GraphQL query, from creating a document to connecting to a server and executing operations. Canonical source: https://chillicream.com/docs/nitro/getting-started Ready to explore the features of Nitro? In this getting started guide, we'll show you how to get up and running with Nitro. You have the option to choose between the convenience of the web app or the enhanced capabilities of the desktop version. We'll guide you through the setup process, so you can start enjoying all that Nitro has to offer. Get ready to unlock the full potential of GraphQL with Nitro. Choose your preferred option and follow the setup instructions to start enjoying the features that Nitro has to offer. ## Web App To begin your exploration, simply visit [nitro.chillicream.com](https://nitro.chillicream.com/) and experience the convenience of Nitro's web-based version. This excellent starting point allows you to conveniently probe public GraphQL APIs and access most of the features that Nitro offers. For an even more immersive experience, you can also install the web app as a Progressive Web App (PWA) on your device, providing a native-like experience. ## Desktop App You can install the Nitro App directly from our [download page](https://chillicream.com/products/nitro). The download page provides DMG installers for macOS, x64 installers for Windows, and AppImage/Snap installers for Ubuntu and Ubuntu-based Linux distributions. Note that other Linux distributions or installer formats are currently unsupported. Upon successful installation, follow the steps in the [Your first Query](#your-first-query) guide to execute your first GraphQL query using Nitro. ## Your First Query Lets guide you through the process of creating your first GraphQL query document using Nitro. **Step 1:** Start the Nitro application. You should see an interface similar to this: ![Nitro - Start](https://chillicream.com/images/nitro-docs/getting-started-0.webp) **Step 2:** Click on the "Create Document" button to initiate the creation of your first query document. **Step 3:** You'll see a new document called 'untitled 1' generated. Simultaneously, the connection settings dialog box will pop up automatically. Now, input the following link into the 'HTTP Endpoint' field: It should resemble this: ![Nitro - Start](https://chillicream.com/images/nitro-docs/getting-started-1.webp) **Step 4:** Click on the 'Apply' button. This will save your settings and close the dialog box. **Step 5:** At this point, your screen should look like this: ![Nitro - Start](https://chillicream.com/images/nitro-docs/getting-started-2.webp) Take note of the following key elements: 1. **Request Pane:** This is where you will input your queries. 2. **Response Pane:** Here, you will see the output or response of your queries. 3. **Variables Pane:** This is where you can define variables for your query. **Step 6:** The green circle at the top right next to the schema url indicates that you are connected to the GraphQL server and that your schema is fetched. You now have full intellisense for your queries. Copy the following GraphQL query and paste it into the 'Operations' editor area: GraphQL ``` { sessionById(id: "U2Vzc2lvbgppMQ==") { title track { name } startTime endTime } } ``` **Step 7:** Click on the 'Run' button. You should see a response with data in the 'Response' area. Congratulations, you have completed a GraphQL query! **Step 8:**Now, let's save your document. ![Nitro - Start](https://chillicream.com/images/nitro-docs/getting-started-3.webp)Sign in to your account by clicking the 'Sign In' button on the user icon in the bottom left corner of the screen. If you don't have an account, you can create a new one. **Step 9:** Once you are signed in, save your document by clicking the 'Save' button next to the tabs. Your documents are now synced between your devices. ![Nitro - Start](https://chillicream.com/images/nitro-docs/getting-started-4.webp) **Step 10:** Great job! You've successfully created, executed, and saved your first GraphQL query using Nitro. To learn more about the Nitro User Interface, head over to the [Explore the UI](https://chillicream.com/docs/nitro/explore-the-ui) guide. Happy querying! [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/getting-started.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Nitro - Express - Nitro > Serve the Nitro GraphQL IDE from your Express server with the @chillicream/nitro-express-middleware package, using a CDN hosted or self-hosted app. Canonical source: https://chillicream.com/docs/nitro/integrations/express You can easily integrate Nitro GraphQL IDE with your server app using the `@chillicream/nitro-express-middleware` package. You can either use a CDN hosted version of the app or a self-hosted version using the dedicated package. ## Installation First, you need to install this package and the required peer dependencies in your project: Bash ``` npm install @chillicream/nitro-express-middleware --save-dev # or yarn add @chillicream/nitro-express-middleware --dev # or pnpm add @chillicream/nitro-express-middleware --save-dev ``` Note: `@chillicream/nitro-embedded` is optional and only needed if you prefer to self host the app. ## Usage To use the middleware, simply import it and add it to your Express app. JavaScript ``` import express from "express"; import { graphqlHTTP } from "express-graphql"; import { GraphQLObjectType, GraphQLSchema, GraphQLString } from "graphql"; import nitroMiddleware from "@chillicream/nitro-express-middleware"; const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "Query", fields: { greeting: { type: GraphQLString, resolve(_parent, _args) { return "Hello, World!"; }, }, }, }), }); const app = express(); app.use( "/graphql", // for `cdn` hosted version nitroMiddleware({ mode: "cdn" }), // for `embedded` version // nitroMiddleware({ mode: "embedded" }), graphqlHTTP({ schema, graphiql: false, }), ); app.listen(3000, () => { console.log(`GraphQL on http://localhost:3000/graphql`); }); ``` You can also use it in the `embedded` mode for a self-hosted version: JavaScript ``` nitroMiddleware({ mode: "embedded" }); // for `embedded` version ``` ## Extended configuration ### Pin a specific version To pin a specific version instead of using "latest": JavaScript ``` nitroMiddleware({ mode: "cdn", target: { version: "3.0.0" }, }); ``` ### Use your own infrastructure To use your own infrastructure: JavaScript ``` nitroMiddleware({ mode: "cdn", target: "https://mycompany.com/nitro", }); ``` ### Custom options To pass options supported by Nitro GraphQL IDE: JavaScript ``` nitroMiddleware({ mode: "cdn", options: { title: "Nitro", }, }); ``` | Property | Description | Type | | ----------------------- | ------------------------------------------------------------- | ------------------------------- | | title | The title of the Nitro IDE. | string (optional) | | graphQLDocument | Specifies the GraphQL document (query/mutation/subscription). | string (optional) | | variables | Specifies the variables used in the GraphQL document. | Record (optional) | | includeCookies | If true, includes cookies in the request. | boolean (optional) | | httpHeaders | Specifies HTTP headers for the request. | HttpHeaderDictionary (optional) | | endpoint | The GraphQL endpoint. | string (optional) | | useGet | If true, uses GET method for sending the request. | boolean (optional) | | useBrowserUrlAsEndpoint | If true, uses the browser's URL as the GraphQL endpoint. | boolean | | subscriptionProtocol | Specifies the protocol used for GraphQL subscriptions. | SubscriptionProtocol (optional) | ## Recipes Below are examples of how to use Nitro Express Middleware with different GraphQL server setups. ### graphql-http JavaScript ``` import express from "express"; import { createHandler } from "graphql-http"; //... rest of the imports const app = express(); //... rest of the app setup app.use( "/graphql", nitroMiddleware({ mode: "cdn" }), // or nitroMiddleware({ mode: "embedded" }), async (req, res) => { //... rest of the middleware }, ); app.listen(3000, () => { console.log(`GraphQL on http://localhost:3000/graphql`); }); ``` ### graphql-yoga JavaScript ``` import express from "express"; import { createYoga, createSchema } from "graphql-yoga"; //... rest of the imports const app = express(); //... rest of the app setup app.use( "/graphql", nitroMiddleware({ mode: "cdn" }), // or nitroMiddleware({ mode: "embedded" }), graphQLServer, ); app.listen(3000, () => { console.log(`GraphQL on http://localhost:3000/graphql`); }); ``` ### express-graphql JavaScript ``` import express from "express"; import { graphqlHTTP } from "express-graphql"; //... rest of the imports const app = express(); //... rest of the app setup app.use( "/graphql", nitroMiddleware({ mode: "cdn" }), // or nitroMiddleware({ mode: "embedded" }), graphqlHTTP({ schema, graphiql: false, }), ); app.listen(3000, () => { console.log(`GraphQL on http://localhost:3000/graphql`); }); ``` ### Apollo Server JavaScript ``` import { ApolloServer } from "@apollo/server"; //... rest of the imports const app = express(); //... rest of the app setup app.use( "/graphql", nitroMiddleware({ mode: "cdn" }), // or nitroMiddleware({ mode: "embedded" }), cors(), bodyParser.json(), expressMiddleware(server, { context: async ({ req }) => ({ token: req.headers.token }), }), ); httpServer.listen({ port: 3000 }, () => { console.log(`GraphQL on http://localhost:3000/graphql`); }); ``` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/integrations/express.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # .Net Middleware - Nitro > Serve Nitro from your Hot Chocolate server: MapGraphQL() hosts it at /graphql by default, or use MapNitroApp() to expose the IDE on a separate endpoint. Canonical source: https://chillicream.com/docs/nitro/integrations/hot-chocolate By default, when you map your GraphQL endpoints using `MapGraphQL()`, Nitro is automatically served at the `/graphql` endpoint. C# ``` app.UseEndpoints(endpoints => { endpoints.MapGraphQL(); }); ``` In the example above, the GraphQL service and Nitro are both mapped to the `/graphql` endpoint. If you want to serve Nitro on a separate endpoint, you can use `MapNitroApp()` method: C# ``` app.UseEndpoints(endpoints => { endpoints.MapGraphQL(); endpoints.MapNitroApp("/my-graphql-ui"); }); ``` In this configuration, the GraphQL service remains at the `/graphql` endpoint, and Nitro is served at the `/my-graphql-ui` endpoint. ## Disable the Middleware In some scenarios, you may not want to serve Nitro, e.g., in a production environment. You can disable Nitro by setting the `Enable` property to `false`: C# ``` endpoints .MapGraphQL() .WithOptions(o => o.Enable = false); ``` ## Serve Modes The `ServeMode` property controls which version of Nitro to serve. The default mode is `Latest`, serving the most recent version of Nitro from a CDN. You can also serve the embedded version (`Embedded`) of Nitro, which is included in the package. - `Latest`: Serves the latest version of Nitro from a CDN. - `Insider`: Serves the insider version of Nitro from a CDN, allowing preview of upcoming features. - `Embedded`: Serves the embedded version of Nitro that comes with the package. - `Version(string version)`: Serves a specific version of Nitro from the CDN. Depending on your environment or preferences, you can choose the appropriate mode: C# ``` endpoints .MapNitroApp() .WithOptions(o => o.ServeMode = ServeMode.Embedded); ``` ## Configuration Options You can tailor Nitro to your needs by setting various options via `NitroAppOptions`. You can specify these options using the `WithOptions()` method in both `MapGraphQL()` and `MapNitroApp()` methods. | Property | Type | Description | | ------------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Enable | bool | If false, disables the Nitro tool. | | ServeMode | ServeMode | Defines how Nitro is served. Options include Latest (default), Insider, Embedded, and Version(string version). | | Title | string | Specifies the title of the Nitro page. | | Document | string | Specifies the default document content. | | IncludeCookies | bool? | If true, includes cookies in the HTTP call to the GraphQL backend. | | HttpHeaders | IHeaderDictionary | Specifies the default HTTP headers for Nitro. | | UseGet | bool | If true, uses HTTP GET as the default request method. | | GraphQLEndpoint | string | Specifies the GraphQL endpoint. If UseBrowserUrlAsGraphQLEndpoint is true, it must be a relative path; otherwise, it must be an absolute URL. | | UseBrowserUrlAsGraphQLEndpoint | bool | If true, the schema endpoint URL is inferred from the browser URL. | Here is an example of how to set these options: C# ``` endpoints .MapNitroApp() .WithOptions(o => { o.ServeMode = ServeMode.Insider; o.Title = "My GraphQL API"; o.Document = "Query { hello }"; o.GraphQLEndpoint = "/api/graphql"; o.IncludeCookies = true; o.Enable = true; }); ``` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/integrations/hot-chocolate.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Migrating from 1 to 16 - Nitro > Migration guide for updating the Nitro packages from 1.x to 16.x: renamed package IDs, the removed NitroOptions class, and per-component configuration. Canonical source: https://chillicream.com/docs/nitro/migration/migrate-from-1-to-16 Starting with version 16, the packages align their version numbers with the rest of the platform (HotChocolate, Fusion, Mocha). This means you are migrating from `1.x` to `16.x`. The packages have been restructured to separate connection settings from feature configuration. The old monolithic `NitroOptions` class has been replaced, feature options are now configured per platform component, and whenever possible, have been consolidated into common packages. ## Update Package References | Old Package ID | New Package ID | | ---------------------------- | ------------------------------- | | ChilliCream.Nitro.Core | ChilliCream.Nitro.GraphQL | | ChilliCream.Nitro | ChilliCream.Nitro.HotChocolate | | ChilliCream.Nitro.Telemetry | ChilliCream.Nitro.OpenTelemetry | | ChilliCream.Nitro.Azure.Core | ChilliCream.Nitro.Azure | `ChilliCream.Nitro.Abstractions` is unchanged. Warning The old `ChilliCream.Nitro` package becomes `ChilliCream.Nitro.HotChocolate`. A new meta-package is published under the old `ChilliCream.Nitro` ID. Don't mix these up when referencing. ### Azure packages consolidated The old per-component Azure packages (`ChilliCream.Nitro.HotChocolate.Azure`, `ChilliCream.Nitro.Fusion.Azure`) have been replaced by a single `ChilliCream.Nitro.Azure` package. Remove the old ones and add the new one. ## API Changes ### `services.AddNitro()` returns builder The return type changed from `IServiceCollection` to `INitroBuilder`. This enables fluent chaining for add-on packages like OpenTelemetry and Azure asset caching. If you were chaining off `IServiceCollection`, capture the builder or access `.Services`: C# ``` // Before services .AddNitro(options => { options.ApiId = "my-api"; }) .AddSingleton(); // After services.AddNitro(options => { options.ApiId = "my-api"; }); services.AddSingleton(); ``` ### `AddNitro()` configures connection settings only The `IServiceCollection.AddNitro()` overload accepts `Action`, which contains only connection properties. Feature configuration happens on the GraphQL builder via `ModifyNitroOptions()`. C# ``` services.AddNitro(options => { // Connection settings options.ApiId = "my-api"; options.ApiKey = "my-key"; options.Stage = "production"; options.ServerUrl = "https://api.chillicream.com"; options.TelemetryUrl = "https://otel.chillicream.com"; // Feature options like Metrics and PersistedOperations // are no longer available here. }); ``` ### HotChocolate: `AddNitro()` is now `ModifyNitroOptions()` The method on `IRequestExecutorBuilder` has been renamed from `AddNitro()` to `ModifyNitroOptions()`, and the options type changed to `NitroHotChocolateOptions`. The `ModifyNitroOptions()` method is optional. If you don't need to configure any per-schema options, you can omit it entirely. **Before** C# ``` services .AddGraphQLServer() .AddNitro(options => { options.ApiId = "my-api"; options.EnablePersistedQueries = true; options.Metrics.Enabled = true; }); ``` **After** C# ``` services.AddNitro(options => { options.ApiId = "my-api"; }); services .AddGraphQLServer() .ModifyNitroOptions(options => { options.PersistedOperations.Enabled = true; options.Metrics.Enabled = true; }); ``` If you do not need to modify any per-schema options, you can omit the `ModifyNitroOptions()` call entirely. ### Fusion: `ConfigureFromCloud()` is now `AddNitro().AddDefaults()` **Before** C# ``` builder.Services .AddFusionGatewayServer() .ConfigureFromCloud(options => { options.ApiId = "my-gateway"; options.ApiKey = "my-key"; options.Stage = "production"; }); ``` **After** C# ``` builder.Services .AddNitro(options => { options.ApiId = "my-gateway"; options.ApiKey = "my-key"; options.Stage = "production"; }) .AddDefaults(); builder.Services.AddGraphQLGatewayServer(); ``` Per-gateway feature options can be overridden via `ModifyNitroOptions()`: C# ``` builder.Services .AddGraphQLGatewayServer() .ModifyNitroOptions(options => { options.PersistedOperations.Enabled = true; options.Metrics.Enabled = true; options.OperationReporting.Enabled = true; }); ``` If you do not need to modify any per-gateway options, you can omit the `ModifyNitroOptions()` call entirely. ### OTLP exporters: `AddNitroExporter()` replaced by `AddOpenTelemetry()` `AddOpenTelemetry()` on `INitroBuilder` registers the Nitro OTLP exporters on the meter, tracer, and logger providers. You no longer wire each exporter by hand. **Before** C# ``` services.ConfigureOpenTelemetryMeterProvider(x => x.AddNitroExporter()); services.ConfigureOpenTelemetryTracerProvider(x => x.AddNitroExporter()); services.ConfigureOpenTelemetryLoggerProvider(x => x.AddNitroExporter()); ``` **After** C# ``` services .AddNitro(options => { options.ApiId = "my-api"; options.TelemetryUrl = "https://otel.chillicream.com"; }) .AddOpenTelemetry(); ``` ### `AddNitroTelemetry` replaced If you were using `AddNitroTelemetry` for non-GraphQL service monitoring, replace it with `AddNitro().AddOpenTelemetry()`: **Before** C# ``` services.AddNitroTelemetry(options => { options.ApiId = apiId; options.ApiKey = apiKey; options.Stage = stage; }); ``` **After** C# ``` services .AddNitro(options => { options.ApiId = apiId; options.ApiKey = apiKey; options.Stage = stage; }) .AddOpenTelemetry(); ``` ### Asset cache is now global on `INitroBuilder` The asset cache is no longer configured per-schema on the request executor builder. Instead, it is a global singleton configured on `INitroBuilder`. A default file system cache is registered automatically. **Before** C# ``` services .AddGraphQLServer() .AddNitro() .AddBlobStorageAssetCache(o => { o.Client = blobServiceClient; o.ContainerName = "my-container"; }); ``` **After** C# ``` services .AddNitro(options => { options.ApiId = "my-api"; options.ApiKey = "my-key"; }) .AddBlobStorageAssetCache(o => { o.Client = blobServiceClient; o.ContainerName = "my-container"; }); ``` ## Source Generator: `AddDefaults()` The `ChilliCream.Nitro` meta-package ships with a Roslyn source generator. ### Compile-time warnings for missing integration packages If your project references HotChocolate or Fusion but is missing the corresponding Nitro integration package, you get a compile-time warning: | Warning | Trigger | Fix | | ---------- | --------------------------------------------------------------- | ------------------------- | | **NS0001** | HotChocolate referenced without ChilliCream.Nitro.HotChocolate | Add the package reference | | **NS0002** | HotChocolate.Fusion referenced without ChilliCream.Nitro.Fusion | Add the package reference | ### Generated `AddDefaults()` extension method When the correct integration package is referenced, the generator emits an `AddDefaults()` extension method on `INitroBuilder`. This method wires up the default Nitro integration with the GraphQL pipeline in a single call. For HotChocolate projects, `AddDefaults()` is equivalent to calling `services.AddGraphQLServer().ModifyNitroOptions()`. For Fusion projects, it is equivalent to calling `services.AddFusionGatewayServer().ModifyNitroOptions()`. C# ``` builder.Services .AddNitro(options => { options.ApiId = "my-api"; options.ApiKey = "my-key"; options.Stage = "production"; }) .AddDefaults(); ``` You can still customize per-schema options by calling `ModifyNitroOptions()` on the builder afterwards: C# ``` builder.Services .AddNitro(options => { options.ApiId = "my-api"; options.ApiKey = "my-key"; }) .AddDefaults(); builder.Services .AddGraphQLServer() .AddQueryType() .ModifyNitroOptions(options => { options.PersistedOperations.Enabled = true; options.Metrics.Enabled = true; }); ``` ## Complete Before/After Examples ### HotChocolate Standalone **Before** C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddNitro(options => { options.ApiId = "my-api"; options.ApiKey = "my-key"; options.Stage = "production"; options.EnablePersistedQueries = true; options.Metrics.Enabled = true; }); builder.Services .AddGraphQLServer() .AddQueryType() .AddNitro(); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` **After** C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddNitro(options => { options.ApiId = "my-api"; options.ApiKey = "my-key"; options.Stage = "production"; }) .AddDefaults(); builder.Services .AddGraphQLServer() .AddQueryType() .ModifyNitroOptions(options => { options.PersistedOperations.Enabled = true; options.Metrics.Enabled = true; }); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` ### Fusion Gateway **Before** C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddFusionGatewayServer() .ConfigureFromCloud(options => { options.ApiId = "my-gateway"; options.ApiKey = "my-key"; options.Stage = "production"; }); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` **After** C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddNitro(options => { options.ApiId = "my-gateway"; options.ApiKey = "my-key"; options.Stage = "production"; }) .AddDefaults(); builder.Services.AddGraphQLGatewayServer(); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` ### With OpenTelemetry **Before** C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddGraphQLServer() .AddQueryType() .AddNitro(options => { options.ApiId = "my-api"; options.Metrics.Enabled = true; }); services.ConfigureOpenTelemetryMeterProvider(x => x.AddNitroExporter()); services.ConfigureOpenTelemetryTracerProvider(x => x.AddNitroExporter()); services.ConfigureOpenTelemetryLoggerProvider(x => x.AddNitroExporter()); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` **After** C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddNitro(options => { options.ApiId = "my-api"; options.ApiKey = "my-key"; options.Stage = "production"; options.TelemetryUrl = "https://otel.chillicream.com"; }) .AddDefaults() .AddOpenTelemetry(); builder.Services .AddGraphQLServer() .AddQueryType() .ModifyNitroOptions(options => { options.Metrics.Enabled = true; }); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` ### With Azure Blob Storage Asset Cache **Before** C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddGraphQLServer() .AddQueryType() .AddNitro() .AddBlobStorageAssetCache(o => { o.Client = blobServiceClient; o.ContainerName = "assets"; }); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` **After** C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddNitro(options => { options.ApiId = "my-api"; options.ApiKey = "my-key"; }) .AddDefaults() .AddBlobStorageAssetCache(o => { o.Client = blobServiceClient; o.ContainerName = "assets"; }); builder.Services .AddGraphQLServer() .AddQueryType(); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/migration/migrate-from-1-to-16.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Connect your API - Nitro > Connect your Hot Chocolate server to Nitro to fetch persisted operations from the client registry and report telemetry, using the ChilliCream.Nitro packages. Canonical source: https://chillicream.com/docs/nitro/nitro-services Nitro can be smoothly integrated into your HotChocolate server, enabling utilization of the Persisted Operation Storage found within the client registry, to report operations and collect open telemetry. Your server will establish a connection with Nitro, retrieving persisted operations based on their unique hashes. Additional information on the client registry can be found [here](https://chillicream.com/docs/nitro/apis/client-registry). ## Getting Started To get started, follow these steps: 1. Set up a client registry as instructed [here](https://chillicream.com/docs/nitro/apis/client-registry). 2. Install the Nitro package from NuGet using the following command: Bash ``` dotnet add package ChilliCream.Nitro dotnet add package ChilliCream.Nitro.HotChocolate ``` 1. Configure your services as shown in the following code snippet: C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddNitro(x => { x.ApiId = "VGhpcyBpcyBub3QgYSByZWFsIGFwaSBpZA=="; x.ApiKey = "Tm9wZSwgdGhpcyBpcyBhbHNvIG5vIHJlYWwga2V5IDspIA=="; x.Stage = "dev"; }); builder .AddGraphQL() .AddQueryType() .AddInstrumentation() // if you want to use telemetry .UseOnlyPersistedOperationAllowed() // optional .UsePersistedOperationPipeline(); // if you want to use persisted operations var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` 1. Retrieve the API id and API key from Nitro using the `nitro api list` and `nitro api-key create` commands respectively. Instructions for these commands can be found [here](https://chillicream.com/docs/nitro/cli/installation). Congratulations! You have successfully integrated Nitro into your HotChocolate server. You can now publish new versions of your clients and your server will automatically retrieve the latest persisted operations. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/nitro-services.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Logging - Nitro > Collect and analyze OpenTelemetry logs in Nitro: browse the Logs tab of each API, expand entries for details, and inspect logs within individual traces. Canonical source: https://chillicream.com/docs/nitro/open-telemetry/logging Nitro includes Open Telemetry logging, allowing seamless log collection and analysis directly within the app. This documentation provides guidance on setting up and utilizing logging features in Nitro for enhanced monitoring, debugging, and performance analysis of your APIs. ## API Logs ![Api Logs](https://chillicream.com/images/nitro-docs/open-telemetry/logs-0.webp)Each API in Nitro features a **Logs** tab, providing a centralized interface for viewing and managing logs associated with your API. This unified log view offers insights into your system’s activities, enabling you to monitor and troubleshoot in real-time. ### Detailed Log Inspection ![API Logs - Expanded](https://chillicream.com/images/nitro-docs/open-telemetry/logs-1.webp) Within the Logs tab, individual log entries can be expanded to reveal additional details such as timestamps, log levels, and message content. ## Trace Logs ![Trace Logs](https://chillicream.com/images/nitro-docs/open-telemetry/logs-2.webp) Logs can also be inspected within individual traces, providing detailed insights into the correlation between specific traces and their corresponding logs. ## Log Retention Log retention in Nitro is configured as follows: - **Shared Clusters:** Logs are retained for 1 day to allow for recent log review. - **Dedicated Clusters:** Dynamic log retention times can be configured to meet specific needs, offering flexibility in log management. ## Connect your service All the logging is done on a per API basis. An api represents one of your deployments. To monitor you services you need to create an API in Nitro. The api needs to be from type "Api Service" or "Api Gateway". To install the Nitro services, run the following commands in your project's root directory: Bash ``` dotnet add package ChilliCream.Nitro dotnet add package ChilliCream.Nitro.HotChocolate dotnet add package ChilliCream.Nitro.OpenTelemetry dotnet add package OpenTelemetry.Extensions.Hosting dotnet add package OpenTelemetry.Instrumentation.AspNetCore ``` After installing the package, you need to configure the services in your startup class. Below is a sample implementation in C#: C# ``` public void ConfigureServices(IServiceCollection services) { services .AddNitro(x => { x.ApiKey = "<>"; x.ApiId = "QXBpCmc5NGYwZTIzNDZhZjQ0NjBmYTljNDNhZDA2ZmRkZDA2Ng=="; x.Stage = "dev"; }) .AddOpenTelemetry(); services .AddGraphQL() .AddQueryType() .AddInstrumentation(); services .AddLogging(x => x .AddOpenTelemetry(options => { options.IncludeFormattedMessage = true; options.IncludeScopes = true; })); } ``` Tip **Using Environment Variables** Alternatively, you can set the required values using environment variables. This method allows you to call `AddNitro` without explicitly passing parameters. - `NITRO_API_KEY` maps to `ApiKey` - `NITRO_API_ID` maps to `ApiId` - `NITRO_STAGE` maps to `Stage` C# ``` public void ConfigureServices(IServiceCollection services) { services .AddNitro() .AddOpenTelemetry(); services .AddGraphQL() .AddQueryType() .AddInstrumentation(); // Enable the graphql telemetry services .AddLogging(x => x .AddOpenTelemetry(options => { options.IncludeFormattedMessage = true; options.IncludeScopes = true; })); } ``` In this setup, the API key, ID, and stage are set through environment variables. ## Full Example For a complete implementation example, visit the [example repository](https://link.chillicream.com/docs/logging-example). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/open-telemetry/logging.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Operation Monitoring - Nitro > Visualize OpenTelemetry traces for your GraphQL operations in Nitro to spot slow queries, analyze resolver impact, and monitor your API's performance. Canonical source: https://chillicream.com/docs/nitro/open-telemetry/operation-monitoring ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-0.webp) Nitro can collect your [Open Telemetry](https://opentelemetry.io/) data and visualize your traces in the app. With telemetry you can get a better understanding of how your application is performing and where you can improve it. ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-1.webp) It helps you to understand which resolver is impacting your system the most, which queries are slow and which are fast and deeply analyze each trace to your system. ### Connect your service to the telemetry system All the reporting is done on a per API basis. An api represents one of your deployments. To monitor you services you need to create an API in Nitro. The api needs to be from type "Api Service" or "Api Gateway". Note You can use Nitro to monitor any .NET service, not just GraphQL APIs. This includes REST APIs, gRPC services, and background jobs. Checkout the [Service Monitoring](https://chillicream.com/docs/nitro/open-telemetry/service-monitoring) section for more details. To install the Nitro services, run the following commands in your project's root directory: Bash ``` dotnet add package ChilliCream.Nitro dotnet add package ChilliCream.Nitro.HotChocolate dotnet add package ChilliCream.Nitro.OpenTelemetry dotnet add package OpenTelemetry.Extensions.Hosting dotnet add package OpenTelemetry.Instrumentation.AspNetCore ``` After installing the package, you need to configure the services in your startup class. Below is a sample implementation in C#: C# ``` public void ConfigureServices(IServiceCollection services) { services .AddNitro(x => { x.ApiKey = "<>"; x.ApiId = "QXBpCmc5NGYwZTIzNDZhZjQ0NjBmYTljNDNhZDA2ZmRkZDA2Ng=="; x.Stage = "dev"; }) .AddOpenTelemetry(); services .AddGraphQL() .AddQueryType() .AddInstrumentation(); // Enable the graphql telemetry services .AddOpenTelemetry() .WithTracing(x => { x.AddAspNetCoreInstrumentation(); }); } ``` Tip **Using Environment Variables** Alternatively, you can set the required values using environment variables. This method allows you to call `AddNitro` without explicitly passing parameters. - `NITRO_API_KEY` maps to `ApiKey` - `NITRO_API_ID` maps to `ApiId` - `NITRO_STAGE` maps to `Stage` C# ``` public void ConfigureServices(IServiceCollection services) { services .AddNitro() .AddOpenTelemetry(); services .AddGraphQL() .AddQueryType() .AddInstrumentation(); // Enable the graphql telemetry services .AddOpenTelemetry() .WithTracing(x => { x.AddAspNetCoreInstrumentation(); }); } ``` In this setup, the API key, ID, and stage are set through environment variables. ## Monitoring Dashboard The monitoring dashboard in Nitro offers various metrics and visualizations to understand your service's performance better. ### Changing the time range ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-2.webp) You can change the time range of the dashboard by clicking on the time range selector in the top right corner of the dashboard. You can either customize the time range or select one of the predefined ranges. ### Latency ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-3.webp) The latency graph shows the average latency of your service over time. You can also see the 95th and the 99th percentile of the latency. ### Throughput ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-4.webp) The throughput graph shows you the operations per minute over time. You can see how many operations are executed on your service and how many of them failed. ### Clients ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-5.webp) You can track how many requests are done by each client. This helps you to understand which client is impacting your system the most. To track this, your clients need to send two headers with each request: - `GraphQL-Client-Id` \- The ID of the client. You can get the ID from the client by executing `nitro client list` in your terminal. - `GraphQL-Client-Version` \- The version of the client. ### Failed Operations ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-6.webp) Shows you the number of failed operations over time. ### Errors ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-7.webp) Shows error details. ### Insights ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-8.webp) Insights shows you a list of executed operations. You can see the latency, the throughput and also how many percent of the operations had errors. You also have a column impact, which will help you to understand which operations are impacting your system the most. You can sort the columns by clicking on the column header. By clicking on an operation, you can see the telemetry information about the operation and its traces. ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-9.webp) On the top right corner you can change from the operation insights to the resolver insights view. ## Operation Dashboard ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-1.webp) You can drill down into the telemetry information of a single operation by clicking on it in the insights view. ### Latency, Throughput, and Errors Theses graphs show you the latency, throughput, and error rate of the selected operation over time similar to the graphs on the monitoring dashboard. ### Latency Distribution ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-10.webp) This graph shows you the distribution of different traces of the selected operation. This way you can quickly see outliers and understand how the operation is performing. ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-11.webp) You can also select a time range in the graph. This selection will impact which traces are shown in the trace table. You can for example select the slow operations on the right and inspect why they are slow. ### Traces ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-12.webp) On the very bottom of the page you see sample traces of the selected operation with all of the spans. If you click on a trace, the sidebar will show additional information about the trace, including all of its attributes. ![Image](https://chillicream.com/images/nitro-docs/open-telemetry/telemetry-13.webp) [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/open-telemetry/operation-monitoring.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Service Monitoring - Nitro > Centralize logs and traces from any .NET service in Nitro with the ChilliCream.Nitro.OpenTelemetry package, covering REST APIs and background workers. Canonical source: https://chillicream.com/docs/nitro/open-telemetry/service-monitoring Nitro’s OpenTelemetry support extends beyond GraphQL, allowing you to gather and analyze telemetry data from any .NET application. Whether you have REST APIs, background workers, or other services, you can seamlessly centralize logging and tracing in Nitro for a unified observability experience. Note If you are looking specifically for GraphQL telemetry, please refer to the [Operation Monitoring](https://chillicream.com/docs/nitro/open-telemetry/operation-monitoring#connect-your-service-to-the-telemetry-system) section. ## Prerequisites 1. **.NET Application**: You’ll need a .NET project (e.g., ASP.NET Core, worker service, etc.) where you want to enable telemetry. 2. **ChilliCream.Nitro.OpenTelemetry**: Make sure to reference the `ChilliCream.Nitro` meta-package and the `ChilliCream.Nitro.OpenTelemetry` package. 3. **OpenTelemetry**: Have the OpenTelemetry packages or extensions configured in your project. ## Quick Start ### 1\. Install Required Packages In your .NET project, install the following NuGet packages if they are not already present: ``` dotnet add package ChilliCream.Nitro dotnet add package ChilliCream.Nitro.OpenTelemetry dotnet add package OpenTelemetry --version dotnet add package OpenTelemetry.Extensions.Hosting --version ``` ### 2\. Register Nitro and OpenTelemetry Exporters Register the Nitro connection and OpenTelemetry exporters. Call `AddNitro` with your API credentials, then chain `AddOpenTelemetry()` to register the OTLP exporters for tracing, metrics, and logging. For example, in your `Program.cs` or `Startup.cs`: C# ``` services .AddNitro(options => { options.ApiId = apiId; // Replace with your Nitro API ID options.ApiKey = apiKey; // Replace with your Nitro API Key options.Stage = stage; // Replace with your environment or stage name }) .AddOpenTelemetry(); ``` ### 3\. Add Additional Instrumentation You can continue configuring OpenTelemetry providers for non-Nitro instrumentation as needed, such as for ASP.NET Core or HTTP requests. ``` dotnet add package OpenTelemetry.Extensions.Hosting dotnet add package OpenTelemetry.Instrumentation.AspNetCore ``` C# ``` services.ConfigureOpenTelemetryTracerProvider(x => { x.AddAspNetCoreInstrumentation(); }); ``` ### 4\. View Your Traces Once your service is running, head over to the **Nitro dashboard**. - Select your API. - Choose **OpenTelemetry** from the trace overview dropdown. You’ll see a unified view of all the HTTP requests, background worker jobs, or other .NET processes you’re tracking through OpenTelemetry. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/open-telemetry/service-monitoring.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Organizations - Nitro > Create and manage organizations in Nitro, the structure that hosts your workspaces and members, including the personal organization and management portal. Canonical source: https://chillicream.com/docs/nitro/organizations This guide will walk you through the features and functionalities of Nitro Organizations, a structure designed to help teams and individuals work efficiently and effectively on GraphQL related services. ## What is an Organization An organization typically represents an entire entity or a larger unit within an entity. Each organization can host multiple workspaces, allowing further categorization and organization of resources. A workspace might correspond to a specific project, a specific team within the organization, or a group of related APIs. Each Nitro user has a personal organization. This is a private space for individual work or for projects that are not associated with any team or company organization. The personal organization is not visible to other users, is created by default and cannot be deleted. ## Creating an Organization Nitro allows for multiple organizations under one account. To add a new organization, navigate to the organization switcher in the status bar and click on the "Add Organization" button. ![Screenshot showing the "Add Organization" button in the organization switcher](https://chillicream.com/images/nitro-docs/organizations/create-0.webp) You will be redirected to the management portal. Here you can create a new organization by clicking the 'Create' button. ![Picture showing the create button in the management portal](https://chillicream.com/images/nitro-docs/organizations/create-1.webp) Each organization has a unique name and a display name which can be set during creation. The name must be lowercase and may only include dashes (-) and underscores (\_). ![Screenshot of the create organization screen](https://chillicream.com/images/nitro-docs/organizations/create-2.webp) ## Managing Organizations To manage your organizations, you have to open the management portal. ![Screenshot showing the manage button in the organization switcher](https://chillicream.com/images/nitro-docs/organizations/manage-0.webp) In the client app, you can do this by clicking 'Manage Organizations' and then 'Manage' on the organization that you'd like to manage. You can also navigate to [here](https://identity.chillicream.com/Organizations) to open the management portal and press 'Manage' on the organization that you like to manage. ## Switching Organizations In Nitro, you can be signed into multiple organizations at the same time and switch between them in the status bar. The organization switcher also allows you to create new organizations. ![Screenshot showing the organization switcher in the status bar](https://chillicream.com/images/nitro-docs/organizations/switch-0.webp) ## Managing Members ![Screenshot showing the invite user button and the process](https://chillicream.com/images/nitro-docs/organizations/members-0.webp) 1. The count of members and the number of seats available in your subscription are displayed in the management overview section. (members/total seats) 2. This list show all the members of the organization and their role. 3. Members can be invited to join an organization via email. In the management overview section, the 'Invite User' section allows you to send an invitation email containing a join link. 4. Open invitations are displayed below. You can revoke an invitation by clicking on the 'Cancel' button. Once an invitation is expired, you can resend it by clicking on the 'Resend' button. ### Joining an Organization Invited users can join an organization in two ways: 1. By clicking on the join link they received via email. 2. By opting to join during login on the sign in page. During sign-in, the user can select into which organization (join orgs or personal) they want to sign in. \[Description of visualization: Screenshot showing the organization selection during sign-in\] ## User Roles and Access There are three distinct roles within an organization: Owner, Admin, and Collaborator. Each role carries specific permissions: | Role | Transfer Ownership | Delete Organization | Invite Users | View Subscriptions | Add Redirect URLs | | ------------ | ------------------ | ------------------- | ------------ | ------------------ | ----------------- | | Owner | Yes | Yes | Yes | Yes | Yes | | Admin | No | No | Yes | Yes | Yes | | Collaborator | No | No | No | No | No | - **Owner:** The owner can transfer ownership, delete the organization, and has all the permissions of an admin. Each organization can only have one owner. - **Admin:** Admins can invite users to the organization, view subscriptions, and add redirect URLs. They also have all the permissions of a collaborator. - **Collaborator:** Collaborators can log into the organization and leave the organization. They cannot add redirect URLs or invite new users. ### Managing Permissions ![Screenshot showing the invite user button and the process](https://chillicream.com/images/nitro-docs/organizations/members-1.webp) You can manage the permissions of each member in the member list. 1. Press the 'Edit' button on the right side of the member list to open the edit dialog. 2. Select the desired role for the member. 3. Press the 'Save' button to save the changes. 4. You can also remove a member from the organization by clicking on the 'Revoke Access' button. ## Transferring Ownership Ownership of the organization can be transferred to any existing member from the management portal. In the danger zone section, click on the 'Transfer' button to transfer ownership. ![Screenshot showing the transfer ownership button and the process](https://chillicream.com/images/nitro-docs/organizations/danger-zone-0.webp) ## Redirect URLs Each time a user signs into an organization from a new origin, a redirect URL must be added to the organization. This security measure is designed to prevent your access tokens from being leaked to any arbitrary site hosted by third parties. If the user is a collaborator, they won't have the permissions to add the URL and will see an informative message. An admin, on the other hand, can directly add the URL to the organization. You can add the redirect URL in two ways: 1. By signing in from the new origin. This will trigger a prompt to add the URL to the organization. ![Screenshot showing the redirect URL prompt](https://chillicream.com/images/nitro-docs/organizations/redirect-0.webp) 1. By adding the URL directly from the management portal. ![Screenshot showing the redirect URL prompt](https://chillicream.com/images/nitro-docs/organizations/redirect-1.webp) In the portal you have more options to configure the allowed destinations. You can for example allow all localhost origins. Or disable the access to the web UI in the [cloud](https://nitro.chillicream.com/) 1. Configure the allowed types of origins. 2. Add redirect Urls to the organization. 3. Remove redirect Urls from the organization. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/organizations/index.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Settings - Overview - Nitro > Overview of the Settings section in Nitro, reached via the cog icon, covering customization options like Themes and where to find organization settings. Canonical source: https://chillicream.com/docs/nitro/settings ![Image](https://chillicream.com/images/nitro-docs/settings/settings-0.webp) The Settings section provides users with various customization options. To access the settings, click on the cog icon located in the navigation bar on the left. Currently, the most significant setting available is the [Themes](https://chillicream.com/docs/nitro/settings/themes) feature. This allows users to personalize the appearance and visual style of the application. Please note that organization settings are exclusively accessible through the management portal. For more information about organization settings, refer to the [Organizations](https://chillicream.com/docs/nitro/organizations) documentation. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/settings/index.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Key Bindings - Nitro > Keyboard shortcut reference for Nitro on Mac and Windows: key bindings for documents, tabs, formatting, saving, and navigating the application quickly. Canonical source: https://chillicream.com/docs/nitro/settings/key-bindings This document provides a list of useful keyboard shortcuts for performing various actions in the application. A full list of all available keyboard shortcuts can be found by clicking on the 'Keyboard Shortcuts' button in the status bar. ![Screenshot showing the keyboard shortcuts button in the status bar](https://chillicream.com/images/nitro-docs/settings/key-bindings-0.webp) ## Mac ### Application Shortcuts - Open New Document Tab: ⌘ + ⌥ + T - Open New Document Tab from Clipboard: ⌘ + ⌥ + V - Toggle Sidebar (Collapse/Expand): ⌘ + B - Open Document Explorer: ⌘ + ⇧ + D - Open History Explorer: ⌘ + ⇧ + H - Open Settings: ⌘ + , - Open Keyboard Shortcuts: ⌘ + K S ### Document Shortcuts - Copy Document to Clipboard as cURL: ⌘ + ⌥ + C - Close Document: ⌘ + ⌥ + W - Save Document: ⌘ + S - Save All Documents: ⌘ + ⌥ + S - Format Document: ⇧ + ⌥ + F - Toggle Operation (Run/Cancel): ⌘ + ⌥ + ⏎ - Reload Schema: ⌘ + ⌥ + R - Show Operation: ⌘ + ⇧ + O - Show Schema: ⌘ + ⇧ + S ## Windows ### Application Shortcuts - Open New Document Tab: CTRL + ALT + T - Open New Document Tab from Clipboard: Application Key + W (Windows key + W) - Toggle Sidebar (Collapse/Expand): Application Key + B (Windows key + B) - Open Document Explorer: CTRL + SHIFT + D - Open History Explorer: CTRL + SHIFT + H - Open Settings: Application Key + , (Windows key + comma) - Open Keyboard Shortcuts: CTRL + K S ### Document Shortcuts - Copy Document to Clipboard as cURL: CTRL + ALT + C - Close Document: CTRL + ALT + W - Save Document: CTRL + S - Save All Documents: CTRL + ALT + S - Format Document: SHIFT + ALT + F - Toggle Operation (Run/Cancel): CTRL + ALT + Enter - Reload Schema: CTRL + ALT + R - Show Operation: CTRL + SHIFT + O - Show Schema: CTRL + SHIFT + S [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/settings/key-bindings.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Settings - Themes - Nitro > Customize the appearance of Nitro with light and dark themes, including OS theme sync and options like ChilliCream Blue and the GitHub theme family. Canonical source: https://chillicream.com/docs/nitro/settings/themes Nitro offers a range of themes to customize the visual appearance of the application. These themes are available as light and dark themes, providing flexibility to suit different user preferences. ![Theme Sync](https://chillicream.com/images/nitro-docs/settings/theme-0.webp) By syncing the theme mode with your operating system (OS) settings, Nitro can automatically adjust the theme based on your OS's light or dark mode. When you switch between light and dark mode on your OS, the theme in Nitro will change accordingly. You can specify a dark theme and a light theme to be used when syncing with your OS settings. You can set a default theme for light mode by clicking on the left side of the theme, or set a default theme for dark mode by clicking on the right side of the theme. Here are the available themes: **ChilliCream Blue** ![ChilliCream Blue](https://chillicream.com/images/nitro-docs/settings/theme-1.webp) **ChilliCream White** ![ChilliCream Light](https://chillicream.com/images/nitro-docs/settings/theme-2.webp) **GitHub Dark** ![GitHub Dark](https://chillicream.com/images/nitro-docs/settings/theme-3.webp) **GitHub Light** ![GitHub Light](https://chillicream.com/images/nitro-docs/settings/theme-4.webp) **GitHub High Contrast Dark** ![GitHub High Contrast Dark](https://chillicream.com/images/nitro-docs/settings/theme-5.webp) **GitHub High Contrast Light** ![GitHub High Contrast Light](https://chillicream.com/images/nitro-docs/settings/theme-6.webp) [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/settings/themes.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Subscription - Nitro > Manage your Nitro subscription in the management portal: create a Pro plan, change or cancel plans, and handle monthly or annual billing for your organization. Canonical source: https://chillicream.com/docs/nitro/subscriptions You can manage your subscriptions directly from the web portal, accessible from [here](https://identity.chillicream.com/Organization), or through Manage Organizations on the client. ## Creating a Subscription When you do not have a subscription yet, you can create one in the 'Subscription' section of the management portal. Open the organization in the context of an owner or admin to see this section. ![Screenshot showing the subscription](https://chillicream.com/images/nitro-docs/subscription-0.webp) To create a Pro subscription, click the 'Select' button. You will be redirected to the billing portal to complete the payment process. Enter your payment details and click on the 'Subscribe' button to complete the payment process. You have the option to pay monthly or annually. Annual payments are discounted. You can cancel your subscription at any time. The subscription will remain active until the end of the current billing period. You can also change your subscription plan at any time. Check out the [Changing a Subscription](#changing-a-subscription) section for more information. ## Changing a Subscription To change your subscription plan, follow these steps: 1. **Access the Management Portal:** Open the management portal by clicking ['Manage Organizations' -> 'Manage'](https://chillicream.com/docs/nitro/organizations#managing-organizations) option in the context menu of Nitro or by navigating to [here](https://identity.chillicream.com/Organizations). 2. **Navigate to Subscription:** Within the management portal, click on 'Manage' in the subscription section. ![Screenshot showing the subscription](https://chillicream.com/images/nitro-docs/subscription-2.webp) 3. **Update plan:** Here, you can update your subscription plan. Changing this value will affect the number of users who can join your organization. ![Screenshot showing the subscription](https://chillicream.com/images/nitro-docs/subscription-3.webp) ## Expiry and Seats Nitro's subscription model allows a certain number of users (seats) to join an organization based on your chosen subscription plan. If your subscription expires, all users except the organization owner will be deactivated a few days after the expiration. The deactivated users will regain access once the subscription is renewed. If your organization reaches its maximum seat limit, no new users will be able to join the organization. You can manage this by either upgrading your subscription to allow for more seats or by removing existing users from the organization to free up seats. ## Billing In the client app you can directly access the billing portal by clicking 'Billing' and managing your running subscriptions: ![Description of visualization: Screenshot showing the subscription](https://chillicream.com/images/nitro-docs/subscription-1.webp) The billing of an organization is currently on a per-user basis. All billing-related aspects, including subscription management and payment processing, are handled securely through Stripe, which you can directly manage from the management portal. > Please note that the pricing model and subscription terms are subject to change, and users are advised to stay updated by regularly checking our website or contacting our customer support team. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/subscriptions.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Workspaces - Nitro > Workspaces group APIs, documents, and environments within a Nitro organization; switch them in the status bar or create one with nitro workspace create. Canonical source: https://chillicream.com/docs/nitro/workspaces Workspaces are logical groups within an organization. A workspace could represent a team, a department, or a group of APIs that are related to each other. Each workspace has its own APIs, documents, API documents, environments, and more. You cannot share resources between workspaces. All members of an organization have default access to all workspaces and can edit the documents within them. In the future we will add more granular permissions to workspaces. In Nitro, you can switch between workspaces using the status bar. ![Screenshot showing the workspace and organization switcher in the status bar](https://chillicream.com/images/nitro-docs/workspace-0.webp) At the moment, workspaces can only be created with the Nitro CLI. To create a new workspace, use the `nitro workspace create` command. You can find out more about Nitro CLI [here](https://chillicream.com/docs/nitro/cli/installation). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/nitro/workspaces.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Mocha: Messaging Bus for .NET > Mocha is a messaging framework for .NET with a message bus for inter-service communication and a source-generated mediator for in-process CQRS. Canonical source: https://chillicream.com/docs/mocha C# ``` // Inter-service messaging via the message bus builder.Services .AddMessageBus() .AddOrderService() // source-generated handler registration .AddRabbitMQ(); // In-process CQRS via the mediator builder.Services .AddMediator() .AddHandlers(); ``` Mocha gives you two dispatch mechanisms. The **message bus** sends messages across service boundaries through a transport like RabbitMQ. The **mediator** dispatches commands, queries, and notifications within a single process using source-generated code - no reflection, no dictionary lookups. Use them independently or together. ## What Mocha is Mocha is a messaging framework for .NET with two complementary dispatch systems: - **Message bus** \- sends messages across service boundaries through transports like RabbitMQ. Supports pub/sub events, request/reply, saga orchestration, inbox/outbox reliability, and pluggable transports. Follows the patterns described in [Enterprise Integration Patterns](https://www.enterpriseintegrationpatterns.com/patterns/messaging/Introduction.html). - **Mediator** \- dispatches commands, queries, and notifications within a single process. A Roslyn source generator produces a concrete mediator class at compile time with specialized dispatch and pre-compiled pipeline delegates. No reflection, no runtime code generation. Both integrate directly into ASP.NET Core's dependency injection and are designed for [event-driven architectures](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven). Use the message bus when messages cross process boundaries. Use the mediator when you want in-process CQRS with pipeline behaviors for cross-cutting concerns like validation, logging, and transactions. Most real-world services use both: the mediator handles internal command/query dispatch, and the message bus handles inter-service events. The framework is handler-first in both cases. You implement handler interfaces, and Mocha builds the infrastructure around those declarations - whether that means wiring up transport endpoints and middleware pipelines for the bus, or generating a type-safe mediator class with pre-compiled dispatch for in-process handlers. ## Terminology These terms appear throughout the documentation. They are defined once here and used consistently everywhere. | Term | Definition | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Message** | A plain C# record representing business data. The unit of communication between handlers and services. | | **Event** | A message published via PublishAsync. Represents something that happened. Multiple handlers can receive an event. | | **Request** | A message sent via SendAsync or RequestAsync. When sent with RequestAsync, the sender awaits a typed response. When sent with SendAsync, it is fire-and-forget. | | **Handler** | A class implementing a Mocha handler interface that processes a specific message type. | | **Consumer** | The processing unit that wraps a handler or custom logic. Mocha builds consumers automatically from handlers, or you can implement IConsumer or Consumer directly. | | **Endpoint** | A named, addressable unit in the messaging topology. Each endpoint has a transport address, a middleware pipeline, and a kind (default, error, skipped, or reply). Receive endpoints consume messages; dispatch endpoints produce them. | | **Transport** | The infrastructure layer connecting Mocha to a message broker, such as RabbitMQ or an in-process channel. | | **Pipeline** | The chain of middleware that processes a message from the transport through to the handler. | | **Saga** | A long-running stateful workflow that coordinates multiple messages and transitions across services. | | **Mediator** | An in-process dispatcher that routes commands, queries, and notifications to their handlers without a transport layer. Source-generated at compile time. | | **Source generator** | A Roslyn analyzer that discovers handlers and sagas at compile time and generates typed registration code. Used by both the [mediator](https://chillicream.com/docs/mocha/mediator) and the [message bus](https://chillicream.com/docs/mocha/handler-registration). | | **Command** | A mediator message representing an action. Implements ICommand (void) or ICommand (with response). Dispatched via SendAsync. | | **Query** | A mediator message representing a read operation. Implements IQuery. Dispatched via QueryAsync. | ## Architecture When you call `PublishAsync`, here is what happens: Your code in OrderService calls `PublishAsync` with an `OrderPlaced` message **(1)**. Mocha serializes it and hands it to the dispatch endpoint **(2)**, which sends it into RabbitMQ. The broker routes the message through two exchanges into the queue (the unnumbered transport layer between the services). On the BillingService side, the receive endpoint **(3)** picks the message off the queue, runs it through the middleware pipeline, and calls `HandleAsync` on your `OrderPlacedHandler` **(4)**. Middleware in the pipeline handles cross-cutting concerns - tracing, retries, concurrency limits - without touching your handler code. ## Core capabilities ### Handler-first design You write handlers. That is the primary abstraction. Implement an interface, register it with the builder, and your handler runs when the matching message arrives. C# ``` public class OrderPlacedHandler(AppDbContext db) : IEventHandler { public async ValueTask HandleAsync( OrderPlaced message, CancellationToken cancellationToken) { var invoice = new Invoice { OrderId = message.OrderId }; db.Invoices.Add(invoice); await db.SaveChangesAsync(cancellationToken); } } ``` Mocha provides handler interfaces for each messaging pattern: | Interface | Pattern | Bus method | | -------------------------------- | ---------------------- | ---------------------- | | IEventHandler | Pub/sub events | PublishAsync | | IEventRequestHandler | Request/reply | RequestAsync | | IEventRequestHandler | Send (fire-and-forget) | SendAsync | | IBatchEventHandler | Batch processing | PublishAsync (batched) | ### Messaging patterns and consumers Mocha supports three core patterns for message-driven systems. Each pattern answers a different question: - **Events (pub/sub):** Who needs to know? Publish once, all subscribers receive it. Use `PublishAsync` and `IEventHandler`. - **Send (fire-and-forget):** Who should act? Deliver to one endpoint without waiting for a result. Use `SendAsync` and `IEventRequestHandler`. - **Request/reply:** What is the result? Send and await a typed response. Use `RequestAsync` and `IEventRequestHandler`. Handlers are the fastest way to get started, but they are not the only option. If you need full control over message processing, implement `IConsumer` for a lightweight consumer or extend `Consumer` for complete customization. ### Pluggable transports Switch transports without changing your handler code, and use multiple transports at the same time. Register a default transport, then route specific messages through a different transport when you need different throughput or delivery characteristics: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEventHandler() // Default transport for most messages .RabbitMQ() // High-throughput transport for specific messages .AddEventHub(t => t.AddEventHandler()); ``` ### Middleware pipelines Underneath, everything in Mocha is a middleware pipeline - dispatch, receive, and consumer processing are each a chain you can customize. Mocha compiles these pipelines into an optimized chain with no per-message dictionary lookups or dynamic dispatch at runtime. You can insert your own middleware at any point in the pipeline for cross-cutting concerns like logging, validation, or custom error handling. ### OpenTelemetry-native observability Every message dispatch, receive, and handler execution produces structured traces and metrics through OpenTelemetry. Correlation IDs propagate across service boundaries automatically. C# ``` builder.Services .AddMessageBus() .AddInstrumentation() .AddEventHandler() .AddRabbitMQ(); ``` Connect your services to Nitro to introspect your messaging configuration visually. Here is a real-world example: OrderApi publishes `OrderPlaced`, and the broker fans it out to both BillingService and InventoryService through separate exchanges and queues. The trace shows every hop: Expand the visualization to see the full trace sidebar. Each numbered step - publish, dispatch, receive, consume - is a real OpenTelemetry span. The unnumbered nodes between dispatch and receive are the RabbitMQ exchanges and queues the message passed through. When something goes wrong, this is how you answer "where did the message go?" without digging through logs. ### Resiliency Mocha provides an inbox and outbox to guarantee reliable message processing. The **outbox** ensures that database writes and message dispatches succeed or fail together - no lost messages during failures. The **inbox** ensures that messages are processed exactly once, even when the transport delivers duplicates. Both are optimized for your specific database system. C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEntityFramework(p => { p.UsePostgresOutbox(); p.UseTransaction(); p.UsePostgresInbox(); }) .AddRabbitMQ(); ``` ### Saga orchestration Sagas coordinate multi-step workflows that span multiple services and messages. You define a state machine with states and transitions, and Mocha validates the entire state machine - every state must be reachable and every path must lead to a final state. Reducing the possibility of deploying a saga that gets stuck in an intermediate state. Mocha persists saga state, manages transitions, and supports compensation when steps fail. See [Sagas](https://chillicream.com/docs/mocha/sagas) for a full walkthrough. ### In-process mediator For commands and queries that stay within a single service, the mediator provides CQRS dispatch with middleware - without a transport layer. Define your messages with marker interfaces, implement handlers, and the source generator wires everything at compile time: C# ``` // Define a command and its handler public record PlaceOrderCommand(Guid ProductId, int Quantity) : ICommand; public class PlaceOrderCommandHandler(AppDbContext db) : ICommandHandler { public async ValueTask HandleAsync( PlaceOrderCommand command, CancellationToken cancellationToken) { // business logic return new PlaceOrderResult(true, Guid.NewGuid()); } } ``` C# ``` // Register and use builder.Services .AddMediator() .AddCatalog() .UseEntityFrameworkTransactions(); app.MapPost("/orders", async (ISender sender) => await sender.SendAsync(new PlaceOrderCommand(productId, 2))); ``` The mediator supports commands (with and without responses), queries, notifications, middleware, and EF Core transaction wrapping (commands only by default, configurable via delegate). The source generator produces a typed registration method per assembly (e.g. `AddCatalog()`) that wires up all handlers and pre-compiled dispatch pipelines automatically. See [Mediator](https://chillicream.com/docs/mocha/mediator) for the full guide. ## Learning paths Choose an entry point based on how you learn best: - **Get something running first:** [Quick Start](https://chillicream.com/docs/mocha/quick-start) \-zero to a working message bus in under five minutes with the InMemory transport. - **Understand the concepts first:** [Messages](https://chillicream.com/docs/mocha/messages) then [Messaging Patterns](https://chillicream.com/docs/mocha/messaging-patterns) \- learn what flows through the system and what patterns govern how it flows. - **Evaluating Mocha for a specific broker:** [Transports](https://chillicream.com/docs/mocha/transports) \- understand the transport abstraction and what is available. - **In-process CQRS:** [Mediator](https://chillicream.com/docs/mocha/mediator) \- dispatch commands, queries, and notifications within a single service using the source-generated mediator. - **See a real-world system:** The [Demo application](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo) is a complete e-commerce system with three services (Catalog, Billing, Shipping) that demonstrates event-driven communication, sagas, batch processing, the transactional outbox, and .NET Aspire orchestration. Ready to build? Start with the [Quick Start](https://chillicream.com/docs/mocha/quick-start). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/index.md) Maintained by ChilliCream. Last updated on **July 01, 2026** by **Tobias Tengler** --- # Diagnostics - Mocha > Reference for all compile-time diagnostics emitted by the Mocha source generator, including causes, examples, and fixes for each warning and error. Canonical source: https://chillicream.com/docs/mocha/diagnostics Mocha uses a Roslyn source generator to validate your message handlers, consumers, and sagas at compile time. When the generator detects a problem - a missing handler, a duplicate registration, an invalid type - it emits a diagnostic that appears as a compiler warning or error in your IDE and build output. You can fix these issues before your code ever runs. ## Quick reference | Code | Description | Severity | | ----------------- | -------------------------------------------------------- | -------- | | [MO0001](#mo0001) | Missing handler for message type | Warning | | [MO0002](#mo0002) | Duplicate handler for message type | Error | | [MO0003](#mo0003) | Handler is abstract | Warning | | [MO0004](#mo0004) | Open generic message type cannot be dispatched | Info | | [MO0005](#mo0005) | Handler implements multiple mediator handler interfaces | Error | | [MO0006](#mo0006) | Open generic handler cannot be auto-registered | Info | | [MO0011](#mo0011) | Duplicate handler for request type | Error | | [MO0012](#mo0012) | Open generic messaging handler cannot be auto-registered | Info | | [MO0013](#mo0013) | Messaging handler is abstract | Warning | | [MO0014](#mo0014) | Saga must have a public parameterless constructor | Error | | [MO0015](#mo0015) | Missing JsonSerializerContext for AOT | Error | | [MO0016](#mo0016) | Missing JsonSerializable attribute | Error | | [MO0018](#mo0018) | Type not in JsonSerializerContext | Warning | | [MO0020](#mo0020) | Command/query sent but no handler found | Warning | ## Mediator diagnostics These diagnostics apply to the in-process [mediator](https://chillicream.com/docs/mocha/mediator) \- commands, queries, and notifications dispatched within a single process. ### MO0001 **Missing handler for message type** | | | | ------------ | -------------------------------------------- | | **Severity** | Warning | | **Message** | Message type '{0}' has no registered handler | #### Cause A command or query type is declared but no corresponding handler implementation exists. The mediator requires exactly one handler for each command and query type. This diagnostic does not apply to notifications, which can have zero handlers. #### Example C# ``` using Mocha.Mediator; // Command with no handler - triggers MO0001 public record PlaceOrder(Guid OrderId, decimal Total) : ICommand; ``` #### Fix Implement a handler for the message type. C# ``` using Mocha.Mediator; public record PlaceOrder(Guid OrderId, decimal Total) : ICommand; public class PlaceOrderHandler : ICommandHandler { public ValueTask HandleAsync( PlaceOrder command, CancellationToken cancellationToken) { // process the order return ValueTask.CompletedTask; } } ``` ### MO0002 **Duplicate handler for message type** | | | | ------------ | --------------------------------------------- | | **Severity** | Error | | **Message** | Message type '{0}' has multiple handlers: {1} | #### Cause A command or query type has more than one handler implementation. Commands and queries require exactly one handler - the mediator cannot decide which one to call. This diagnostic does not apply to notifications, which support multiple handlers by design. #### Example C# ``` using Mocha.Mediator; public record PlaceOrder(Guid OrderId, decimal Total) : ICommand; // Two handlers for the same command - triggers MO0002 public class PlaceOrderHandler : ICommandHandler { public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct) => ValueTask.CompletedTask; } public class DuplicateOrderHandler : ICommandHandler { public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct) => ValueTask.CompletedTask; } ``` #### Fix Remove all but one handler. If you need multiple side effects for the same action, consider publishing a notification from the single handler and reacting to it with separate notification handlers. C# ``` using Mocha.Mediator; public record PlaceOrder(Guid OrderId, decimal Total) : ICommand; public class PlaceOrderHandler : ICommandHandler { public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct) => ValueTask.CompletedTask; } ``` ### MO0003 **Handler is abstract** | | | | ------------ | ---------------------------------------------------- | | **Severity** | Warning | | **Message** | Handler '{0}' is abstract and will not be registered | #### Cause A class implements a [handler](https://chillicream.com/docs/mocha/handlers-and-consumers) interface (`ICommandHandler`, `IQueryHandler`, or `INotificationHandler`) but is declared `abstract`. The source generator skips abstract types because they cannot be instantiated. #### Example C# ``` using Mocha.Mediator; public record GetOrderTotal(Guid OrderId) : IQuery; // Abstract handler - triggers MO0003 public abstract class GetOrderTotalHandler : IQueryHandler { public abstract ValueTask HandleAsync( GetOrderTotal query, CancellationToken cancellationToken); } ``` #### Fix Make the handler concrete. If you want shared base logic, move it to a base class that does not implement the handler interface, and have the concrete handler extend it. C# ``` using Mocha.Mediator; public record GetOrderTotal(Guid OrderId) : IQuery; public class GetOrderTotalHandler : IQueryHandler { public ValueTask HandleAsync( GetOrderTotal query, CancellationToken cancellationToken) { return ValueTask.FromResult(99.99m); } } ``` ### MO0004 **Open generic message type cannot be dispatched** | | | | ------------ | ------------------------------------------------------------------------- | | **Severity** | Info | | **Message** | Message type '{0}' is an open generic and cannot be dispatched at runtime | #### Cause A command or query type has unbound type parameters. The mediator dispatches concrete types at runtime and cannot resolve an open generic like `MyCommand`. #### Example C# ``` using Mocha.Mediator; // Open generic command - triggers MO0004 public record ProcessItem(T Item) : ICommand; ``` #### Fix Use concrete message types instead. C# ``` using Mocha.Mediator; public record ProcessOrder(Guid OrderId) : ICommand; public record ProcessPayment(decimal Amount) : ICommand; ``` ### MO0005 **Handler implements multiple mediator handler interfaces** | | | | ------------ | ------------------------------------------------------------------- | | **Severity** | Error | | **Message** | Handler '{0}' must implement exactly one mediator handler interface | #### Cause A single class implements more than one of `ICommandHandler`, `IQueryHandler`, or `INotificationHandler`. Each handler class must implement exactly one mediator handler interface so the generator can produce unambiguous registrations. #### Example C# ``` using Mocha.Mediator; public record PlaceOrder(Guid OrderId) : ICommand; public record GetOrder(Guid OrderId) : IQuery; // Implements both command and query handler - triggers MO0005 public class OrderHandler : ICommandHandler, IQueryHandler { public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct) => ValueTask.CompletedTask; public ValueTask HandleAsync(GetOrder query, CancellationToken ct) => ValueTask.FromResult(new Order()); } ``` #### Fix Split into separate handler classes, one per interface. C# ``` using Mocha.Mediator; public record PlaceOrder(Guid OrderId) : ICommand; public record GetOrder(Guid OrderId) : IQuery; public class PlaceOrderHandler : ICommandHandler { public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct) => ValueTask.CompletedTask; } public class GetOrderHandler : IQueryHandler { public ValueTask HandleAsync(GetOrder query, CancellationToken ct) => ValueTask.FromResult(new Order()); } ``` ### MO0006 **Open generic handler cannot be auto-registered** | | | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Severity** | Info | | **Message** | Handler '{0}' has unbound type parameters; source-generated registration is skipped. Register the closed form manually (e.g. AddHandler>()) if intentional. | #### Cause A mediator handler (`ICommandHandler`, `ICommandHandler`, `IQueryHandler`, or `INotificationHandler`) is declared with unbound type parameters. This diagnostic fires on the handler _declaration_ \- the source generator cannot produce a registration for an open generic because there is no closed type to register. Under AOT, this matters because every dispatched message must correspond to a statically-known handler type; the runtime cannot close the generic for you. #### Example C# ``` using Mocha.Mediator; public record MyNotification : INotification; // Open generic handler - triggers MO0006 public class GenericNotificationHandler : INotificationHandler { public ValueTask HandleAsync( MyNotification notification, CancellationToken ct) => ValueTask.CompletedTask; } ``` #### Fix Either close the generic by declaring a concrete handler, or register the closed form manually and suppress the diagnostic on the declaration. Close the generic at the handler declaration: C# ``` using Mocha.Mediator; public record MyNotification : INotification; public class MyNotificationHandler : INotificationHandler { public ValueTask HandleAsync( MyNotification notification, CancellationToken ct) => ValueTask.CompletedTask; } ``` Or, if the open generic is intentional (for example, a reusable handler body parameterized on a secondary type), register the closed form manually and suppress MO0006 on the declaration: C# ``` using Mocha.Mediator; public record MyNotification : INotification; #pragma warning disable MO0006 public class GenericNotificationHandler : INotificationHandler { public ValueTask HandleAsync( MyNotification notification, CancellationToken ct) => ValueTask.CompletedTask; } #pragma warning restore MO0006 // Register the closed form explicitly: // services.AddMediator(m => m.AddHandler>()); ``` ## Messaging diagnostics These diagnostics apply to the [message bus](https://chillicream.com/docs/mocha/handlers-and-consumers) \- event handlers, request handlers, batch handlers, consumers, and sagas that communicate across service boundaries. ### MO0011 **Duplicate handler for request type** | | | | ------------ | --------------------------------------------- | | **Severity** | Error | | **Message** | Request type '{0}' has multiple handlers: {1} | #### Cause A request type (used with `SendAsync` or `RequestAsync`) has more than one [handler](https://chillicream.com/docs/mocha/handlers-and-consumers) implementation. Request types require exactly one handler - the bus cannot route to multiple targets. #### Example C# ``` using Mocha; public record ProcessPayment(decimal Amount); // Two handlers for the same request type - triggers MO0011 public class PaymentHandlerA : IEventRequestHandler { public ValueTask HandleAsync( ProcessPayment request, CancellationToken ct) => ValueTask.CompletedTask; } public class PaymentHandlerB : IEventRequestHandler { public ValueTask HandleAsync( ProcessPayment request, CancellationToken ct) => ValueTask.CompletedTask; } ``` #### Fix Keep one handler per request type. C# ``` using Mocha; public record ProcessPayment(decimal Amount); public class ProcessPaymentHandler : IEventRequestHandler { public ValueTask HandleAsync( ProcessPayment request, CancellationToken ct) => ValueTask.CompletedTask; } ``` ### MO0012 **Open generic messaging handler cannot be auto-registered** | | | | ------------ | -------------------------------------------------------------- | | **Severity** | Info | | **Message** | Handler '{0}' is an open generic and cannot be auto-registered | #### Cause A messaging handler (`IEventHandler`, `IEventRequestHandler`, `IBatchEventHandler`, or `IConsumer`) has unbound type parameters. The source generator cannot produce registration code for open generic types. #### Example C# ``` using Mocha; // Open generic handler - triggers MO0012 public class GenericEventHandler : IEventHandler { public ValueTask HandleAsync( T message, CancellationToken ct) => ValueTask.CompletedTask; } ``` #### Fix Make the handler concrete. If you need to handle multiple event types with shared logic, create a concrete handler for each type and extract the shared logic into a base class or shared service. If you need to register an open generic handler, register it manually through DI instead of relying on auto-registration. C# ``` using Mocha; public record OrderPlaced(Guid OrderId); public class OrderPlacedHandler : IEventHandler { public ValueTask HandleAsync( OrderPlaced message, CancellationToken ct) => ValueTask.CompletedTask; } ``` ### MO0013 **Messaging handler is abstract** | | | | ------------ | ---------------------------------------------------- | | **Severity** | Warning | | **Message** | Handler '{0}' is abstract and will not be registered | #### Cause A class implements a messaging [handler](https://chillicream.com/docs/mocha/handlers-and-consumers) interface but is declared `abstract`. The source generator skips abstract types because they cannot be instantiated. #### Example C# ``` using Mocha; public record OrderPlaced(Guid OrderId); // Abstract handler - triggers MO0013 public abstract class OrderEventHandler : IEventHandler { public abstract ValueTask HandleAsync( OrderPlaced message, CancellationToken ct); } ``` #### Fix Make the handler concrete. If you need shared base logic, move it to a base class that does not implement the handler interface. C# ``` using Mocha; public record OrderPlaced(Guid OrderId); public class OrderPlacedHandler : IEventHandler { public ValueTask HandleAsync( OrderPlaced message, CancellationToken ct) => ValueTask.CompletedTask; } ``` ### MO0014 **Saga must have a public parameterless constructor** | | | | ------------ | ------------------------------------------------------- | | **Severity** | Error | | **Message** | Saga '{0}' must have a public parameterless constructor | #### Cause A [Saga](https://chillicream.com/docs/mocha/sagas) subclass does not have a public parameterless constructor. The saga infrastructure requires this constructor to instantiate the saga type. This is enforced by the `new()` constraint on the `AddSaga` registration method. #### Example C# ``` using Mocha.Sagas; public class RefundSagaState : SagaStateBase { public Guid OrderId { get; set; } } // Constructor requires a parameter - triggers MO0014 public class RefundSaga : Saga { private readonly ILogger _logger; public RefundSaga(ILogger logger) { _logger = logger; } } ``` #### Fix Add a public parameterless constructor. Sagas are configured through their state machine definition, not through constructor injection. If you need dependencies, access them through the saga's built-in service resolution. C# ``` using Mocha.Sagas; public class RefundSagaState : SagaStateBase { public Guid OrderId { get; set; } } public class RefundSaga : Saga { public RefundSaga() { } } ``` ### MO0015 **Missing JsonSerializerContext for AOT** | | | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | **Severity** | Error | | **Message** | MessagingModule '{0}' must specify JsonContext when publishing for AOT. Add JsonContext = typeof(YourJsonContext) to the MessagingModule attribute. | #### Cause The project has `PublishAot` set to `true` but the `[assembly: MessagingModule]` attribute does not include a `JsonContext` property. AOT publishing requires a `JsonSerializerContext` so the source generator can produce trim-safe serialization code for all message types. #### Example C# ``` using Mocha; // No JsonContext specified while targeting AOT - triggers MO0015 [assembly: MessagingModule("OrderService")] ``` #### Fix Create a `JsonSerializerContext` that includes all your message types and reference it from the `MessagingModule` attribute. C# ``` using System.Text.Json.Serialization; using Mocha; [assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))] public record OrderPlaced(Guid OrderId); [JsonSerializable(typeof(OrderPlaced))] public partial class OrderServiceJsonContext : JsonSerializerContext; ``` ### MO0016 **Missing JsonSerializable attribute** | | | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | **Severity** | Error | | **Message** | Type '{0}' is used as a message type but is not included in JsonSerializerContext '{1}'. Add \[JsonSerializable(typeof({0}))\] to the context. | #### Cause A type is used as a message, request, response, or saga state through a handler registration, but it is not declared via `[JsonSerializable(typeof(...))]` on the `JsonSerializerContext` specified in the `MessagingModule` attribute. Without this declaration, the AOT compiler cannot generate serialization code for the type. #### Example C# ``` using System.Text.Json.Serialization; using Mocha; [assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))] public record OrderPlaced(Guid OrderId); public class OrderPlacedHandler : IEventHandler { public ValueTask HandleAsync( OrderPlaced message, CancellationToken ct) => ValueTask.CompletedTask; } // OrderPlaced is missing from the context - triggers MO0016 [JsonSerializable(typeof(string))] public partial class OrderServiceJsonContext : JsonSerializerContext; ``` #### Fix Add a `[JsonSerializable]` attribute for every message type used by your handlers. C# ``` using System.Text.Json.Serialization; using Mocha; [assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))] public record OrderPlaced(Guid OrderId); public class OrderPlacedHandler : IEventHandler { public ValueTask HandleAsync( OrderPlaced message, CancellationToken ct) => ValueTask.CompletedTask; } [JsonSerializable(typeof(OrderPlaced))] public partial class OrderServiceJsonContext : JsonSerializerContext; ``` ### MO0018 **Type not in JsonSerializerContext** | | | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | **Severity** | Warning | | **Message** | Type '{0}' is used in a {1} call but is not included in JsonSerializerContext '{2}'. Add \[JsonSerializable(typeof({0}))\] to the context. | #### Cause Emitted only when `PublishAot` is enabled and a call site publishes a type not covered by the declared `JsonContext`. For example, `bus.PublishAsync()` is called with a type that has no matching `[JsonSerializable(typeof(T))]` on the `JsonSerializerContext` referenced by `[assembly: MessagingModule(..., JsonContext = typeof(...))]`. This is similar to [MO0016](#mo0016), but applies to types discovered at call sites rather than handler registrations. Without the declaration, the message cannot be serialized at runtime in an AOT environment. This diagnostic does not fire when `PublishAot` is unset, even if `JsonContext` is declared on the module - opting into a source-generated context for non-AOT reasons does not require every call-site type to appear in it. #### Example C# ``` using System.Text.Json.Serialization; using Mocha; [assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))] public record OrderPlaced(Guid OrderId); public record OrderShipped(Guid OrderId); // OrderShipped is published but missing from the context - triggers MO0018 public class OrderService(IMessageBus bus) { public async Task ShipOrderAsync(Guid orderId, CancellationToken ct) { await bus.PublishAsync(new OrderShipped(orderId), ct); } } [JsonSerializable(typeof(OrderPlaced))] public partial class OrderServiceJsonContext : JsonSerializerContext; ``` #### Fix Add a `[JsonSerializable]` attribute for every type used at a call site. C# ``` using System.Text.Json.Serialization; using Mocha; [assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))] public record OrderPlaced(Guid OrderId); public record OrderShipped(Guid OrderId); [JsonSerializable(typeof(OrderPlaced))] [JsonSerializable(typeof(OrderShipped))] public partial class OrderServiceJsonContext : JsonSerializerContext; ``` ## Mediator call-site diagnostics These diagnostics are reported when the source generator inspects call sites that use `ISender` to dispatch commands or queries. ### MO0020 **Command/query sent but no handler found** | | | | ------------ | ----------------------------------------------------------------------------------------------------- | | **Severity** | Warning | | **Message** | Type '{0}' is sent via {1} but no handler was found in this assembly. Ensure a handler is registered. | #### Cause A command or query type is dispatched via `ISender.SendAsync` or `ISender.QueryAsync`, but no corresponding handler implementation exists in the assembly. This catches cases where a call site references a message type that was never wired up with a handler. #### Example C# ``` using Mocha.Mediator; public record PlaceOrder(Guid OrderId, decimal Total) : ICommand; // PlaceOrder is sent but no handler exists - triggers MO0020 public class OrderController(ISender sender) { public async Task CreateOrderAsync(Guid orderId, CancellationToken ct) { await sender.SendAsync(new PlaceOrder(orderId, 99.99m), ct); } } ``` #### Fix Implement a handler for the message type, or remove the call site if the handler is intentionally in another assembly. C# ``` using Mocha.Mediator; public record PlaceOrder(Guid OrderId, decimal Total) : ICommand; public class PlaceOrderHandler : ICommandHandler { public ValueTask HandleAsync( PlaceOrder command, CancellationToken cancellationToken) { // process the order return ValueTask.CompletedTask; } } ``` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/diagnostics.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Exception Policies - Mocha > Configure per-exception handling with composable retry, redelivery, and terminal actions. Canonical source: https://chillicream.com/docs/mocha/exception-policies Not every exception deserves the same treatment. A database deadlock might resolve on immediate retry. A downstream service outage needs minutes to recover. A validation error will never succeed no matter how many times you retry. Exception policies let you define per-exception handling strategies - retry, redeliver, dead-letter, or discard - as composable escalation chains in a single `AddResilience` call. C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { // Validation errors are permanent - route straight to the error endpoint policy.On().DeadLetter(); // Duplicate messages are safe to drop policy.On().Discard(); // Database deadlocks resolve quickly - retry then redeliver policy.On(ex => ex.IsTransient) .Retry(5, TimeSpan.FromMilliseconds(200)) .ThenRedeliver(); // Everything else: retry 3 times, then redeliver on a schedule, then dead-letter policy.Default() .Retry() .ThenRedeliver() .ThenDeadLetter(); }) .AddEventHandler() .AddRabbitMQ(); ``` ## How exception handling works When a handler throws an exception, Mocha evaluates exception policies to determine what happens next. The decision flows through two pipeline stages - retry in the consumer pipeline and redelivery in the receive pipeline - before reaching the fault middleware as a last resort. Each exception policy rule targets a specific exception type and defines an escalation chain. The chain controls which stages the message passes through and with what settings. Exception matching respects inheritance. A policy on `NpgsqlException` also matches any subclass. When multiple rules could match, the most specific type wins - the same precedence as C# `catch` blocks. ## Configure exception policies `AddResilience` is the single entry point for all exception handling configuration. There is no separate `AddRetry` or `AddRedelivery` call - retry and redelivery settings are configured per-exception within the policy. Note Calling `On()` for the same exception type replaces the previous rule for that type - last write wins. If you call `On()` twice without a predicate, the second call overwrites the first. The same applies to `Default()`: calling it again replaces the previous default rule. For example, the parameterless `AddResilience()` registers `Default().Retry().ThenRedeliver()`. If you later call `AddResilience(p => p.Default().Retry(5))`, the new default replaces the one registered by the parameterless overload. ### Parameterless defaults The parameterless overload registers a catch-all `Default()` rule with both retry and redelivery enabled using built-in defaults: C# ``` builder.Services .AddMessageBus() .AddResilience() .AddEventHandler() .AddRabbitMQ(); ``` This is equivalent to: C# ``` .AddResilience(policy => { policy.Default().Retry().ThenRedeliver(); }) ``` ### Default() and On() `ExceptionPolicyOptions` exposes two methods for creating rules: - **`Default()`** \- shorthand for `On()`. Configures the catch-all behavior for any exception that does not match a more specific rule. - **`On()`** \- configures behavior for a specific exception type. - **`On(predicate)`** \- configures behavior for a specific exception type when a predicate matches. C# ``` .AddResilience(policy => { policy.On().Retry(5).ThenRedeliver(); policy.On().Retry(3); policy.Default().Retry().ThenRedeliver(); }) ``` ### Bus-level policies Bus-level policies apply to all endpoints and all consumers across the entire message bus. C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { policy.On().DeadLetter(); policy.On().Discard(); policy.Default().Retry().ThenRedeliver(); }) .AddEventHandler() .AddRabbitMQ(); ``` ### Transport-level policies Override bus-level policies for a specific transport. Transport-level policies replace the bus-level policies entirely for all endpoints on that transport - they are not merged. C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { policy.Default().Retry().ThenRedeliver(); }) .AddRabbitMQ(transport => { transport.AddResilience(policy => { policy.On().DeadLetter(); }); }); ``` ### Consumer-level policies Override policies for a specific consumer. Consumer-level policies replace the bus-level and transport-level policies for that consumer. C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { policy.On() .Retry(2, TimeSpan.FromMilliseconds(100), RetryBackoffType.Constant); }); builder.Services.ConfigureMessageBus(bus => { bus.AddHandler(consumer => { consumer.AddResilience(policy => { policy.On() .Retry(5, TimeSpan.FromMilliseconds(500), RetryBackoffType.Exponential); }); }); }); ``` The `PaymentHandler` gets 5 retries with exponential backoff. All other consumers get the bus-level policy of 2 retries. ### Scope hierarchy Exception policies resolve at four levels. The most specific scope wins, and replacement is atomic - the entire set of rules is replaced, not individual rules. | Scope | Applies to | Configured on | | --------- | ------------------------------------- | -------------------------- | | Bus | All endpoints and consumers | IMessageBusBuilder | | Host | All message buses on the host | IMessageBusHostBuilder | | Transport | All endpoints on a specific transport | IReceiveMiddlewareProvider | | Consumer | A single consumer | IConsumerDescriptor | ``` Consumer policies → Transport policies → Bus policies → Host policies (highest priority) (lowest priority) ``` If a consumer defines exception policies, the bus-level and transport-level policies are ignored for that consumer. If you need a bus-level rule to also apply at the consumer level, include it in the consumer-level configuration. ## Terminal actions Terminal actions end the message's lifecycle immediately. No retry, no redelivery - the message is either routed to the error endpoint or discarded. ### DeadLetter `DeadLetter()` routes the message to the error endpoint, skipping both retry and redelivery. Use this for exceptions that are permanent - retrying will never succeed and you want the message preserved for inspection. C# ``` policy.On().DeadLetter(); policy.On().DeadLetter(); policy.On().DeadLetter(); ``` **When to use:** Malformed messages, authorization failures, schema violations, business rule violations that require manual intervention. ### Discard `Discard()` swallows the exception and the message disappears. No error endpoint, no fault headers, no trace beyond logging. C# ``` policy.On().Discard(); policy.On().Discard(); ``` **When to use:** Messages that are safe to lose - duplicates you have already processed, stale events that no longer matter. Use with caution: discarded messages leave no audit trail in the error endpoint. ## Retry policies Retry re-runs the handler in-process using immediate retries. The message stays in memory, the concurrency slot is held, and the handler is invoked again after a short delay. Use retry for transient failures that resolve in milliseconds to seconds. When you call `.Retry()` without chaining `.ThenRedeliver()`, redelivery is disabled for that exception type. If all retries are exhausted, the message routes to the error endpoint. ### Retry with defaults C# ``` policy.On().Retry(); ``` | Setting | Default value | | --------- | ------------- | | Attempts | 3 | | Delay | 200 ms | | Backoff | Exponential | | Jitter | Enabled | | Max delay | 30 seconds | ### Retry with custom attempts C# ``` policy.On(ex => ex.IsTransient).Retry(5); ``` Overrides the number of retry attempts. Delay, backoff strategy, jitter, and max delay use the built-in defaults. ### Retry with full configuration C# ``` policy.On() .Retry(3, TimeSpan.FromMilliseconds(500), RetryBackoffType.Exponential); ``` Overrides attempts, base delay, and backoff strategy for this exception type. ### Retry with explicit intervals C# ``` policy.On().Retry( [ TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(500), TimeSpan.FromSeconds(2) ]); ``` Specifies the exact delay before each retry attempt. The array length determines the number of retries. ### Backoff strategies | Strategy | Behavior | | ----------- | ---------------------------------- | | Constant | Same delay every attempt | | Linear | delay \* attempt | | Exponential | delay \* 2^(attempt-1) _(default)_ | All strategies apply jitter by default to prevent thundering herd effects. ## Redelivery policies Redelivery schedules the message for later delivery through the transport. The concurrency slot is released, the message re-enters the full receive pipeline on each redelivery attempt, and fresh retry cycles run on each delivery. Use redelivery for failures that need minutes or hours to resolve - a downstream service recovering from an outage, a rate limit resetting, or a database completing a failover. When you call `.Redeliver()` directly (without `.Retry()` first), retry is disabled for that exception type. The handler failure goes straight to redelivery scheduling. ### Redeliver with defaults C# ``` policy.On().Redeliver(); ``` | Setting | Default value | | --------- | --------------------- | | Intervals | 5 min, 15 min, 30 min | | Jitter | Enabled | | Max delay | 1 hour | ### Redeliver with custom attempts and delay C# ``` policy.On().Redeliver(5, TimeSpan.FromMinutes(2)); ``` Overrides the number of attempts and base delay. ### Redeliver with explicit intervals C# ``` policy.On().Redeliver( [ TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(30) ]); ``` Specifies the exact delay before each redelivery attempt. The array length determines the number of redelivery attempts. ## Escalation chains The fluent API composes retry, redelivery, and terminal actions into escalation chains. The interface design enforces valid chains at compile time - you cannot chain `.ThenRedeliver()` after `.Redeliver()`, or `.Retry()` after `.ThenRedeliver()`. ### Retry then redeliver C# ``` policy.On(ex => ex.IsTransient) .Retry(3) .ThenRedeliver(); ``` Try 3 immediate retries. If all fail, schedule redelivery with defaults (5, 15, 30 minutes). Each redelivery attempt runs a fresh cycle of 3 retries. ### Retry then redeliver with custom settings C# ``` policy.On() .Retry(5, TimeSpan.FromMilliseconds(500)) .ThenRedeliver( [ TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30) ]) .ThenDeadLetter(); ``` Try 5 immediate retries with 500 ms exponential backoff. If exhausted, schedule redeliveries at 5, 15, and 30 minutes. If all redeliveries are exhausted, route to the error endpoint. ### Retry then dead-letter C# ``` policy.On() .Retry(3) .ThenDeadLetter(); ``` Try 3 immediate retries. If all fail, skip redelivery and route to the error endpoint immediately. ### Chain behavior reference | Chain | Retry | Redelivery | Terminal | | ------------------------------------------ | ---------- | ---------- | ---------- | | .Discard() | \- | \- | Discard | | .DeadLetter() | \- | \- | DeadLetter | | .Retry() | Enabled | Disabled | Default | | .Retry(3) | 3 attempts | Disabled | Default | | .Redeliver() | Disabled | Enabled | Default | | .Retry(3).ThenRedeliver() | 3 attempts | Enabled | Default | | .Retry(3).ThenDeadLetter() | 3 attempts | Disabled | DeadLetter | | .Retry(3).ThenRedeliver().ThenDeadLetter() | 3 attempts | Enabled | DeadLetter | **Default terminal behavior:** When no terminal action is specified, exhausted messages route to the error endpoint through the fault middleware. `.ThenDeadLetter()` makes this intent explicit but does not change the behavior. **Disabled vs. default:** "Disabled" means that tier is skipped for this exception type. A dash (-) means the tier is not configured and does not apply. ## Conditional policies Use predicate overloads to apply policies only when an exception matches specific conditions. The predicate receives the typed exception instance. ### Filter by exception property C# ``` // Only retry transient database errors policy.On(ex => ex.IsTransient) .Retry(5); // Dead-letter non-transient database errors policy.On(ex => !ex.IsTransient) .DeadLetter(); ``` ### Filter by HTTP status code C# ``` policy.On(ex => ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests) .Redeliver( [ TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(2), TimeSpan.FromMinutes(10) ]); policy.On(ex => ex.StatusCode == System.Net.HttpStatusCode.BadRequest) .DeadLetter(); ``` ### Filter by inner exception C# ``` policy.On(ex => ex.InnerException is TimeoutException) .Retry(3); ``` ### Multiple rules for the same type You can define multiple rules for the same exception type with different predicates. When an exception is thrown, the first matching rule wins. C# ``` policy.On(ex => ex.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable) .Retry(3).ThenRedeliver(); policy.On(ex => ex.StatusCode == System.Net.HttpStatusCode.BadRequest) .DeadLetter(); // Catch-all for other HTTP errors policy.On().Retry(3); ``` [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/exception-policies.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Handler Registration - Mocha > How Mocha discovers message bus handlers at compile time with a Roslyn source generator, and how to customize or mix in manual registration. Canonical source: https://chillicream.com/docs/mocha/handler-registration C# ``` builder.Services .AddMessageBus() .AddOrderService(); // source-generated from your assembly name ``` That registers the message bus, discovers your handlers and sagas at compile time, and wires up the registration. `.AddOrderService()` is a source-generated extension method - it knows your handler types at compile time and produces direct registration calls with no reflection. ## How the source generator works At build time, the `Mocha.Analyzers` package runs a [Roslyn incremental source generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) that scans your assembly for classes implementing message bus handler interfaces. For each handler it finds, it emits a registration call in a generated extension method on `IMessageBusHostBuilder`. The generator discovers these types: | Interface | Pattern | | -------------------------------- | ---------------------- | | IEventHandler | Pub/sub events | | IEventRequestHandler | Request/reply | | IEventRequestHandler | Send (fire-and-forget) | | IBatchEventHandler | Batch processing | | IConsumer | Low-level consumer | | Saga | Saga orchestration | If you have used the [Mediator source generator](https://chillicream.com/docs/mocha/mediator), this works the same way. The mediator generates `Add{ModuleName}()` on `IMediatorHostBuilder`; the message bus generates `Add{ModuleName}()` on `IMessageBusHostBuilder`. > **Recommendation:** Always use the source generator for handler registration. The generated code uses optimized, reflection-free registration paths. The source-generated output is designed for long-term stability across versions. Manual registration methods are available for edge cases but their internal behavior may change between releases. ## Module naming The source generator names the extension method based on your assembly: 1. If you apply `[assembly: MessagingModule("OrderService")]`, the method is `AddOrderService()` 2. Otherwise, it uses the last segment of the assembly name: `MyCompany.OrderService.Api` produces `AddApi()` To set an explicit module name, add the attribute to any file in your project: C# ``` using Mocha; [assembly: MessagingModule("OrderService")] ``` This generates: C# ``` builder.Services .AddMessageBus() .AddOrderService() // from [assembly: MessagingModule("OrderService")] .AddRabbitMQ(); ``` > **Convention:** Use a short, meaningful name that identifies the service or bounded context - `OrderService`, `Billing`, `Inventory`. This name appears in the generated code and in your `Program.cs`, so keep it readable. ## What the generator produces For a project named `OrderService` with an event handler, a request handler, and a saga, the source generator produces an extension method `AddOrderService()` on `IMessageBusHostBuilder`. This method registers all discovered handlers and sagas with optimized, reflection-free factory delegates. Handlers are grouped by kind and ordered alphabetically within each group. The registration order is: batch handlers, consumers, request handlers, event handlers, sagas. Note The generated code is an implementation detail and may change between versions. Do not depend on the shape of the generated output. ## Manual handler registration When you need to register handlers outside the source generator's reach - from a plugin assembly, a dynamically loaded module, or in integration tests - use the explicit registration methods: C# ``` builder.Services .AddMessageBus() .AddOrderService() // source-generated handlers .AddEventHandler() // from another assembly .AddRequestHandler() // from a plugin .AddRabbitMQ(); ``` You can mix source-generated and manual registration freely. If both the source generator and manual code register the same handler type, the configurations are composed - the source generator sets up the base registration and your manual call layers additional configuration (such as consumer middleware) on top. > **Prefer the source generator.** Manual registration methods use runtime reflection to create handler consumers. The source generator produces direct, reflection-free factory calls. We guarantee backwards compatibility for the source-generated registration path; the manual registration API is stable at the surface level but its internal behavior may evolve. ## Troubleshooting ### The source-generated method does not appear If IntelliSense does not show `Add{ModuleName}()`: - Confirm the `Mocha.Analyzers` package is referenced with `OutputItemType="Analyzer"` in your `.csproj` - Rebuild the project - source generators run during compilation - Check the build output for [analyzer diagnostics](https://chillicream.com/docs/mocha/diagnostics) prefixed with `MO` - Verify you have at least one concrete handler class in the assembly ### Handler is not being called If the source-generated method is available but a specific handler does not run: - Check for [**MO0013**](https://chillicream.com/docs/mocha/diagnostics#mo0013) (abstract handler) - only concrete classes are registered - Check for [**MO0012**](https://chillicream.com/docs/mocha/diagnostics#mo0012) (open generic) - close the generic type - Verify the handler implements the correct interface for the messaging pattern you are using - Ensure the handler is in the same project that references `Mocha.Analyzers` ### Priority when a handler implements multiple interfaces If a class implements more than one messaging interface (e.g., both `IBatchEventHandler` and `IEventHandler`), the source generator registers it using the highest-priority interface only: `IBatchEventHandler` \> `IConsumer` \> `IEventRequestHandler` \> `IEventRequestHandler` \> `IEventHandler` ## Next steps - [Handlers and Consumers](https://chillicream.com/docs/mocha/handlers-and-consumers) \- handler interfaces, DI scoping, and exception behavior - [Routing and Endpoints](https://chillicream.com/docs/mocha/routing-and-endpoints) \- how the bus routes messages to registered handlers - [Sagas](https://chillicream.com/docs/mocha/sagas) \- saga state machines and long-running workflows - [Mediator](https://chillicream.com/docs/mocha/mediator) \- the mediator uses the same source generation approach for in-process CQRS [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/handler-registration.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Handlers and Consumers - Mocha > Learn how to implement Mocha message handlers, from event, request, send, and batch handlers down to the low-level consumer interface. Canonical source: https://chillicream.com/docs/mocha/handlers-and-consumers You implement a handler interface, and the source generator discovers it at compile time. Mocha routes matching messages to your handler automatically. This page covers every handler type, when to use each one, and the patterns that apply to all of them: DI scoping, exception behavior, and publishing from within a handler. ### When to use which handler Choose a handler interface based on the messaging pattern you are implementing: | Interface | Use when | | -------------------------------- | ----------------------------------------------------------------------------------------------- | | IEventHandler | Reacting to a published event. No reply expected. Multiple handlers can receive the same event. | | IEventRequestHandler | Handling a request and returning a typed response to the caller. | | IEventRequestHandler | Handling a send (fire-and-forget) with no typed response. | | IBatchEventHandler | Processing multiple events at once for throughput efficiency. | | IConsumer | Accessing raw envelope metadata - headers, correlation IDs, or the full consume context. | If you have read [Messaging Patterns](https://chillicream.com/docs/mocha/messaging-patterns), these map directly: `IEventHandler` is for `PublishAsync`, `IEventRequestHandler` is for `SendAsync`, and `IEventRequestHandler` is for `RequestAsync`. ## Event handler An event handler reacts to published events. The publisher does not know or care who handles the event. Multiple handlers can receive the same event type independently. By the end of this section, you will have a working event handler that logs published orders. ### Define the message C#OrderPlaced.cs ``` namespace MyApp.Messages; public sealed record OrderPlaced { public required Guid OrderId { get; init; } public required string CustomerId { get; init; } public required decimal TotalAmount { get; init; } } ``` Messages are plain C# records. No base class or marker interface required for pub/sub events. ### Implement the handler C#OrderPlacedHandler.cs ``` using Mocha; using MyApp.Messages; namespace MyApp.Handlers; public class OrderPlacedHandler(ILogger logger) : IEventHandler { public async ValueTask HandleAsync( OrderPlaced message, CancellationToken cancellationToken) { logger.LogInformation( "Order placed: {OrderId} for customer {CustomerId}, total {TotalAmount}", message.OrderId, message.CustomerId, message.TotalAmount); await Task.CompletedTask; } } ``` `IEventHandler` has a single method: `HandleAsync(T message, CancellationToken cancellationToken)`. The bus deserializes the message and calls your handler. Constructor dependencies are resolved from a scoped DI container - more on this in [DI scoping](#di-scoping). ### Register and run C#Program.cs ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddMessageBus() .AddMyApp() // source-generated - registers OrderPlacedHandler automatically .AddInMemory(); // or .AddRabbitMQ() var app = builder.Build(); app.Run(); ``` `.AddMyApp()` is a source-generated extension method that discovers all handlers in the assembly and registers them. The source generator found `OrderPlacedHandler`, saw that it implements `IEventHandler`, and emitted a registration call for it. For details on how the source generator works and how to customize the module name, see [Handler Registration](https://chillicream.com/docs/mocha/handler-registration). ### Verify the handler runs Publish an event from an API endpoint. `IMessageBus` is a scoped service - resolve it through endpoint injection: C# ``` app.MapPost("/orders", async (IMessageBus bus) => { await bus.PublishAsync(new OrderPlaced { OrderId = Guid.NewGuid(), CustomerId = "customer-42", TotalAmount = 149.99m }, CancellationToken.None); return Results.Ok("Published"); }); ``` If everything worked, you see this in the console: ``` info: MyApp.Handlers.OrderPlacedHandler[0] Order placed: 3f2504e0-4f89-11d3-9a0c-0305e82c3301 for customer customer-42, total 149.99 ``` ## Request handler A request handler processes a request and returns a typed response. The caller awaits the result. Use this when the sender needs data back. ### Define the request and response The request record must implement `IEventRequest`. This marker interface tells Mocha the expected response type and enables type-safe `RequestAsync` calls. C#GetProductRequest.cs ``` using Mocha; namespace MyApp.Messages; public sealed record GetProductRequest : IEventRequest { public required Guid ProductId { get; init; } } public sealed record GetProductResponse { public required Guid ProductId { get; init; } public required string Name { get; init; } public required decimal Price { get; init; } public required bool IsAvailable { get; init; } } ``` ### Implement the handler C#GetProductRequestHandler.cs ``` using Mocha; using MyApp.Messages; namespace MyApp.Handlers; public class GetProductRequestHandler(AppDbContext db) : IEventRequestHandler { public async ValueTask HandleAsync( GetProductRequest request, CancellationToken cancellationToken) { var product = await db.Products .FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken); return new GetProductResponse { ProductId = request.ProductId, Name = product?.Name ?? string.Empty, Price = product?.Price ?? 0, IsAvailable = product?.StockQuantity > 0 }; } } ``` The return value is sent back to the caller automatically. The return value must not be `null` \- if you return `null`, the bus throws an `InvalidOperationException`. ### Register and call C# ``` builder.Services .AddMessageBus() .AddMyApp() // source-generated - registers GetProductRequestHandler automatically .AddRabbitMQ(); ``` From the caller side: C# ``` var response = await bus.RequestAsync( new GetProductRequest { ProductId = productId }, cancellationToken); // response.Name, response.Price, response.IsAvailable ``` If the handler throws, the exception propagates back to the caller. See [Exception behavior](#exception-behavior) for details. ## Send handler A send handler processes a one-way instruction. There is no typed response. The sender dispatches the message and moves on. ### Define the message Send messages do not implement `IEventRequest` because there is no typed response. C#ReserveInventoryCommand.cs ``` namespace MyApp.Messages; public sealed record ReserveInventoryCommand { public required Guid OrderId { get; init; } public required Guid ProductId { get; init; } public required int Quantity { get; init; } } ``` ### Implement the handler Use `IEventRequestHandler` \- the single type parameter variant, with no response type: C#ReserveInventoryCommandHandler.cs ``` using Mocha; using MyApp.Messages; namespace MyApp.Handlers; public class ReserveInventoryCommandHandler( AppDbContext db, ILogger logger) : IEventRequestHandler { public async ValueTask HandleAsync( ReserveInventoryCommand request, CancellationToken cancellationToken) { logger.LogInformation( "Reserving {Quantity} units of product {ProductId}", request.Quantity, request.ProductId); var product = await db.Products .FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken); if (product is null) { throw new InvalidOperationException( $"Product {request.ProductId} not found"); } product.StockQuantity -= request.Quantity; await db.SaveChangesAsync(cancellationToken); } } ``` ### Register and send C# ``` builder.Services .AddMessageBus() .AddMyApp() // source-generated - registers ReserveInventoryCommandHandler automatically .AddRabbitMQ(); ``` Send the message: C# ``` await bus.SendAsync(new ReserveInventoryCommand { OrderId = orderId, ProductId = productId, Quantity = 3 }, cancellationToken); ``` Expected output: ``` info: MyApp.Handlers.ReserveInventoryCommandHandler[0] Reserving 3 units of product a1b2c3d4-... ``` `SendAsync` completes after the message is dispatched to the transport. It does not wait for the handler to finish. To wait for completion, use `RequestAsync` instead - Mocha sends an automatic acknowledgment when the handler finishes. ## Batch handler A batch handler receives groups of messages at once instead of one at a time. Use batch handlers for high-throughput scenarios where processing messages in bulk is more efficient - bulk database writes, aggregations, or analytics pipelines. ### Implement the handler C#OrderPlacedBatchHandler.cs ``` using Mocha; using MyApp.Messages; namespace MyApp.Handlers; public class OrderPlacedBatchHandler( AppDbContext db, ILogger logger) : IBatchEventHandler { public async ValueTask HandleAsync( IMessageBatch batch, CancellationToken cancellationToken) { logger.LogInformation( "Processing batch of {Count} orders", batch.Count); var totalRevenue = 0m; foreach (var order in batch) { totalRevenue += order.TotalAmount; } db.RevenueSummaries.Add(new RevenueSummary { OrderCount = batch.Count, TotalRevenue = totalRevenue, CreatedAt = DateTimeOffset.UtcNow }); await db.SaveChangesAsync(cancellationToken); logger.LogInformation( "Revenue summary created: {Count} orders, {Total:C} total", batch.Count, totalRevenue); } } ``` `IMessageBatch` implements `IReadOnlyList`, so you can iterate, index, and check `.Count`. The `CompletionMode` property tells you why the batch was dispatched: `Size` (reached the configured maximum), `Time` (timeout expired), or `Forced` (endpoint shutting down). To access envelope metadata for a specific message in the batch, call `batch.GetContext(index)` to get an `IConsumeContext` with headers, correlation IDs, and timestamps. ### Register and configure C# ``` builder.Services .AddMessageBus() .AddMyApp() // source-generated - registers OrderPlacedBatchHandler automatically .AddBatchHandler(opts => { opts.MaxBatchSize = 50; opts.BatchTimeout = TimeSpan.FromSeconds(10); }) .AddRabbitMQ(); ``` The source generator registers the batch handler, but you can chain `.AddBatchHandler(opts => ...)` after `AddMyApp()` to override batch configuration options. Without explicit configuration, Mocha uses the defaults: 100 messages per batch, 1-second timeout. ### Publish events as normal C# ``` for (var i = 0; i < 100; i++) { await bus.PublishAsync(new OrderPlaced { OrderId = Guid.NewGuid(), CustomerId = $"customer-{i}", TotalAmount = 99.99m }, cancellationToken); } ``` With `MaxBatchSize = 50`, the 100 messages arrive as two batches of 50\. Expected output: ``` info: MyApp.Handlers.OrderPlacedBatchHandler[0] Processing batch of 50 orders info: MyApp.Handlers.OrderPlacedBatchHandler[0] Revenue summary created: 50 orders, $4,999.50 total info: MyApp.Handlers.OrderPlacedBatchHandler[0] Processing batch of 50 orders info: MyApp.Handlers.OrderPlacedBatchHandler[0] Revenue summary created: 50 orders, $4,999.50 total ``` ## Advanced: accessing the envelope For cases where you need the full consume context - message headers, correlation IDs, source addresses - implement `IConsumer` instead of a handler interface. C#OrderAuditConsumer.cs ``` using Mocha; using MyApp.Messages; namespace MyApp.Consumers; public class OrderAuditConsumer(ILogger logger) : IConsumer { public async ValueTask ConsumeAsync( IConsumeContext context, CancellationToken cancellationToken) { var order = context.Message; logger.LogInformation( "Audit: OrderId={OrderId} MessageId={MessageId} " + "CorrelationId={CorrelationId} Source={Source}", order.OrderId, context.MessageId, context.CorrelationId, context.SourceAddress); // Read custom headers attached at publish time if (context.Headers.TryGetValue("x-tenant", out var tenant)) { logger.LogInformation("Tenant: {Tenant}", tenant); } await Task.CompletedTask; } } ``` `IConsumeContext` gives you the deserialized message plus envelope fields: `MessageId`, `CorrelationId`, `ConversationId`, `CausationId`, `SourceAddress`, `DestinationAddress`, `SentAt`, `Headers`, `DeliveryCount`, and more. See [Messages](https://chillicream.com/docs/mocha/messages) for how correlation identifiers relate to each other. Register with `.AddConsumer()`: C# ``` builder.Services .AddMessageBus() .AddMyApp() // source-generated - registers OrderAuditConsumer automatically .AddRabbitMQ(); ``` Use `IConsumer` when you need envelope metadata or custom header inspection. For business logic that operates on the message payload alone, handler interfaces are simpler. ## DI scoping Mocha creates a new DI scope for each message. Your handler is instantiated from that scope, its constructor dependencies are resolved from it, and the scope is disposed when the handler completes. This means `DbContext` and other scoped services are safe to inject directly into handler constructors: C# ``` // AppDbContext is a scoped service - safe to inject public class OrderPlacedHandler(AppDbContext db, ILogger logger) : IEventHandler { public async ValueTask HandleAsync( OrderPlaced message, CancellationToken cancellationToken) { db.ProcessedOrders.Add(new ProcessedOrder { OrderId = message.OrderId }); await db.SaveChangesAsync(cancellationToken); } } ``` Each message gets its own scope and its own `DbContext` instance. Two messages processing concurrently do not share a `DbContext`. Singleton services are resolved from the root container as usual. If you inject a singleton that holds scoped state, you will get unexpected behavior - the same problem as in any ASP.NET Core application. ## Exception behavior When `HandleAsync` throws, the behavior depends on the handler type and the middleware pipeline: - **Event handlers and send handlers:** The exception is caught by the pipeline. By default, Mocha retries the message according to the configured retry policy, then moves it to the dead-letter queue if retries are exhausted. See [Reliability](https://chillicream.com/docs/mocha/reliability) for retry and fault configuration. - **Request handlers:** The exception propagates back to the caller as a fault. If you use `RequestAsync`, it throws on the caller side. The caller receives the error, not a timeout. - **Batch handlers:** If the handler throws, all messages in the batch fault together. The pipeline treats the entire batch as a failed unit. When a message arrives, it passes through middleware before reaching your handler. The pipeline handles fault routing, dead-letter delivery, observability, and concurrency limits - without any code in your handler. See [Middleware and Pipelines](https://chillicream.com/docs/mocha/middleware-and-pipelines) for details on writing custom pipeline middleware. ## Publishing from a handler To publish a message from within a handler, inject `IMessageBus` via the constructor: C# ``` public class OrderPlacedHandler( AppDbContext db, IMessageBus messageBus, ILogger logger) : IEventHandler { public async ValueTask HandleAsync( OrderPlaced message, CancellationToken cancellationToken) { // Handle the inbound event var invoice = new Invoice { OrderId = message.OrderId }; db.Invoices.Add(invoice); await db.SaveChangesAsync(cancellationToken); // Publish a downstream event await messageBus.PublishAsync( new InvoiceCreated { InvoiceId = invoice.Id, OrderId = message.OrderId, Amount = message.TotalAmount }, cancellationToken); logger.LogInformation( "Invoice created and InvoiceCreated published for order {OrderId}", message.OrderId); } } ``` Messages published from within a handler automatically inherit the `ConversationId` and `CorrelationId` from the inbound message. The bus sets `CausationId` on the outgoing message to the `MessageId` of the inbound message. This creates a traceable parent-child chain across services without any extra code. See [Messages](https://chillicream.com/docs/mocha/messages) for how correlation identifiers work. ## Further reading - [Handler Registration](https://chillicream.com/docs/mocha/handler-registration) \- How the source generator discovers handlers and how to customize registration. - [Event-Driven Consumer](https://www.enterpriseintegrationpatterns.com/patterns/messaging/EventDrivenConsumer.html) \- The EIP pattern that defines push-based message consumption, which is what Mocha's handlers implement. - [Competing Consumers](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CompetingConsumers.html) \- When multiple instances of your service run, they compete for messages on the same queue. This is the concurrency model for Mocha handlers under load. ## Next steps Your handlers are registered. Learn how the source generator discovers and registers them in [Handler Registration](https://chillicream.com/docs/mocha/handler-registration), or how Mocha routes messages to them in [Routing and Endpoints](https://chillicream.com/docs/mocha/routing-and-endpoints). > **Runnable examples:** [BatchHandler](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/HandlersAndConsumers/BatchHandler), [LowLevelConsumer](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/HandlersAndConsumers/LowLevelConsumer), [CustomConsumer](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/HandlersAndConsumers/CustomConsumer) > > **Full demo:** [Demo.Billing](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Billing) shows event handlers (`OrderPlacedEventHandler`), batch handlers (`OrderPlacedBatchHandler` with revenue aggregation, `BulkOrderBatchHandler` for high-volume processing), and request handlers (`ProcessRefundCommandHandler`). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/handlers-and-consumers.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Hosting - Mocha > Integrate Mocha with ASP.NET Core using the Mocha.Hosting package for health checks. Canonical source: https://chillicream.com/docs/mocha/hosting The `Mocha.Hosting` package provides ASP.NET Core integrations for the message bus: health checks that verify end-to-end connectivity through serialization, transport, routing, and handler execution. Bash ``` dotnet add package Mocha.Hosting ``` ## Health checks Mocha integrates with the [ASP.NET Core health checks](https://learn.microsoft.com/aspnet/core/host-and-monitor/health-checks) system. The health check sends a `HealthRequest` message through the bus and waits for a `HealthResponse`. This verifies the full pipeline - serialization, transport, routing, and handler execution - not just that the broker is reachable. ### Register the health check handler On the bus builder, call `.AddHealthCheck()` to register the built-in `HealthRequestHandler`. This handler responds to `HealthRequest` messages with an `"OK"` response. C# ``` using Mocha; using Mocha.Hosting; var builder = WebApplication.CreateBuilder(args); builder.Services .AddMessageBus() .AddHealthCheck() // Registers the HealthRequestHandler .AddMyApp() // source-generated handler registration .AddRabbitMQ(); ``` ### Add the health check to ASP.NET Core Use the `AddMessageBus()` extension on `IHealthChecksBuilder` to register a health check that sends a request through the bus and verifies the response: C# ``` builder.Services .AddHealthChecks() .AddMessageBus(); // Sends HealthRequest via RequestAsync and checks the reply ``` Then map the health check endpoint: C# ``` var app = builder.Build(); app.MapHealthChecks("/health"); app.Run(); ``` A `GET /health` request will now include the message bus status. If the bus cannot process and reply to the health request within the timeout, the check reports `Unhealthy`. ### Target a specific endpoint By default the health check uses the bus's default routing to deliver the `HealthRequest`. To target a specific endpoint (useful when you have multiple transports or want to verify a particular service), pass a URI: C# ``` builder.Services .AddHealthChecks() .AddMessageBus(new Uri("queue://my-service-health")); ``` The health check is registered with the `"ready"` and `"live"` tags, so you can use tag-based filtering to separate readiness from liveness probes: C# ``` app.MapHealthChecks("/health/ready", new() { Predicate = check => check.Tags.Contains("ready") }); app.MapHealthChecks("/health/live", new() { Predicate = check => check.Tags.Contains("live") }); ``` ## Next steps - [Observability](https://chillicream.com/docs/mocha/observability) \- Add OpenTelemetry tracing and metrics to the bus. - [Reliability](https://chillicream.com/docs/mocha/reliability) \- Configure outbox, inbox, and circuit breakers. - [Transports](https://chillicream.com/docs/mocha/transports) \- Configure RabbitMQ, InMemory, and multi-transport setups. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/hosting.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Mediator - Mocha > Use the Mocha Mediator to dispatch commands, queries, and notifications within a single process using zero-reflection, source-generated dispatch. Canonical source: https://chillicream.com/docs/mocha/mediator C# ``` builder.Services .AddMediator() .AddCatalog(); // source-generated from your assembly name ``` That registers the mediator infrastructure, discovers your handlers at compile time, and wires up the dispatch pipeline. `.AddCatalog()` is a source-generated extension method - it knows your handlers and message types at compile time and produces direct dispatch code with no reflection. ## What the mediator is The mediator sits between your application code and your handlers. Instead of injecting handler interfaces directly, you inject `IMediator` (or `ISender` / `IPublisher`) and dispatch messages through it. The mediator routes each message to the correct handler based on its type. C# ``` // Without mediator - tight coupling app.MapPost("/orders", async (PlaceOrderCommandHandler handler) => await handler.HandleAsync(new PlaceOrderCommand(...))); // With mediator - decoupled dispatch app.MapPost("/orders", async (ISender sender) => await sender.SendAsync(new PlaceOrderCommand(...))); ``` The mediator provides three things your handlers cannot do alone: a middleware pipeline that wraps every handler invocation with cross-cutting concerns (logging, transactions, validation), polymorphic dispatch that routes messages by type at runtime, and a seam between your application layer and your domain logic. This is the [Mediator pattern](https://refactoring.guru/design-patterns/mediator) \-- objects communicate through a central hub instead of referencing each other directly. If you have used [MediatR](https://github.com/jbogard/MediatR), the concepts are familiar. Mocha Mediator takes a different approach to performance: a [Roslyn source generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) analyzes your handler registrations at compile time and produces pre-compiled pipeline delegates. No `MakeGenericType`, no service provider lookups to resolve the pipeline, no reflection at runtime. ## When to use the mediator vs. the message bus Mocha has two dispatch mechanisms. Use the right one for the situation: | Use the **mediator** when... | Use the **message bus** when... | | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | Dispatch stays in-process | Messages cross process or service boundaries | | You want [CQRS](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs) separation of commands and queries | You want pub/sub events across services | | You need a request/response pipeline with middleware | You need transport-level features (retries, outbox) | | Handlers live in the same assembly or solution | Handlers live in different services | The mediator and the message bus complement each other. A common pattern is to use the mediator for in-process CQRS dispatch within a service, and the message bus for inter-service event-driven communication. ## Messages Messages are plain C# types that implement a marker interface. The marker interface tells the mediator how to route the message and what return type to expect. ### Commands Commands represent actions that change state. Use imperative verb-noun naming: `PlaceOrder`, `ProcessPayment`. C# ``` // A command that returns no response public record DeleteOrderCommand(Guid OrderId) : ICommand; // A command that returns a response public record PlaceOrderCommand( Guid ProductId, int Quantity, string CustomerId) : ICommand; public record PlaceOrderResult(bool Success, Guid? OrderId = null, string? Error = null); ``` ### Queries Queries represent read operations that return data without side effects. Use noun-based naming: `GetProducts`, `GetOrderById`. C# ``` public record GetProductsQuery : IQuery>; public record GetProductByIdQuery(Guid Id) : IQuery; ``` ### Notifications Notifications represent events that multiple handlers can observe. Use past-tense naming: `OrderPlaced`, `PaymentCompleted`. C# ``` public record OrderPlacedNotification( Guid OrderId, decimal Amount) : INotification; ``` ### Message type reference | Interface | Purpose | Dispatch method | Return type | | ------------------- | -------------------- | --------------- | -------------------- | | ICommand | Action, no response | SendAsync | ValueTask | | ICommand | Action with response | SendAsync | ValueTask | | IQuery | Read operation | QueryAsync | ValueTask | | INotification | Multi-handler event | PublishAsync | ValueTask | ## Handlers Each message type has a corresponding handler interface. The mediator routes each message to exactly one handler - except notifications, which fan out to all registered handlers. ### Command handlers C# ``` // Handles a void command public sealed class DeleteOrderCommandHandler(AppDbContext db) : ICommandHandler { public async ValueTask HandleAsync( DeleteOrderCommand command, CancellationToken cancellationToken) { var order = await db.Orders.FindAsync(command.OrderId); if (order is not null) { db.Orders.Remove(order); await db.SaveChangesAsync(cancellationToken); } } } // Handles a command with a response public sealed class PlaceOrderCommandHandler(AppDbContext db) : ICommandHandler { public async ValueTask HandleAsync( PlaceOrderCommand command, CancellationToken cancellationToken) { var order = new Order { Id = Guid.NewGuid(), ProductId = command.ProductId, Quantity = command.Quantity, CustomerId = command.CustomerId }; db.Orders.Add(order); await db.SaveChangesAsync(cancellationToken); return new PlaceOrderResult(true, order.Id); } } ``` ### Query handlers C# ``` public sealed class GetProductsQueryHandler(AppDbContext db) : IQueryHandler> { public async ValueTask> HandleAsync( GetProductsQuery query, CancellationToken cancellationToken) => await db.Products.ToListAsync(cancellationToken); } ``` ### Notification handlers Multiple handlers can subscribe to the same notification type. The mediator invokes all of them. C# ``` public sealed class SendOrderConfirmationEmail(IEmailService email) : INotificationHandler { public async ValueTask HandleAsync( OrderPlacedNotification notification, CancellationToken cancellationToken) { await email.SendAsync( $"Order {notification.OrderId} confirmed", cancellationToken); } } public sealed class UpdateAnalyticsDashboard(IAnalytics analytics) : INotificationHandler { public async ValueTask HandleAsync( OrderPlacedNotification notification, CancellationToken cancellationToken) { await analytics.RecordOrderAsync( notification.OrderId, notification.Amount); } } ``` ### Handler interface reference | Interface | Message type | Response | | ------------------------------------ | ------------------- | --------- | | ICommandHandler | ICommand | void | | ICommandHandler | ICommand | TResponse | | IQueryHandler | IQuery | TResponse | | INotificationHandler | INotification | void | ## Dispatching messages Inject `IMediator`, `ISender`, or `IPublisher` from DI and call the appropriate method. C# ``` // Send a command with a response app.MapPost("/orders", async (PlaceOrderRequest request, ISender sender) => { var result = await sender.SendAsync( new PlaceOrderCommand(request.ProductId, request.Quantity, request.CustomerId)); return result.Success ? Results.Created($"/api/orders/{result.OrderId}", result) : Results.BadRequest(result.Error); }); // Send a query app.MapGet("/products", async (ISender sender) => await sender.QueryAsync(new GetProductsQuery())); // Publish a notification app.MapPost("/orders/{id}/ship", async (Guid id, IPublisher publisher) => { await publisher.PublishAsync(new OrderShippedNotification(id)); return Results.Ok(); }); ``` `ISender` handles commands and queries. `IPublisher` handles notifications. `IMediator` combines both interfaces - inject it when you need both in the same class. ### Untyped dispatch When the message type is not known at compile time, use the `object`\-based overloads: C# ``` // Dispatch a command or query by runtime type object message = GetMessageFromSomewhere(); object? result = await sender.SendAsync(message); // Dispatch a notification by runtime type object notification = GetNotificationFromSomewhere(); await publisher.PublishAsync(notification); ``` The runtime type of the message must implement one of the marker interfaces (`ICommand`, `ICommand`, `IQuery`, or `INotification`). An exception is thrown if it does not. ## Registration and source generation ### Register the mediator C# ``` builder.Services .AddMediator() .AddCatalog(); // source-generated from assembly name "Demo.Catalog" ``` `AddMediator()` registers the core mediator infrastructure and the default notification publish mode. The source-generated `Add{ModuleName}()` method registers: - All command, query, and notification handlers found in your assembly - Pre-compiled terminal delegates for each message type (no reflection at runtime) You do not register handlers manually unless you need to. The source generator discovers them by scanning for classes that implement handler interfaces. ### Module naming The source generator names the registration method based on your assembly: 1. If you apply `[assembly: MediatorModule("Billing")]`, the method is `AddBilling()` 2. Otherwise, it uses the last segment of the assembly name: `Demo.Catalog` produces `AddCatalog()` To set an explicit module name, add the attribute to any file in your project: C# ``` using Mocha.Mediator; [assembly: MediatorModule("Billing")] ``` ### Manual handler registration with AddHandler When you need to register handlers outside the source generator's reach - from a plugin assembly, a dynamically loaded module, or in integration tests - use `AddHandler()`: C# ``` builder.Services .AddMediator() .AddHandler() .AddHandler() .AddHandler(); ``` `AddHandler()` inspects the type for handler interfaces (`ICommandHandler`, `IQueryHandler`, `INotificationHandler`), builds the pipeline configuration, and registers the handler in DI. It throws `InvalidOperationException` if `T` does not implement a handler interface. You can mix source-generated and manual registration. If both register the same handler type, the configurations are composed: C# ``` builder.Services .AddMediator() .AddCatalog() // source-generated handlers .AddHandler(); // additional handler from another assembly ``` > **Prefer the source generator.** The source-generated registration path uses pre-compiled terminal delegates with no reflection. The source-generated output is designed for long-term stability across versions. Manual registration is available for edge cases but its internal behavior may change between releases. ### Configure service lifetime By default, handlers are registered as `Scoped`. To change the default: C# ``` builder.Services .AddMediator() .ConfigureOptions(options => { options.ServiceLifetime = ServiceLifetime.Transient; }) .AddCatalog(); ``` Call `ConfigureOptions` before `Add{ModuleName}()` so the source-generated method reads the updated lifetime. ### Configuration options reference | Option | Type | Default | Description | | ----------------------- | ----------------------- | ---------- | --------------------------------------------- | | ServiceLifetime | ServiceLifetime | Scoped | Default DI lifetime for handler registrations | | NotificationPublishMode | NotificationPublishMode | Sequential | How notification handlers are dispatched | ## Named mediators To run multiple independent mediator instances (each with its own handlers and middleware), use named mediators. Named mediators use .NET's [keyed dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#keyed-services). C# ``` // Register a named mediator builder.Services .AddMediator("billing") .AddBilling(); // Register the default (unnamed) mediator builder.Services .AddMediator() .AddCatalog(); ``` Resolve a named mediator from DI: C# ``` app.MapPost("/payments", async ( [FromKeyedServices("billing")] ISender sender, ProcessPaymentRequest request) => { var result = await sender.SendAsync( new ProcessPaymentCommand(request.Amount)); return Results.Ok(result); }); // Or resolve from IServiceProvider directly var billingMediator = serviceProvider .GetRequiredKeyedService("billing"); ``` Each named mediator has its own handler registrations, middleware pipeline, and runtime. The default mediator (registered with `AddMediator()` without a name) is resolved normally without keyed services. ## Putting it together Here is a complete minimal API application with commands, queries, and notifications: C# ``` using Mocha.Mediator; var builder = WebApplication.CreateBuilder(args); // Register mediator with source-generated handlers builder.Services .AddMediator() .AddMyApp(); var app = builder.Build(); // Command - place an order app.MapPost("/orders", async (PlaceOrderRequest request, ISender sender) => { var result = await sender.SendAsync( new PlaceOrderCommand(request.ProductId, request.Quantity)); return result.Success ? Results.Created($"/orders/{result.OrderId}", result) : Results.BadRequest(result.Error); }); // Query - list products app.MapGet("/products", async (ISender sender) => await sender.QueryAsync(new GetProductsQuery())); // Notification - broadcast that an order shipped app.MapPost("/orders/{id}/ship", async (Guid id, IPublisher publisher) => { await publisher.PublishAsync(new OrderShippedNotification(id)); return Results.Ok(); }); app.Run(); // ── Messages ──────────────────────────────────────── public record PlaceOrderCommand(Guid ProductId, int Quantity) : ICommand; public record PlaceOrderResult( bool Success, Guid? OrderId = null, string? Error = null); public record GetProductsQuery : IQuery>; public record ProductDto(Guid Id, string Name, decimal Price); public record OrderShippedNotification(Guid OrderId) : INotification; // ── Handlers ──────────────────────────────────────── public sealed class PlaceOrderCommandHandler(ILogger logger) : ICommandHandler { public ValueTask HandleAsync( PlaceOrderCommand command, CancellationToken cancellationToken) { var orderId = Guid.NewGuid(); logger.LogInformation("Order {OrderId} placed", orderId); return new ValueTask( new PlaceOrderResult(true, orderId)); } } public sealed class GetProductsQueryHandler : IQueryHandler> { private static readonly IReadOnlyList Products = [ new(Guid.NewGuid(), "Keyboard", 149.99m), new(Guid.NewGuid(), "Mouse", 79.99m), ]; public ValueTask> HandleAsync( GetProductsQuery query, CancellationToken cancellationToken) => new(Products); } public sealed class OrderShippedEmailHandler(ILogger logger) : INotificationHandler { public ValueTask HandleAsync( OrderShippedNotification notification, CancellationToken cancellationToken) { logger.LogInformation( "Order {OrderId} shipped - email sent", notification.OrderId); return ValueTask.CompletedTask; } } public record PlaceOrderRequest(Guid ProductId, int Quantity); ``` If everything worked, `dotnet run` starts the server and you can: - `POST /orders` with a JSON body to place an order - `GET /products` to list products - `POST /orders/{id}/ship` to publish a shipped notification ## Troubleshooting ### `InvalidOperationException: No pipeline registered for message type` The source generator did not find a handler for your message type. Verify: - Your handler class implements the correct interface (e.g., `ICommandHandler`) - Your message type implements the correct marker interface (e.g., `ICommand`) - You called the source-generated `.Add{ModuleName}()` method on the mediator builder - The handler is in the same project that the source generator can see - The project references `Mocha.Analyzers` as an analyzer (not a regular project reference) ### Handlers are not being called If dispatch succeeds but your handler code does not execute, check that: - Your middleware calls the `next` delegate - a middleware that forgets to call `next` silently short-circuits the pipeline - You are not accidentally registering handlers manually in addition to the source-generated method, which could result in duplicate registrations ### The source-generated method does not appear If IntelliSense does not show `Add{ModuleName}()`: - Confirm the `Mocha.Analyzers` package is referenced with `OutputItemType="Analyzer"` in your `.csproj` - Rebuild the project - source generators run during compilation - Check the build output for analyzer warnings prefixed with `MO` ### `InvalidOperationException` when calling `AddHandler()` The type you passed does not implement any handler interface. Make sure `T` implements one of: `ICommandHandler`, `ICommandHandler`, `IQueryHandler`, or `INotificationHandler`. ### Named mediator returns wrong handlers Each named mediator resolves handlers from the same DI container. Make sure you register each module's handlers on the correct `IMediatorHostBuilder` instance - the one returned by the `AddMediator("name")` call for that name. ## Next steps You have a working mediator with CQRS dispatch. Here is where to go next: - **Customize the pipeline:** [Pipeline & Middleware](https://chillicream.com/docs/mocha/mediator/pipeline-and-middleware) \- add validation, logging, transactions, and other cross-cutting concerns. Configure notification publish modes and OpenTelemetry instrumentation. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/mediator/index.md) Maintained by ChilliCream. Last updated on **July 01, 2026** by **Tobias Tengler** --- # Pipeline & Middleware - Mocha > Add cross-cutting concerns like logging, validation, and transactions to the Mocha Mediator dispatch pipeline with custom middleware. Canonical source: https://chillicream.com/docs/mocha/mediator/pipeline-and-middleware C# ``` internal sealed class LoggingMiddleware(ILogger logger) { public async ValueTask InvokeAsync(IMediatorContext context, MediatorDelegate next) { logger.LogInformation("Handling {MessageType}...", context.MessageType.Name); var sw = Stopwatch.StartNew(); await next(context); sw.Stop(); logger.LogInformation( "Handled {MessageType} in {ElapsedMs}ms", context.MessageType.Name, sw.ElapsedMilliseconds); } public static MediatorMiddlewareConfiguration Create() => new( static (factoryCtx, next) => { var logger = factoryCtx.Services.GetRequiredService>(); var middleware = new LoggingMiddleware(logger); return ctx => middleware.InvokeAsync(ctx, next); }, "Logging"); } ``` That is a middleware. It wraps every command, query, and notification with timing and logging. Register it with `.Use(LoggingMiddleware.Create())` and it runs for every message that passes through the pipeline. ## How the pipeline works The mediator compiles a middleware pipeline for each registered message type at application startup. Each middleware wraps the next one, forming a [chain of responsibility](https://refactoring.guru/design-patterns/chain-of-responsibility) that terminates at the handler. If you have used [middleware in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/), the mental model is the same - this is the [Pipes and Filters](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PipesAndFilters.html) pattern applied to in-process message dispatch. ``` SendAsync(PlaceOrderCommand) -> LoggingMiddleware -> ValidationMiddleware -> TransactionMiddleware -> PlaceOrderCommandHandler <- commit / rollback <- throw on invalid <- log elapsed time ``` The pipeline is built from two delegate types: C# ``` // The terminal pipeline delegate - each step in the chain has this shape public delegate ValueTask MediatorDelegate(IMediatorContext context); // The factory that creates a middleware - runs once per message type at startup public delegate MediatorDelegate MediatorMiddleware( MediatorMiddlewareFactoryContext context, MediatorDelegate next); ``` At startup, the mediator iterates every registered middleware in reverse order. Each factory receives the `next` delegate and returns a new delegate that wraps it. The result is a single compiled `MediatorDelegate` per message type. At runtime, dispatch is a direct delegate invocation - no reflection, no generic resolution. ## Write a middleware A middleware is a class with an `InvokeAsync(IMediatorContext, MediatorDelegate)` method and a static `Create()` method that returns a `MediatorMiddlewareConfiguration`. The configuration holds two things: a factory delegate and an optional string key used for [positioning](#middleware-positioning). The factory delegate receives two arguments: | Argument | Available at | Purpose | | -------------------------------- | ---------------------- | ----------------------------------------------------------------------------------- | | MediatorMiddlewareFactoryContext | Startup (compile time) | Resolve singleton services, inspect message/response types, opt out of the pipeline | | MediatorDelegate next | Startup (compile time) | The next middleware or handler in the chain | The factory returns a `MediatorDelegate` \- the runtime function that receives `IMediatorContext` for each dispatch. By convention, that runtime delegate forwards to an `InvokeAsync` method on a small middleware class so the dispatch logic reads top-to-bottom. Here is a minimal timing middleware: C# ``` internal sealed class TimingMiddleware(ILogger logger) { public async ValueTask InvokeAsync(IMediatorContext context, MediatorDelegate next) { var sw = Stopwatch.StartNew(); await next(context); // call the next middleware or handler sw.Stop(); logger.LogInformation( "{MessageType} handled in {ElapsedMs}ms", context.MessageType.Name, sw.ElapsedMilliseconds); } public static MediatorMiddlewareConfiguration Create() => new( static (factoryCtx, next) => { // 1. Resolve services once at startup (not per request) var logger = factoryCtx.Services.GetRequiredService>(); // 2. Build the middleware instance once var middleware = new TimingMiddleware(logger); // 3. Return the runtime delegate return ctx => middleware.InvokeAsync(ctx, next); }, "Timing"); // 4. Key for positioning } ``` Register it on the mediator builder: C# ``` builder.Services .AddMediator() .AddCatalog() // source-generated handler registration .Use(TimingMiddleware.Create()); ``` The `IMediatorContext` available at runtime provides everything you need during dispatch: | Property | Type | Description | | ----------------- | ------------------ | ------------------------------------------------------------------------- | | Message | object | The message instance being dispatched | | MessageType | Type | Runtime type of the message | | ResponseType | Type | Expected response type (typeof(void) for void commands and notifications) | | Result | object? | The handler's return value, readable by middleware after calling next | | Services | IServiceProvider | Scoped service provider for the current request | | CancellationToken | CancellationToken | Cancellation token for the operation | | Features | IFeatureCollection | Per-request feature collection for sharing state between middleware | | Runtime | IMediatorRuntime | The mediator runtime that owns this context | ### Short-circuiting To prevent the handler from executing, return from `InvokeAsync` without calling `next`: C# ``` public async ValueTask InvokeAsync(IMediatorContext context, MediatorDelegate next) { if (context.Message is PlaceOrderCommand { Quantity: <= 0 }) { throw new ArgumentException("Quantity must be greater than zero."); } await next(context); // only reached if validation passes } ``` ### Exception handling Wrap `next` in a try/catch to handle exceptions: C# ``` internal sealed class ExceptionHandlingMiddleware(ILogger logger) { public async ValueTask InvokeAsync(IMediatorContext context, MediatorDelegate next) { try { await next(context); } catch (Exception ex) { logger.LogError(ex, "Error handling {MessageType}", context.MessageType.Name); throw; // re-throw or set context.Result to recover } } public static MediatorMiddlewareConfiguration Create() => new( static (factoryCtx, next) => { var logger = factoryCtx.Services.GetRequiredService>(); var middleware = new ExceptionHandlingMiddleware(logger); return ctx => middleware.InvokeAsync(ctx, next); }, "ExceptionHandling"); } ``` To recover from an exception instead of re-throwing, set `context.Result` to a fallback value and return normally. ## Compile-time filtering The `MediatorMiddlewareFactoryContext` is available during pipeline compilation - before your application handles its first request. Use it to exclude your middleware from pipelines where it does not apply. To opt out, return `next` directly from the factory. The middleware is not included in that pipeline at all - zero runtime cost, no delegate wrapper, no type check on every dispatch. C# ``` internal sealed class TransactionMiddleware { public async ValueTask InvokeAsync(IMediatorContext context, MediatorDelegate next) { // Resolve DbContext from the scoped service provider so each dispatch gets its own var db = context.Services.GetRequiredService(); await using var tx = await db.Database.BeginTransactionAsync(context.CancellationToken); try { await next(context); await tx.CommitAsync(context.CancellationToken); } catch { await tx.RollbackAsync(context.CancellationToken); throw; } } public static MediatorMiddlewareConfiguration Create() => new( static (factoryCtx, next) => { // Skip notifications and queries at compile time - they don't need transactions if (factoryCtx.IsNotification() || factoryCtx.IsQuery()) { return next; // not included in this pipeline } var middleware = new TransactionMiddleware(); return ctx => middleware.InvokeAsync(ctx, next); }, "Transaction"); } ``` Notice that `DbContext` is scoped, so it must be resolved per dispatch from `context.Services` inside `InvokeAsync` \- **not** from `factoryCtx.Services` in the factory, which would capture a single startup-scope instance and share it across every message. ### Message kind checks | Method | Returns true when | | ----------------------- | ------------------------------------------- | | IsCommand() | Void command (ICommand) | | IsCommandWithResponse() | Command with response (ICommand) | | IsQuery() | Query (IQuery) | | IsNotification() | Notification (INotification) | ### Type assignability checks | Method | Returns true when | | ---------------------------- | ---------------------------------------------------------------------------- | | IsMessageAssignableTo() | Message type is assignable to T | | IsMessageAssignableTo(Type) | Message type is assignable to the given type | | IsResponseAssignableTo() | Response type is assignable to T (false for void commands and notifications) | | IsResponseAssignableTo(Type) | Response type is assignable to the given type | Use `IsMessageAssignableTo` to scope a middleware to a specific message or base type. Once the factory has filtered, `InvokeAsync` can cast directly without re-checking: C# ``` internal sealed class PlaceOrderValidationMiddleware { public async ValueTask InvokeAsync(IMediatorContext context, MediatorDelegate next) { // Safe cast - the factory's compile-time filter guarantees the message type var order = (PlaceOrderCommand)context.Message; if (order.Quantity <= 0) { throw new ArgumentException("Quantity must be greater than zero."); } await next(context); } public static MediatorMiddlewareConfiguration Create() => new( static (factoryCtx, next) => { // Only compile this middleware into the PlaceOrderCommand pipeline. // Every other pipeline gets `next` directly - no wrapper, no per-dispatch type check. if (!factoryCtx.IsMessageAssignableTo()) { return next; } var middleware = new PlaceOrderValidationMiddleware(); return ctx => middleware.InvokeAsync(ctx, next); }, "Validation"); } ``` Use `IsResponseAssignableTo` to filter by response type: C# ``` // Only audit pipelines that return OrderResult if (!factoryCtx.IsResponseAssignableTo()) { return next; } ``` ### When to use compile-time vs. runtime checks Use **compile-time filtering** (`MediatorMiddlewareFactoryContext`) when: - You know at registration time which message kinds the middleware applies to - You want zero overhead for pipelines that do not need the middleware - You are filtering by message kind, response type, or base class Use **runtime checks** (`IMediatorContext`) when: - You need to inspect the actual message instance (check a property value) - The decision depends on runtime state (feature flags, configuration) Both approaches combine well - filter out entire message kinds at compile time, then do finer-grained checks at runtime for the pipelines that remain. ## Middleware positioning The `Use` method accepts optional `before` and `after` parameters to control where the middleware sits in the pipeline. | Call | Behavior | | ------------------------------------- | ------------------------------------------------------- | | Use(config) | Appends to the end of the middleware list | | Use(config, before: "Logging") | Inserts before the middleware with key "Logging" | | Use(config, after: "Instrumentation") | Inserts after the middleware with key "Instrumentation" | Only one of `before` or `after` can be specified at the same time. If the referenced key is not found, an `InvalidOperationException` is thrown at startup. C# ``` builder.Services .AddMediator() .AddCatalog() .Use(LoggingMiddleware.Create()) // position 1 .Use(ValidationMiddleware.Create()) // position 2 .Use(ExceptionHandlingMiddleware.Create()) // position 3 .Use(SecurityMiddleware.Create(), before: "Logging") // before "Logging" .Use(CorrelationIdMiddleware.Create(), after: "Logging"); // after "Logging" ``` Resulting order: Security -> Logging -> CorrelationId -> Validation -> ExceptionHandling -> Handler. The `Key` property on `MediatorMiddlewareConfiguration` is optional. Middleware without a key can still be registered with `Use(config)`, but cannot be referenced by other middleware for relative positioning. ### Built-in middleware keys | Key | Middleware | Added by | | ---------------------------- | ------------------------------------ | ----------------------------------------------------- | | "Instrumentation" | MediatorDiagnosticMiddleware | Always present (added automatically by AddMediator()) | | "EntityFrameworkTransaction" | EntityFrameworkTransactionMiddleware | UseEntityFrameworkTransactions() | ## Pipeline execution order Middleware executes in registration order. The first registered middleware becomes the outermost layer - it runs first on the way in and last on the way out. ``` Registered: [Instrumentation, Logging, Validation, Transaction] Instrumentation <- outermost (runs first) Logging Validation Transaction Handler <- innermost Transaction returns Validation returns Logging returns Instrumentation returns <- runs last on the way out ``` The `Instrumentation` middleware is always present as the first entry because `AddMediator()` adds it automatically. Your middleware registered via `Use()` follows in the order you call it. ## Notification publish modes When a notification has multiple handlers, the **notification publish mode** controls how they are invoked. Configure it via `MediatorOptions`: C# ``` builder.Services .AddMediator() .ConfigureOptions(o => o.NotificationPublishMode = NotificationPublishMode.Sequential) .AddCatalog(); ``` | Mode | Behavior | Default | | ---------- | -------------------------------------------------------------- | ------- | | Sequential | Invokes handler pipelines one at a time, in registration order | Yes | | Concurrent | Invokes all handler pipelines concurrently with Task.WhenAll | No | ### Sequential mode (default) Handlers execute one after another. If a handler throws, subsequent handlers do not execute and the exception propagates to the caller. This is the right choice when handlers have ordering dependencies or when you want fail-fast behavior. ### Concurrent mode All handlers execute in parallel. If any handler throws, the remaining handlers still run to completion and all exceptions are collected into an `AggregateException`. C# ``` builder.Services .AddMediator() .ConfigureOptions(o => o.NotificationPublishMode = NotificationPublishMode.Concurrent) .AddCatalog(); ``` Warning In concurrent mode, all handler pipelines share the same scoped `IServiceProvider`. Scoped services such as `DbContext` are not thread-safe and must not be used concurrently across handlers. If your notification handlers need scoped services, use `Sequential` mode or create a new scope inside each handler. ### Per-handler middleware pipelines Each notification handler gets its own independently compiled middleware pipeline. When middleware is registered on the mediator, it wraps each notification handler individually - not the notification dispatch as a whole. ``` PublishAsync(OrderPlacedNotification) ├── Pipeline for SendOrderConfirmationEmail: │ Instrumentation -> Logging -> SendOrderConfirmationEmail │ └── Pipeline for UpdateAnalyticsDashboard: Instrumentation -> Logging -> UpdateAnalyticsDashboard ``` This means middleware like logging or exception handling runs independently around each handler. If middleware around one handler modifies `ctx.Result` or catches an exception, it does not affect the other handler's pipeline. ## Entity Framework Core transactions The `Mocha.EntityFrameworkCore` package provides middleware that wraps command handlers in a database transaction. Install the package and call `UseEntityFrameworkTransactions`: C# ``` builder.Services .AddMediator() .AddCatalog() .UseEntityFrameworkTransactions(); ``` The middleware (key: `"EntityFrameworkTransaction"`): 1. Checks at compile time whether the pipeline is for a command. Queries and notifications are excluded by default - the middleware is not present in their pipelines at all. 2. Begins a database transaction 3. Calls the next middleware or handler 4. Commits the transaction on success 5. Rolls back on any exception Your command handlers are responsible for calling `SaveChangesAsync` to persist their changes. The middleware handles the transaction lifecycle - your handlers do not need to call `BeginTransactionAsync`, `CommitAsync`, or `RollbackAsync`. ### Customizing transaction scope To override which messages get a transaction, provide a `ShouldCreateTransaction` predicate: C# ``` builder.Services .AddMediator() .AddCatalog() .UseEntityFrameworkTransactions(options => { options.ShouldCreateTransaction = context => { // Wrap everything except this specific query return context.MessageType != typeof(GetCachedReportQuery); }; }); ``` When `ShouldCreateTransaction` is set, the compile-time elimination for queries and notifications is disabled - the middleware is included in every pipeline and the predicate runs at dispatch time instead. The `ShouldCreateTransaction` delegate receives the `IMediatorContext`, giving you access to the message type and instance for fine-grained control. ## Instrumentation and observability The `MediatorDiagnosticMiddleware` (key: `"Instrumentation"`) is always present in the pipeline. By default it uses a no-op listener. To activate OpenTelemetry-compatible tracing, call `AddInstrumentation`: C# ``` builder.Services .AddMediator() .AddCatalog() .AddInstrumentation(); ``` This registers the `ActivityMediatorDiagnosticListener`, which follows the [OpenTelemetry messaging semantic conventions](https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/): - Creates an [Activity](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts) (OpenTelemetry span) named `"{MessageTypeName} send"` for commands/queries or `"{MessageTypeName} publish"` for notifications - Tags the span with `messaging.system` \= `"mocha.mediator"`, `messaging.operation.type` \= `"send"` or `"publish"`, and `messaging.message.type` \= the message type name - Sets `ActivityStatusCode.Ok` on success - On error: adds an `exception` event with `exception.type` and `exception.message` tags, and sets `ActivityStatusCode.Error` Configure your OpenTelemetry exporter to collect from the `Mocha.Mediator` source: C# ``` builder.Services.AddOpenTelemetry() .WithTracing(t => t.AddSource("Mocha.Mediator")) .WithMetrics(m => m.AddMeter("Mocha.Mediator")); ``` ### Custom diagnostic event listeners To add your own instrumentation alongside or instead of the built-in listener, extend `MediatorDiagnosticEventListener`: C# ``` public sealed class SlowMessageListener : MediatorDiagnosticEventListener { public override IDisposable Execute( Type messageType, Type responseType, object message) { return new TimingScope(messageType); } public override void ExecutionError( Type messageType, Type responseType, object message, Exception exception) { // log or alert on errors } private sealed class TimingScope(Type messageType) : IDisposable { private readonly long _start = Stopwatch.GetTimestamp(); public void Dispose() { var elapsed = Stopwatch.GetElapsedTime(_start); if (elapsed > TimeSpan.FromSeconds(1)) Console.WriteLine($"Slow message: {messageType.Name} took {elapsed}"); } } } ``` Register it: C# ``` builder.Services .AddMediator() .AddCatalog() .AddDiagnosticEventListener(); ``` Multiple listeners can be registered. They all receive every diagnostic event in registration order. ## Troubleshooting ### Middleware does not run for a specific message type If your middleware factory returns `next` for that message type (via compile-time filtering), the middleware is excluded from the pipeline entirely. Check your `IsCommand()`, `IsQuery()`, `IsNotification()`, or `IsMessageAssignableTo()` conditions. The filtering runs once at startup, so you will not see any runtime indication that the middleware was skipped. ### Middleware runs in the wrong order Middleware executes in registration order (first registered = outermost). Use `Use(config, before: "key")` or `Use(config, after: "key")` to control placement relative to other middleware. Check that the middleware you are referencing has a `Key` set in its `MediatorMiddlewareConfiguration`. ### Entity Framework transactions do not wrap queries This is the default behavior. The `EntityFrameworkTransactionMiddleware` excludes queries and notifications at compile time. To include specific queries, set `ShouldCreateTransaction` in the options. Note that setting this predicate disables compile-time elimination - the middleware will be included in all pipelines and the predicate runs at dispatch time. ### No OpenTelemetry traces appear The `MediatorDiagnosticMiddleware` is always present, but it uses a no-op listener by default. You must call `.AddInstrumentation()` to register the `ActivityMediatorDiagnosticListener`. You also need to configure your OpenTelemetry SDK to collect from the `Mocha.Mediator` source via `.AddSource("Mocha.Mediator")`. ### Services resolved in the factory vs. at runtime Services resolved from `factoryCtx.Services` in the middleware factory are resolved once at startup from the mediator's internal service provider. Use this for singletons like `ILoggerFactory`. To resolve scoped services (like `DbContext`), use `ctx.Services` inside the runtime delegate instead. ### Notification handler throws but other handlers do not run In `Sequential` mode (the default), the first handler exception stops execution of subsequent handlers. If you need all handlers to run regardless of failures, switch to `Concurrent` mode. In `Concurrent` mode, all handlers run to completion and exceptions are aggregated into an `AggregateException`. ### Scoped service exceptions in concurrent notifications When using `NotificationPublishMode.Concurrent`, all handler pipelines execute in parallel but share the same scoped `IServiceProvider`. Scoped services like `DbContext` are not thread-safe. You will see race conditions or `ObjectDisposedException` if multiple handlers access the same scoped service concurrently. Switch to `Sequential` mode or create a new `IServiceScope` inside handlers that need their own scoped services. > **Full demo:** The [Demo application](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo) uses `UseEntityFrameworkTransactions` and `AddInstrumentation` alongside the mediator and message bus in a complete e-commerce system. ## Next steps - **Mediator overview:** [Overview](https://chillicream.com/docs/mocha/mediator) \- messages, handlers, dispatching, and registration. - **Message bus middleware:** [Middleware & Pipelines](https://chillicream.com/docs/mocha/middleware-and-pipelines) \- the message bus has its own three-layer pipeline (dispatch, receive, consume) using the same middleware model. - **Cross service boundaries:** [Messaging Patterns](https://chillicream.com/docs/mocha/messaging-patterns) \- when your commands need to reach another service, switch to the message bus. - **Coordinate workflows:** [Sagas](https://chillicream.com/docs/mocha/sagas) \- orchestrate multi-step processes across services. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/mediator/pipeline-and-middleware.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Messages - Mocha > Understand the message envelope, naming conventions, correlation, and message type identity in Mocha, and learn how to define message types. Canonical source: https://chillicream.com/docs/mocha/messages A message is any C# class, record, or struct. No base class, no marker interface, no framework attributes. You define a type that holds your business data, and Mocha handles everything else - routing, serialization, correlation, and delivery. Records are used throughout the examples because they naturally fit message semantics, but they are not required. Mocha serializes message bodies as JSON by default. ## Why envelopes exist When a message crosses a process boundary, it carries more than your business payload. The receiving service needs to know what type the message is, where it came from, when it was sent, and how it relates to other messages in a workflow. None of this belongs in your domain model. Mocha solves this with the **envelope pattern**: every message is wrapped in an envelope containing headers and the serialized body. Your POCO contains business data; the envelope contains infrastructure metadata. Keep them separate. This separation means your `OrderPlaced` record stays a clean contract with business properties. The envelope wraps it with everything the infrastructure needs: correlation identifiers, addressing, timestamps, and custom headers. The bus builds the envelope automatically when you publish or send - most of the time you never interact with it directly. ## Naming conventions > **Convention:** Name commands in imperative verb-noun form (`PlaceOrder`, `ProcessPayment`). Name events in past-tense noun-verb form (`OrderPlaced`, `PaymentProcessed`). The name communicates intent - commands request action, events announce what happened. Following this convention makes the intent of each message type clear at a glance and keeps your codebase consistent with the broader .NET messaging ecosystem. ## Define and use messages This section walks through defining a message, attaching custom headers when publishing, and reading envelope metadata inside a handler. ### Define a message Messages can be any class, record, or struct. Records with `{ get; init; }` properties are a natural fit: C#OrderPlaced.cs ``` namespace MyApp; public sealed record OrderPlaced { public required Guid OrderId { get; init; } public required string CustomerId { get; init; } public required decimal TotalAmount { get; init; } } ``` The record holds business data only. No framework types, no base classes, no marker interfaces. ### Publish with custom headers To attach custom metadata, pass `PublishOptions` with a `Headers` dictionary: C# ``` await bus.PublishAsync( new OrderPlaced { OrderId = Guid.NewGuid(), CustomerId = "CUST-001", TotalAmount = 99.95m }, new PublishOptions { Headers = new() { ["x-tenant"] = "acme", ["x-trace-id"] = "abc-123" } }, CancellationToken.None); ``` For commands, use `SendOptions` with the same `Headers` property: C# ``` await bus.SendAsync( new ProcessPayment { OrderId = "ORD-1", Amount = 99.95m }, new SendOptions { Headers = new() { ["x-tenant"] = "acme" } }, CancellationToken.None); ``` The bus merges your headers into the envelope's header collection before dispatching. ### Message headers A header value must map to one of these portable representations: `null`, a boolean, a string, a number, an object with string keys, or an array. You can set headers with the following CLR values; Mocha converts them as shown: | Value supplied | Header value | | ---------------------------------------------------------------- | -------------------------------------------------- | | null, bool, string | null, boolean, or string | | sbyte, byte, short, ushort, int, uint, long, ulong | integer number | | float, double, decimal | number | | char | one-character string | | Guid | GUID string | | Uri | original URI string | | an enum | member name | | DateTime, DateTimeOffset, DateOnly, TimeOnly | ISO 8601 string | | TimeSpan | constant-format (c) string | | byte\[\], ArraySegment, ReadOnlyMemory, Memory | base64 string | | a JSON scalar | the corresponding null, boolean, string, or number | | a JSON object, nested headers, or a dictionary with string keys | object whose values are mapped recursively | | a JSON array or sequence | array whose values are mapped recursively | Dictionary keys must be strings, and every value in a dictionary or sequence must itself be a supported header value. Use a string when the exact text representation matters, such as for a high-precision decimal or an application-specific identifier. Other CLR types are not supported as header values. ### Access envelope metadata in a handler `IEventHandler` receives the deserialized message and a cancellation token - that is all. To read message IDs, correlation IDs, timestamps, or custom headers, implement `IConsumer` instead. The `IConsumeContext` parameter gives you both the deserialized message and all envelope fields: C#OrderAuditConsumer.cs ``` using Mocha; namespace MyApp; public class OrderAuditConsumer(ILogger logger) : IConsumer { public ValueTask ConsumeAsync( IConsumeContext context, CancellationToken cancellationToken) { // The deserialized message var order = context.Message; // Envelope metadata - generated by the bus automatically logger.LogInformation( "MessageId={MessageId} CorrelationId={CorrelationId} " + "ConversationId={ConversationId} SentAt={SentAt}", context.MessageId, context.CorrelationId, context.ConversationId, context.SentAt); // Custom headers you attached when publishing if (context.Headers.TryGetValue("x-tenant", out var tenant)) { logger.LogInformation("Tenant: {Tenant}", tenant); } return default; } } ``` Register a consumer with `.AddConsumer()`: C# ``` builder.Services .AddMessageBus() .AddConsumer() .AddInMemory(); ``` When the handler processes a published `OrderPlaced` event, you see output like: ``` MessageId=3f2a... CorrelationId=7b1c... ConversationId=9d4e... SentAt=2026-02-25T10:30:00Z Tenant: acme ``` The bus generates `MessageId`, `CorrelationId`, and `ConversationId` automatically. Your custom headers appear alongside them. Tip Use `IEventHandler` when you only need the message payload. Switch to `IConsumer` when you need envelope metadata or custom headers. Both can coexist for the same message type - Mocha routes to all registered handlers and consumers. ## How correlation works Mocha uses three identifiers to track relationships between messages: ``` ConversationId ─── groups all messages in a logical conversation │ ├── CorrelationId ─── chains messages in a specific workflow │ │ │ ├── CausationId ─── links parent → child │ │ │ │ │ └── CausationId ─── links parent → child │ │ │ └── CausationId ─── links parent → child │ └── CorrelationId ─── a different workflow in the same conversation ``` **ConversationId** is the broadest scope. When you publish the first message in a flow, the bus generates a `ConversationId`. Every subsequent message in that conversation - across services, across handler chains - inherits the same `ConversationId`. Use it to find all messages that belong to a single business transaction. **CorrelationId** is narrower. It groups messages within a specific workflow or saga instance. A single conversation may contain multiple correlation scopes - for example, an order saga and a payment saga both triggered by the same initial event. **CausationId** traces direct causality. When a handler publishes or sends a new message in response to a received message, the bus sets the new message's `CausationId` to the `MessageId` of the received message. This creates a parent-child chain you can follow to reconstruct the exact sequence of events. Together, these three identifiers give you full traceability without adding any fields to your message records. ## How message type resolution works When you register a message type - explicitly with `AddMessage()` or implicitly by adding a handler - Mocha assigns it a URN-based identity: ``` urn:message:: ``` For example, `MyApp.Contracts.OrderPlaced` becomes: ``` urn:message:my-app.contracts:order-placed ``` The bus stores this identity in the `MessageType` field of the envelope. On the receiving side, the message type selection middleware matches the incoming URN against the registered types to find the correct CLR type for deserialization. Use `AddMessage()` to configure a message type explicitly - for example, to pin its URN when refactoring CLR namespaces, or to configure a send route: C# ``` builder.Services .AddMessageBus() .AddMessage(d => { d.Send(route => route.ToQueue("orders-queue")); }) .AddInMemory(); ``` **Polymorphic messages.** If a message class implements interfaces or extends base classes that are also registered as message types, the envelope carries all of those identities in the `EnclosedMessageTypes` array. This allows a handler registered for an interface to receive messages that implement it. ## Message versioning Evolving message contracts requires care because producers and consumers may deploy independently. Adding a new `init` property with a default value is backward-compatible - existing consumers that do not know about the property ignore it during deserialization. Renaming or removing a required property is breaking and requires a coordinated deployment or a versioning strategy. When you need to refactor a message type's CLR namespace without changing its wire identity, use `AddMessage()` to pin the URN explicitly. This decouples the type's wire identity from its CLR location, so consumers continue to receive the message under the old URN even after you move or rename the class. ## Next steps Now that you understand message structure, learn the three messaging patterns. - [**Messaging Patterns**](https://chillicream.com/docs/mocha/messaging-patterns) \- Pub/sub events, point-to-point commands, and request/reply. > **Full demo:** [Demo.Contracts](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Contracts) contains a complete set of message contracts for an e-commerce system - events (`OrderPlacedEvent`, `PaymentCompletedEvent`), send messages (`ProcessRefundCommand`, `ReserveInventoryCommand`), and request/reply pairs used by sagas. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/messages.md) Maintained by ChilliCream. Last updated on **August 12, 2026** by **alisan3** --- # Messaging Patterns - Mocha > Learn the three core messaging patterns in Mocha - events (pub/sub), send (fire-and-forget), and request/reply - and when to use each one. Canonical source: https://chillicream.com/docs/mocha/messaging-patterns Every message you send answers one of three questions: "Who needs to know?" - an event, broadcast to anyone who cares. "Who should act?" - a command, directed at a single handler. "What is the result?" - a request that blocks until the handler replies. Choosing the wrong pattern is the most common messaging architecture mistake. This page explains each pattern, when to use it, and the anti-pattern to avoid. | Pattern | Bus method | Handler interface | Delivery | | ------------------- | ------------ | ----------------------------------------- | ------------------------------------------- | | **Event** (pub/sub) | PublishAsync | IEventHandler | One-to-many: all subscribers receive a copy | | **Request** (send) | SendAsync | IEventRequestHandler | One-to-one: a single handler processes it | | **Request/Reply** | RequestAsync | IEventRequestHandler | One-to-one: sender awaits a typed response | ## Events (pub/sub) Events represent something that happened. The publisher does not know or care who receives the event - that question is answered by whoever subscribes. Zero, one, or many handlers can react to the same event type. If no handler is registered, the event is silently discarded. This implements the [Publish-Subscribe Channel](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PublishSubscribeChannel.html) pattern. **Naming convention:** Name events using noun-verb past tense. `OrderPlaced`, `PaymentCompleted`, `UserRegistered`. The past tense signals that something already happened - the publisher is not directing anyone to act. ### Publish an event and handle it By the end of this section, you will have two independent handlers both processing the same published event. #### Define the event C# ``` namespace MyApp.Messages; public sealed record OrderPlacedEvent { public required Guid OrderId { get; init; } public required string CustomerId { get; init; } public required decimal TotalAmount { get; init; } public required DateTimeOffset CreatedAt { get; init; } } ``` #### Implement two handlers C# ``` using Mocha; namespace MyApp.Handlers; // Handler 1: Create an invoice when an order is placed public class BillingHandler(ILogger logger) : IEventHandler { public async ValueTask HandleAsync( OrderPlacedEvent message, CancellationToken cancellationToken) { logger.LogInformation( "Creating invoice for order {OrderId}, amount {Amount}", message.OrderId, message.TotalAmount); // Create invoice logic here await Task.CompletedTask; } } // Handler 2: Send a notification when an order is placed public class NotificationHandler(ILogger logger) : IEventHandler { public async ValueTask HandleAsync( OrderPlacedEvent message, CancellationToken cancellationToken) { logger.LogInformation( "Sending confirmation to customer {CustomerId} for order {OrderId}", message.CustomerId, message.OrderId); // Send email/SMS logic here await Task.CompletedTask; } } ``` Each handler implements `IEventHandler`. Both receive the same event independently. If one handler fails, the other still processes its copy. #### Register and publish C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEventHandler() .AddInMemory(); ``` From an endpoint, publish the event through the injected bus: C# ``` app.MapPost("/orders", async (IMessageBus bus) => { await bus.PublishAsync(new OrderPlacedEvent { OrderId = Guid.NewGuid(), CustomerId = "customer-42", TotalAmount = 149.99m, CreatedAt = DateTimeOffset.UtcNow }, CancellationToken.None); return Results.Ok(); }); ``` Expected output: ``` info: MyApp.Handlers.BillingHandler[0] Creating invoice for order 3f2504e0-4f89-11d3-9a0c-0305e82c3301, amount 149.99 info: MyApp.Handlers.NotificationHandler[0] Sending confirmation to customer customer-42 for order 3f2504e0-4f89-11d3-9a0c-0305e82c3301 ``` Both handlers execute. The order of execution is not guaranteed. ### How to chain events across services A common pattern is for a handler to publish a new event after completing its work. This creates an event chain that coordinates multiple services without coupling them. C# ``` public class OrderPlacedEventHandler( BillingDbContext db, IMessageBus messageBus, ILogger logger) : IEventHandler { public async ValueTask HandleAsync( OrderPlacedEvent message, CancellationToken cancellationToken) { logger.LogInformation( "Order placed: {OrderId}, creating invoice", message.OrderId); // Create and process payment... // Publish a downstream event await messageBus.PublishAsync( new PaymentCompletedEvent { PaymentId = Guid.NewGuid(), OrderId = message.OrderId, Amount = message.TotalAmount, PaymentMethod = "CreditCard", ProcessedAt = DateTimeOffset.UtcNow }, cancellationToken); } } ``` The billing service handles `OrderPlacedEvent` and publishes `PaymentCompletedEvent`. A shipping service can subscribe to `PaymentCompletedEvent` without knowing about billing. Each service reacts to events it cares about. ## Send (fire-and-forget) A send represents an instruction directed at a specific handler. Unlike events, a send has exactly one handler. The sender knows what it wants done but does not wait for a typed response - the message either succeeds or faults. This implements the [Command Message](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CommandMessage.html) pattern. **Naming convention:** Name send messages using verb-noun present tense. `ReserveInventory`, `ProcessPayment`, `ScheduleShipment`. The imperative form signals intent - you are telling a specific service what to do. Use send for fire-and-forget operations: reserving inventory, scheduling a job, triggering a side effect in another service. Warning **Send in disguise.** If your "event" expects exactly one handler to take action, it should be a send. Use `SendAsync`, not `PublishAsync`. Publishing a message that requires a single specific handler breaks the semantic contract of events and makes the system harder to reason about. ### Send a message and handle it By the end of this section, you will send a message to a single handler and verify it executes. #### Define the message C# ``` namespace MyApp.Messages; public sealed record ReserveInventoryCommand { public required Guid OrderId { get; init; } public required Guid ProductId { get; init; } public required int Quantity { get; init; } } ``` #### Implement the handler C# ``` using Mocha; namespace MyApp.Handlers; public class ReserveInventoryCommandHandler( AppDbContext db, ILogger logger) : IEventRequestHandler { public async ValueTask HandleAsync( ReserveInventoryCommand request, CancellationToken cancellationToken) { logger.LogInformation( "Reserving {Quantity} units of product {ProductId} for order {OrderId}", request.Quantity, request.ProductId, request.OrderId); var product = await db.Products .FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken); if (product is null) { throw new InvalidOperationException( $"Product {request.ProductId} not found"); } product.StockQuantity -= request.Quantity; await db.SaveChangesAsync(cancellationToken); logger.LogInformation( "Reserved {Quantity} units. Remaining stock: {Remaining}", request.Quantity, product.StockQuantity); } } ``` `IEventRequestHandler` (single type parameter) handles the command without returning a value. If the handler throws, the bus generates a fault. #### Register and send C# ``` builder.Services .AddMessageBus() .AddRequestHandler() .AddInMemory(); ``` Send the command: C# ``` await bus.SendAsync(new ReserveInventoryCommand { OrderId = Guid.NewGuid(), ProductId = productId, Quantity = 3 }, cancellationToken); ``` Expected output: ``` info: MyApp.Handlers.ReserveInventoryCommandHandler[0] Reserving 3 units of product a1b2c3d4-... for order e5f6a7b8-... info: MyApp.Handlers.ReserveInventoryCommandHandler[0] Reserved 3 units. Remaining stock: 97 ``` `SendAsync` completes after the message is dispatched to the transport. It does not wait for the handler to finish processing. ### How to wait for command acknowledgment When you need confirmation that the handler processed the command, use `RequestAsync` instead of `SendAsync`. Mocha sends an automatic `AcknowledgedEvent` when the handler completes. C# ``` // Fire-and-forget: returns after dispatch await bus.SendAsync(command, cancellationToken); // Wait for acknowledgment: returns after the handler completes await bus.RequestAsync(command, cancellationToken); ``` The `RequestAsync` overload that accepts `object` (no `IEventRequest` constraint) waits for the handler's acknowledgment without expecting a typed response. If the handler throws, `RequestAsync` throws a `ResponseTimeoutException`. ## Request/Reply Request/reply is for when the sender needs a typed response. The sender dispatches a request, Mocha routes it to a handler, the handler returns a response, and Mocha delivers the response back to the sender. The entire round trip completes within a single `await`. This implements the [Request-Reply](https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html) pattern. When you call `RequestAsync`, Mocha creates a temporary reply address and embeds it in the request envelope. The handler reads the reply address from the envelope and sends the response back to it. A correlation ID links the request and response across the transport. This is what makes `ResponseTimeoutException` possible - if the reply never arrives at the temporary address, the timeout fires. Note The reply address is generated and managed internally by Mocha for request/reply correlation - it is not a receive endpoint you configure. To scope your own receive endpoint's infrastructure to the lifetime of the process, call `Temporary()` on a queue or endpoint descriptor. See [Temporary endpoints and per-instance identity](https://chillicream.com/docs/mocha/routing-and-endpoints#temporary-endpoints-and-per-instance-identity). ### Send a request and await a response By the end of this section, you will send a request to a handler and use the typed response. #### Define the request and response The request record must implement `IEventRequest`. This marker interface tells Mocha the expected response type and enables compile-time correlation. C# ``` using Mocha; namespace MyApp.Messages; public sealed record ProcessRefundCommand : IEventRequest { public required Guid OrderId { get; init; } public required decimal Amount { get; init; } public required string Reason { get; init; } public required string CustomerId { get; init; } } public sealed record ProcessRefundResponse { public required Guid RefundId { get; init; } public required Guid OrderId { get; init; } public required decimal Amount { get; init; } public required bool Success { get; init; } public string? FailureReason { get; init; } public required DateTimeOffset ProcessedAt { get; init; } } ``` #### Implement the handler C# ``` using Mocha; namespace MyApp.Handlers; public class ProcessRefundCommandHandler( BillingDbContext db, ILogger logger) : IEventRequestHandler { public async ValueTask HandleAsync( ProcessRefundCommand request, CancellationToken cancellationToken) { logger.LogInformation( "Processing refund of {Amount} for order {OrderId}", request.Amount, request.OrderId); // Process refund logic... return new ProcessRefundResponse { RefundId = Guid.NewGuid(), OrderId = request.OrderId, Amount = request.Amount, Success = true, ProcessedAt = DateTimeOffset.UtcNow }; } } ``` `IEventRequestHandler` requires `TRequest : IEventRequest`. The return value is sent back to the caller's reply endpoint. The return value must not be `null` \- if you return `null`, the consumer throws an `InvalidOperationException`. #### Register and request C# ``` builder.Services .AddMessageBus() .AddRequestHandler() .AddInMemory(); ``` Send the request and use the response: C# ``` var response = await bus.RequestAsync( new ProcessRefundCommand { OrderId = orderId, Amount = 49.99m, Reason = "Defective product", CustomerId = "customer-42" }, cancellationToken); Console.WriteLine($"Refund {response.RefundId}: {response.Amount:C}, success={response.Success}"); ``` Expected output: ``` info: MyApp.Handlers.ProcessRefundCommandHandler[0] Processing refund of 49.99 for order e5f6a7b8-... Refund d4c3b2a1-...: $49.99, success=True ``` ### How to handle timeouts If the handler does not respond within the configured timeout, `RequestAsync` throws a `ResponseTimeoutException`. You can catch this and handle it: C# ``` try { var response = await bus.RequestAsync( new ProcessRefundCommand { OrderId = orderId, Amount = 49.99m, Reason = "Defective product", CustomerId = "customer-42" }, cancellationToken); // Use response... } catch (ResponseTimeoutException ex) { logger.LogWarning( "Refund request timed out for order {OrderId}: {Message}", orderId, ex.Message); // Retry, fall back, or notify the user } ``` Common causes of timeouts: the handler service is not running, the handler registration is missing, or the handler is taking longer than the timeout window. ### How to use `IEventRequest` correctly The `IEventRequest` marker interface connects the request type to its response type at compile time. This enables two things: 1. **Type-safe `RequestAsync`.** The compiler infers `TResponse` from the request parameter, so `bus.RequestAsync(request)` returns `ValueTask` without you specifying the type. 2. **Handler constraint.** `IEventRequestHandler` requires `TRequest : IEventRequest`, preventing mismatched request/response pairings at compile time. C# ``` // The compiler infers TResponse = ProcessRefundResponse // because ProcessRefundCommand : IEventRequest var response = await bus.RequestAsync(refundCommand, cancellationToken); ``` If your request record does not implement `IEventRequest`, you cannot use the typed `RequestAsync` overload. ## When to use which pattern | Question | Event (PublishAsync) | Send (SendAsync) | Request/Reply (RequestAsync) | | ------------------------------------ | -------------------- | ----------------------- | ----------------------------- | | How many handlers? | Zero or more | Exactly one | Exactly one | | Does the sender need a response? | No | No | Yes | | Does the sender know who handles it? | No | Yes (by routing) | Yes (by routing) | | Does the sender wait for processing? | No | No | Yes | | Handler interface | IEventHandler | IEventRequestHandler | IEventRequestHandler | **Use events** when multiple parts of the system need to react to something that happened. The publisher does not care who listens or what they do with the event. Examples: order placed, payment completed, user signed up. **Use send** when you need to tell a specific service to do something but do not need a result back. The sender knows the operation should happen but does not need to wait for it. Examples: reserve inventory, send an email, schedule a cleanup job. **Use request/reply** when the sender needs data back or needs to know whether the operation succeeded with details. The call blocks until the response arrives. Examples: process a refund and get the refund ID, look up product details, validate an address. ## See also - [Microsoft Azure Architecture: Publisher-Subscriber Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber) \- When to use pub/sub and when not to, including considerations for idempotency and message ordering. - [CodeOpinion: Commands & Events - What's the difference?](https://codeopinion.com/commands-events-whats-the-difference/) \- A concise explanation of the semantic distinction between commands and events. - [EIP: Publish-Subscribe Channel](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PublishSubscribeChannel.html) \- The canonical definition of the pub/sub pattern. - [EIP: Command Message](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CommandMessage.html) \- Commands as a way to invoke behavior in another service via messaging. - [EIP: Request-Reply](https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html) \- Correlation IDs and reply queues explained. > **Runnable examples:** [EventPubSub](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/MessagingPatterns/EventPubSub), [SendFireAndForget](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/MessagingPatterns/SendFireAndForget), [RequestReply](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/MessagingPatterns/RequestReply) > > **Full demo:** The [Demo application](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo) uses all three patterns: [Demo.Catalog](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Catalog) publishes `OrderPlacedEvent` (pub/sub), [Demo.Billing](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Billing) handles `ProcessRefundCommand` (send), and sagas use `RequestAsync` for request/reply coordination. Ready to implement these patterns? See [Handlers and Consumers](https://chillicream.com/docs/mocha/handlers-and-consumers). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/messaging-patterns.md) Maintained by ChilliCream. Last updated on **August 23, 2026** by **PascalSenn** --- # Middleware and Pipelines - Mocha > Understand how Mocha's three middleware pipelines process messages, which pipeline to target, and how to write and order custom middleware. Canonical source: https://chillicream.com/docs/mocha/middleware-and-pipelines Mocha's pipeline implements the [Pipes and Filters](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PipesAndFilters.html) pattern. Every message flows through a chain of middleware before reaching your handler. Each filter in the chain can inspect, modify, short-circuit, or observe the message - then pass control to the next filter. If you have used middleware in [ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/), the mental model is the same. Middleware wraps middleware in nested layers, like an onion. Each layer runs code before calling the next layer, and optionally runs more code after it returns. Most of the time, the defaults work and you never configure middleware directly. This page is for when you need to add cross-cutting behavior - a database transaction per handler, tenant context from headers, custom rate limiting - or when you want to understand how the built-in reliability and observability features fit into the pipeline. ## How pipelines work ### The nesting model Within a single pipeline, each middleware wraps the next. Execution flows inward through each layer, then unwinds outward as each layer returns: ``` Message arrives → MiddlewareA.InvokeAsync → MiddlewareB.InvokeAsync → MiddlewareC.InvokeAsync → [handler or next pipeline] ← return (or exception propagates back here) ← return (or exception caught/rethrown here) ← return ``` A middleware that does not call `next(context)` short-circuits the pipeline - everything after it is skipped. The expiry middleware uses this to drop stale messages without invoking any downstream code. A middleware that wraps `next` in a try/catch can intercept exceptions from downstream - the fault middleware uses this to route failed messages to an error endpoint. This wrapping model is why registration order matters: the first middleware registered becomes the outermost layer. It runs first on the way in, and last on the way out. ### The three pipelines Mocha has three separate pipelines with different context types and different purposes: ``` PublishAsync / SendAsync / RequestAsync │ ▼ ┌─────────────────────────┐ │ Dispatch Pipeline │ │ Instrumentation │ │ Serialization │ │ → Transport sends │ └─────────────────────────┘ │ ▼ (wire / InMemory) │ ┌─────────────────────────┐ │ Receive Pipeline │ │ TransportCircuitBreaker │ │ ConcurrencyLimiter │ │ ReceiveInstrumentation │ │ DeadLetter │ │ Fault │ │ CircuitBreaker │ │ Expiry │ │ MessageTypeSelection │ │ Routing │ └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ Consumer Pipeline │ │ Instrumentation │ │ Transaction (optional) │ │ Inbox (optional) │ │ → Your Handler │ └─────────────────────────┘ ``` Each pipeline operates on a different context: - **Dispatch** (`IDispatchContext`) - operates on the unserialized message object. Has the CLR message, headers, and destination address. Serialization happens at the end. - **Receive** (`IReceiveContext`) - operates on the raw envelope from the transport. Has the serialized body, headers, and transport metadata. Message type resolution and routing happen here. - **Consumer** (`IConsumeContext`) - operates on the deserialized message inside a specific consumer. Has the typed message, envelope metadata, and scoped services. In a distributed system, the dispatch and receive pipelines run in different processes. With the InMemory transport, they run in the same process. ## Which pipeline should I target? | Use case | Pipeline | | -------------------------------------------------- | ------------------------------------ | | Add a header to every outgoing message | Dispatch | | Validate messages before sending | Dispatch | | Rate-limit incoming messages at the endpoint level | Receive | | Database transaction wrapping every handler | Consumer | | Time individual handler execution | Consumer | | Extract tenant context from message headers | Receive (before Routing) or Consumer | ## Consumer middleware Consumer middleware wraps your handler execution. It is the most common customization point - use it for cross-cutting concerns that apply to every handler: database transactions, validation, timing, or tenant context resolution. ### Database unit-of-work example A database transaction that commits on success and rolls back on failure is the canonical consumer middleware use case: C# ``` using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Mocha; using Mocha.Middlewares; internal sealed class UnitOfWorkConsumerMiddleware { public async ValueTask InvokeAsync( IConsumeContext context, ConsumerDelegate next) { // Resolve DbContext from the per-message DI scope var db = context.Services.GetRequiredService(); await using var tx = await db.Database.BeginTransactionAsync(); try { await next(context); // Run the handler await tx.CommitAsync(); // Commit on success } catch { await tx.RollbackAsync(); // Roll back on any handler exception throw; // Re-throw so fault middleware can route the message } } public static ConsumerMiddlewareConfiguration Create() => new( static (context, next) => { var middleware = new UnitOfWorkConsumerMiddleware(); return ctx => middleware.InvokeAsync(ctx, next); }, "UnitOfWork"); } ``` Register the middleware during bus configuration: C# ``` builder.Services .AddMessageBus(bus => { bus.UseConsume(UnitOfWorkConsumerMiddleware.Create()); }) .AddEventHandler() .AddInMemory(); ``` The re-throw after `RollbackAsync` is intentional. The fault middleware further up the receive pipeline catches handler exceptions and routes the message to an error endpoint. If you swallow the exception, the message is silently dropped. ## Receive middleware Receive middleware wraps the entire receive pipeline for a message, before message type resolution and routing. Use it for concerns that apply to raw envelopes: rate limiting, logging of all incoming messages, or metrics about transport-level behavior. C# ``` using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Mocha; using Mocha.Middlewares; internal sealed class LoggingReceiveMiddleware( ILogger logger) { public async ValueTask InvokeAsync( IReceiveContext context, ReceiveDelegate next) { logger.LogInformation( "Receiving {MessageId}", context.MessageId); await next(context); logger.LogInformation( "Finished {MessageId}", context.MessageId); } public static ReceiveMiddlewareConfiguration Create() => new( static (context, next) => { var logger = context.Services .GetRequiredService>(); var middleware = new LoggingReceiveMiddleware(logger); return ctx => middleware.InvokeAsync(ctx, next); }, "Logging"); } ``` Register after instrumentation so telemetry spans are already open when your middleware runs: C# ``` builder.Services .AddMessageBus(bus => { // Insert after ReceiveInstrumentation, before DeadLetter bus.UseReceive( LoggingReceiveMiddleware.Create(), after: "ReceiveInstrumentation"); }) .AddEventHandler() .AddInMemory(); ``` `UseReceive(..., after: "ReceiveInstrumentation")` places your middleware immediately after the named middleware. The receive pipeline then executes: TransportCircuitBreaker, ConcurrencyLimiter, ReceiveInstrumentation, **Logging**, DeadLetter, Fault, and so on. ## Dispatch middleware Dispatch middleware wraps the outbound path. Use it for concerns on every outgoing message: adding headers, enforcing message schemas, or enriching with correlation context. C# ``` internal sealed class TenantDispatchMiddleware { private readonly string _tenantId; public TenantDispatchMiddleware(string tenantId) { _tenantId = tenantId; } public async ValueTask InvokeAsync( IDispatchContext context, DispatchDelegate next) { // Stamp every outgoing message with the tenant identifier context.Headers.Set("x-tenant", _tenantId); await next(context); } public static DispatchMiddlewareConfiguration Create(string tenantId) => new( (context, next) => { var middleware = new TenantDispatchMiddleware(tenantId); return ctx => middleware.InvokeAsync(ctx, next); }, "TenantDispatch"); } ``` Register before all other dispatch middleware so every outgoing message carries the header: C# ``` builder.Services .AddMessageBus(bus => { bus.UseDispatch( TenantDispatchMiddleware.Create("acme"), before: "Instrumentation"); }) .AddEventHandler() .AddInMemory(); ``` ## The factory pattern and DI scoping The factory lambda in `ReceiveMiddlewareConfiguration`, `ConsumerMiddlewareConfiguration`, and `DispatchMiddlewareConfiguration` runs once at startup to build the pipeline delegate. Only the innermost delegate (your middleware's `InvokeAsync`) runs per message. Services resolved inside the innermost delegate come from the request-scoped DI container for that message. You do not want middleware instances created per message if possible - resolve services from `context.Services` inside `InvokeAsync` instead. Warning **Avoid capturing services outside the lambda.** If you resolve a service outside the factory lambda, it is shared across all messages and behaves as a singleton - even if it was registered as scoped. This breaks scoped services like `DbContext`: C# ``` // Wrong: DbContext captured as a singleton var db = serviceProvider.GetRequiredService(); return new ConsumerMiddlewareConfiguration( (context, next) => ctx => middleware.InvokeAsync(ctx, next, db), // db is shared! "UnitOfWork"); // Correct: resolve inside the per-message factory return new ConsumerMiddlewareConfiguration( static (context, next) => { var db = context.Services.GetRequiredService(); var middleware = new UnitOfWorkConsumerMiddleware(); return ctx => middleware.InvokeAsync(ctx, next); }, "UnitOfWork"); ``` ## Control middleware ordering Each pipeline type has a single registration method with optional `before` and `after` parameters: | Method | Pipeline | Behavior | | ---------------------------------- | -------- | ----------------------------------------------- | | UseReceive(config) | Receive | Append to the pipeline | | UseReceive(config, before: "key") | Receive | Insert before the middleware with the given key | | UseReceive(config, after: "key") | Receive | Insert after the middleware with the given key | | UseDispatch(config) | Dispatch | Append to the pipeline | | UseDispatch(config, before: "key") | Dispatch | Insert before the middleware with the given key | | UseDispatch(config, after: "key") | Dispatch | Insert after the middleware with the given key | | UseConsume(config) | Consumer | Append to the pipeline | | UseConsume(config, before: "key") | Consumer | Insert before the middleware with the given key | | UseConsume(config, after: "key") | Consumer | Insert after the middleware with the given key | Only one of `before` or `after` can be specified at the same time. If neither is specified, the middleware is appended after the built-in defaults. Middleware is compiled once at startup into a single delegate chain. Register all middleware during bus configuration, before the service provider is built. Middleware added after the bus starts has no effect. Middleware can also be registered at transport or endpoint scope. Bus-level middleware applies to all transports and endpoints. Transport-level middleware applies to all endpoints on that transport. Endpoint-level middleware applies to a single endpoint. The most specific scope wins. This is the same scope hierarchy described in [Routing and Endpoints](https://chillicream.com/docs/mocha/routing-and-endpoints). ## Built-in middleware and feature pages The built-in middleware in the receive pipeline implements the reliability and observability features described on their own pages: - The `Inbox` middleware deduplicates incoming messages based on `MessageId`, described in [Reliability](https://chillicream.com/docs/mocha/reliability#deduplicate-messages-with-the-transactional-inbox). It runs in the **consumer pipeline** after the transaction middleware so that the inbox claim participates in the same database transaction as the handler's business data. Use `UseConsume(config, before: "Inbox")` or `UseConsume(config, after: "Inbox")` to position your middleware relative to it. - The `CircuitBreaker` and `DeadLetter` middleware implement the circuit breaker and dead-letter behaviors described in [Reliability](https://chillicream.com/docs/mocha/reliability). Use `UseReceive(config, before: "key")` or `UseReceive(config, after: "key")` with their keys to position your middleware relative to them. - The `ReceiveInstrumentation` middleware generates the OpenTelemetry spans and metrics described in [Observability](https://chillicream.com/docs/mocha/observability). Place logging or correlation middleware after `ReceiveInstrumentation` using `UseReceive(config, after: "ReceiveInstrumentation")`. ## Next steps The pipeline handles failures automatically. Learn how circuit breaking, dead-letter routing, the transactional outbox, and the idempotent inbox work in [Reliability](https://chillicream.com/docs/mocha/reliability). > **Runnable examples:** [CustomMiddleware](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Middleware/CustomMiddleware), [UnitOfWork](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Middleware/UnitOfWork) [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/middleware-and-pipelines.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Observability - Mocha > Instrument Mocha with OpenTelemetry for distributed tracing, metrics, and logging across dispatch, receive, and consumer pipelines. Canonical source: https://chillicream.com/docs/mocha/observability Mocha integrates with OpenTelemetry to give you distributed traces, metrics, and structured logging across every stage of the messaging pipeline. When a message is dispatched, received, or consumed, Mocha emits spans and metrics that let you trace message flows end-to-end across services, measure throughput, and diagnose failures. Observability is opt-in. Call `.AddInstrumentation()` on the bus builder to activate the built-in `OpenTelemetryDiagnosticObserver`. Without it, Mocha uses a no-op observer that adds zero overhead. ## Enable tracing and metrics ### Register instrumentation C# ``` using Mocha; var builder = WebApplication.CreateBuilder(args); builder.Services .AddMessageBus() .AddInstrumentation() // Enables OpenTelemetry traces and metrics .AddEventHandler() .AddRabbitMQ(); ``` `.AddInstrumentation()` registers the `OpenTelemetryDiagnosticObserver` as a singleton `IBusDiagnosticObserver`. This observer creates `Activity` spans for dispatch, receive, and consume operations, and records metrics via the `Mocha` meter. ### Subscribe to the Mocha activity source The .NET OpenTelemetry SDK only collects spans from activity sources you explicitly subscribe to. Add the `"Mocha"` source to your tracing configuration: C# ``` builder.Services .AddOpenTelemetry() .WithTracing(tracing => { tracing .AddSource("Mocha") // Subscribe to Mocha spans .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation(); }) .WithMetrics(metrics => { metrics .AddMeter("Mocha") // Subscribe to Mocha metrics .AddAspNetCoreInstrumentation(); }); ``` Note Mocha follows the [OpenTelemetry messaging semantic conventions](https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/), which are currently in development status. Attribute names may change in future OTel releases. ## What you will see After publishing a message, open your tracing backend (Aspire Dashboard, Jaeger, or an OTLP collector). For each message flow you should see three linked spans: 1. **`publish {destination}`** \- Created by the dispatch pipeline when the message is sent. This span belongs to the publishing service. 2. **`receive {endpoint}`** \- Created by the receive pipeline when the message arrives at an endpoint. This span belongs to the consuming service and links back to the publish span. 3. **`consumer {handler}`** \- Created as a child of the receive span when your handler processes the message. In the Aspire Dashboard, you will see the `publish` span from the publishing service linked to the `receive` span in the consumer service, with a child `consumer` span for the handler execution. The three spans appear as a single distributed trace that crosses service boundaries. ## How trace context propagates When the dispatch instrumentation middleware runs, it writes the current `Activity`'s trace context into the outgoing message headers using the [W3C Trace Context](https://www.w3.org/TR/trace-context/) standard (`traceparent` and `tracestate`). The receive instrumentation middleware on the other side reads those headers and restores the parent context, linking the two spans into a single trace. This is the same propagation format used by ASP.NET Core, HttpClient, and the broader OpenTelemetry ecosystem, so Mocha traces connect seamlessly with spans from other frameworks. Tracing and metrics are injected by middleware in all three pipelines: `DispatchInstrumentation`, `ReceiveInstrumentation`, and `ConsumerInstrumentation`. See [Middleware and Pipelines](https://chillicream.com/docs/mocha/middleware-and-pipelines) for how these fit into the pipeline architecture and how to reorder or replace them. ## How to implement a custom diagnostic observer To collect custom telemetry or integrate with a non-OpenTelemetry backend, implement `IBusDiagnosticObserver`. Each method is called at the start of its pipeline stage and returns an `IDisposable` that the pipeline disposes when the operation completes. This pattern lets you measure duration, track in-flight operations, and clean up resources. C# ``` using Mocha; using Mocha.Middlewares; public sealed class CustomDiagnosticObserver : IBusDiagnosticObserver { public IDisposable Dispatch(IDispatchContext context) { var startTime = DateTimeOffset.UtcNow; Console.WriteLine($"Dispatching to {context.DestinationAddress}"); return new Scope(() => { var duration = DateTimeOffset.UtcNow - startTime; Console.WriteLine($"Dispatch completed in {duration.TotalMilliseconds}ms"); }); } public IDisposable Receive(IReceiveContext context) { Console.WriteLine($"Receiving from {context.Endpoint.Address}"); return new Scope(() => Console.WriteLine("Receive completed")); } public IDisposable Consume(IConsumeContext context) { Console.WriteLine($"Consumer processing message {context.MessageId}"); return new Scope(() => Console.WriteLine("Consumer completed")); } public void OnDispatchError(IDispatchContext context, Exception exception) => Console.WriteLine($"Dispatch error: {exception.Message}"); public void OnReceiveError(IReceiveContext context, Exception exception) => Console.WriteLine($"Receive error: {exception.Message}"); public void OnConsumeError(IConsumeContext context, Exception exception) => Console.WriteLine($"Consume error: {exception.Message}"); private sealed class Scope(Action onDispose) : IDisposable { public void Dispose() => onDispose(); } } ``` Register your observer instead of (or alongside) the built-in one: C# ``` builder.Services.AddSingleton(); ``` ## Configure with Nitro Nitro provides a managed telemetry backend for your Mocha services. Add the `ChilliCream.Nitro` and `ChilliCream.Nitro.OpenTelemetry` packages and configure the exporter: ``` dotnet add package ChilliCream.Nitro dotnet add package ChilliCream.Nitro.OpenTelemetry ``` C# ``` builder.Services .AddNitro() .AddOpenTelemetry(); builder.Services .AddOpenTelemetry() .WithTracing(tracing => { tracing.AddSource("Mocha"); }) .WithMetrics(metrics => { metrics.AddMeter("Mocha"); }); builder.Logging.AddOpenTelemetry(logging => { logging.IncludeFormattedMessage = true; logging.IncludeScopes = true; }); ``` Configure credentials through environment variables: | Variable | Purpose | | --------------- | -------------------------------------------- | | NITRO\_API\_KEY | Authentication key for the Nitro API | | NITRO\_API\_ID | Your Nitro API identifier | | NITRO\_STAGE | Deployment stage (e.g., production, staging) | Then register the bus with instrumentation: C# ``` builder.Services .AddMessageBus() .AddInstrumentation() .AddEventHandler() .AddRabbitMQ(); ``` ## Log-trace correlation When OpenTelemetry is configured with `AddOpenTelemetry().WithLogging()`, your `ILogger` structured log entries automatically include `TraceId` and `SpanId` fields. This means every log line written inside a handler or middleware is automatically correlated to the active span, making it possible to jump from a log entry in your logging backend directly to the corresponding trace. The Aspire configuration block above already includes `builder.Logging.AddOpenTelemetry(...)`, which enables this behavior. No additional code is needed in your handlers. For more detail on .NET distributed tracing concepts and how `Activity` maps to spans, see [.NET Distributed Tracing Concepts](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts). ## Next steps - [Middleware and Pipelines](https://chillicream.com/docs/mocha/middleware-and-pipelines) \- Understand how `DispatchInstrumentation`, `ReceiveInstrumentation`, and `ConsumerInstrumentation` fit into each pipeline. - [Reliability](https://chillicream.com/docs/mocha/reliability) \- Configure fault handling and circuit breakers that work alongside observability. - [Sagas](https://chillicream.com/docs/mocha/sagas) \- For long-running workflows that span multiple messages, see Sagas. > **Runnable example:** [OpenTelemetry](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Observability/OpenTelemetry) > > **Full demo:** [Demo.ServiceDefaults](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.ServiceDefaults) shows how to configure OpenTelemetry tracing and metrics for all services in a shared project, including the `"Mocha"` activity source. The [Demo.AppHost](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.AppHost) orchestrates everything with .NET Aspire for end-to-end observability. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/observability.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Quick Start - Mocha > Get started with Mocha in under five minutes. Install packages, register the message bus, define an event handler, publish your first event, and verify it works. Canonical source: https://chillicream.com/docs/mocha/quick-start By the end of this guide, you will have an ASP.NET Core app that publishes an `OrderPlaced` event and handles it - all running in-process with the InMemory transport. Here is what you are building: ## Create the project Bash ``` dotnet new web -n MochaQuickStart cd MochaQuickStart ``` ## Install the packages You need two packages: the core bus and the InMemory transport. Bash ``` dotnet add package Mocha dotnet add package Mocha.Transport.InMemory ``` The InMemory transport keeps everything in-process - no broker to install, no infrastructure to configure. It is the fastest way to get started. ## Define a message A message is a plain C# record. Create a file called `OrderPlaced.cs`: C#OrderPlaced.cs ``` namespace MochaQuickStart; public sealed record OrderPlaced( Guid OrderId, string ProductName, decimal Amount); ``` No base class, no marker interface. Any record or class works as a message. ## Create a handler A handler is a class that implements `IEventHandler`. Create a file called `OrderPlacedHandler.cs`: C# ``` using Mocha; namespace MochaQuickStart; public class OrderPlacedHandler(ILogger logger) : IEventHandler { public ValueTask HandleAsync( OrderPlaced message, CancellationToken cancellationToken) { logger.LogInformation( "Order received: {OrderId} - {ProductName} for {Amount:C}", message.OrderId, message.ProductName, message.Amount); return ValueTask.CompletedTask; } } ``` The bus calls `HandleAsync` every time an `OrderPlaced` event is published. The handler receives the deserialized message and a cancellation token. ## Register the bus Open `Program.cs` and replace its contents: C#Program.cs ``` using Mocha; using Mocha.Transport.InMemory; using MochaQuickStart; var builder = WebApplication.CreateBuilder(args); // Register the message bus, handlers, and transport builder.Services .AddMessageBus() .AddMochaQuickStart() // source-generated - discovers all handlers in this assembly .AddInMemory(); var app = builder.Build(); // Endpoint that publishes an event app.MapPost("/orders", async (IMessageBus bus) => { var orderPlaced = new OrderPlaced( OrderId: Guid.NewGuid(), ProductName: "Mechanical Keyboard", Amount: 149.99m); await bus.PublishAsync(orderPlaced, CancellationToken.None); return Results.Ok(new { orderPlaced.OrderId, Status = "Published" }); }); app.Run(); ``` Each registration line has a single responsibility: - `AddMessageBus()` \- registers the bus runtime and core services into DI. - `AddMochaQuickStart()` \- source-generated method that discovers and registers all handlers in this assembly. Named after the project - to customize the name, see [Handler Registration](https://chillicream.com/docs/mocha/handler-registration). - `AddInMemory()` \- adds the InMemory transport; messages stay in-process. ## Publish and verify Run the app: Bash ``` dotnet run ``` Check your console output for the actual URL. ASP.NET Core's default port may differ depending on your SDK version and launch settings. Then, in another terminal, send a POST request using that URL: Bash ``` curl -X POST http://localhost:5000/orders ``` You should see JSON from curl: JSON ``` { "orderId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "Published" } ``` And in the application console, the handler's log message appears: ``` info: MochaQuickStart.OrderPlacedHandler[0] Order received: a1b2c3d4-e5f6-7890-abcd-ef1234567890 - Mechanical Keyboard for $149.99 ``` If you see that log line, it worked. ### What happened? Your POST request hit the `/orders` endpoint, which called `PublishAsync` on `IMessageBus`. The bus serialized the `OrderPlaced` record and handed it to the InMemory transport. The transport delivered it to the registered receive endpoint, which ran the message through the pipeline and invoked `HandleAsync` on your `OrderPlacedHandler`. The log line you see is proof the full path executed: publisher to bus to transport to handler. ## Next steps You have a working message bus. Here is where to go next: - **Understand messages:** [Messages](https://chillicream.com/docs/mocha/messages) \- learn what a message is, how the envelope wraps it, and naming conventions for events and commands. - **Learn the three patterns:** [Messaging Patterns](https://chillicream.com/docs/mocha/messaging-patterns) \- understand when to use pub/sub events, commands, and request/reply. - **Move to production:** [Transports](https://chillicream.com/docs/mocha/transports) \- switch from InMemory to RabbitMQ for real workloads. Now that you have a working app, learn how messages work in [Messages](https://chillicream.com/docs/mocha/messages). > **Runnable example:** [Examples/QuickStart](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/QuickStart) > > **Full demo:** The [Demo application](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo) shows a complete e-commerce system with Catalog, Billing, and Shipping services communicating through Mocha and orchestrated with .NET Aspire. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/quick-start.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Reliability - Mocha > Configure retries, fault handling, dead-letter routing, the transactional outbox, and the idempotent inbox to build resilient Mocha pipelines. Canonical source: https://chillicream.com/docs/mocha/reliability Messaging systems fail. Handlers throw exceptions, brokers go offline, databases lock up, and messages arrive faster than consumers can process them. Mocha's reliability features handle these failures at the infrastructure level so your handler code stays focused on business logic. C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { policy.On().DeadLetter(); policy.Default().Retry().ThenRedeliver(); }) .AddCircuitBreaker(opts => { opts.FailureRatio = 0.5; opts.BreakDuration = TimeSpan.FromSeconds(30); }) .AddConcurrencyLimiter(opts => opts.MaxConcurrency = 10) .AddEventHandler() .AddEntityFramework(p => { p.UsePostgresOutbox(); p.UseTransaction(); p.UsePostgresInbox(); }) .AddRabbitMQ(); ``` That configuration adds per-exception retry and redelivery policies, circuit breaking, concurrency limiting, transactional outbox, idempotent inbox, and database transaction wrapping - all as middleware in the receive and dispatch pipelines. ## Delivery guarantees The outbox and inbox change what delivery guarantee your system provides: | Configuration | Guarantee | What it means | | ------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Without outbox | At-most-once | A message may be lost if the broker or handler crashes after receipt but before processing completes. | | With outbox | At-least-once | Every message is persisted before dispatch. Your handlers may be invoked more than once if a crash occurs between dispatch and acknowledgment. | | With outbox + inbox | Effectively exactly-once | The outbox guarantees every message is delivered. The inbox deduplicates on the receiving side, so each message is processed exactly once. | At-least-once delivery is the right default for most production systems. Adding the inbox on the consumer side upgrades the guarantee to effectively exactly-once processing - the outbox ensures delivery and the inbox ensures your handler runs only once per message. If you use the outbox without the inbox, design handlers to be idempotent. ## The receive pipeline and failure flow Mocha processes every inbound message through a compiled receive pipeline. The default middleware runs in this order: ``` TransportCircuitBreaker -> ConcurrencyLimiter -> Instrumentation -> DeadLetter -> Fault -> Redelivery <- schedules for later if retries exhausted -> CircuitBreaker -> Expiry -> MessageTypeSelection -> Routing -> Consumer pipeline -> Retry <- immediate in-process retries -> Transaction middleware (BEGIN) -> Inbox (claim inside transaction) -> Your handler -> Transaction middleware (COMMIT/ROLLBACK) ``` Each middleware can intercept failures from downstream, transform them, or short-circuit the pipeline. The reliability middlewares - dead-letter, fault, circuit breaker, expiry, inbox, and concurrency limiter - are all enabled by default with sensible defaults. You tune them when the defaults do not match your workload. ## Handle faults When your handler throws an exception, Mocha's fault middleware catches it and takes one of two actions depending on the messaging pattern: **Request/reply flows:** The fault middleware sends a `NotAcknowledgedEvent` back to the caller's response address. This gives the requester an explicit failure signal instead of a timeout. **Pub/sub and send flows:** The fault middleware forwards the original message envelope to the endpoint's error endpoint with fault metadata in the headers. The headers include the exception type, message, stack trace, and timestamp: | Header key | Value | | -------------------- | ----------------------------------- | | fault-exception-type | Fully qualified exception type name | | fault-message | Exception message string | | fault-stack-trace | Stack trace of the exception | | fault-timestamp | ISO 8601 timestamp of the fault | The fault middleware marks the message as consumed after handling, which prevents the dead-letter middleware from re-forwarding it. The fault handler is implemented as the `Fault` middleware in the receive pipeline. See [Middleware and Pipelines](https://chillicream.com/docs/mocha/middleware-and-pipelines) for positioning and customization. ### Verify fault behavior Throw an exception in a handler and inspect the error queue. With RabbitMQ, the error endpoint writes faulted messages to an `_error` queue by convention. C# ``` public class OrderPlacedHandler : IEventHandler { public ValueTask HandleAsync( OrderPlacedEvent message, CancellationToken cancellationToken) { throw new InvalidOperationException("Simulated failure"); } } ``` Publish an `OrderPlacedEvent` and check your RabbitMQ management console. The message appears in the error queue with fault headers attached. ## Route unhandled messages to the dead-letter endpoint The dead-letter middleware is the pipeline's safety net. It runs early in the pipeline (before the fault middleware) and catches any message that reaches the end of the pipeline without being consumed. This covers scenarios the fault middleware does not: messages with no matching consumer, messages that fail during routing, or messages that fall through all middleware without being handled. When a message is not consumed, the dead-letter middleware forwards the original envelope to the endpoint's error endpoint, preserving all headers and payload for later inspection. ``` info: Mocha.Middlewares.ReceiveDeadLetterMiddleware[0] An exception occurred while processing the message. The message will be moved to the error endpoint. ``` The dead-letter middleware logs at `Critical` level when it catches an exception, then forwards the message. If no error endpoint is configured for the receive endpoint, the dead-letter middleware is not activated. See [Dead Letter Channel](https://www.enterpriseintegrationpatterns.com/patterns/messaging/DeadLetterChannel.html) for the canonical description of this pattern. ## Expire stale messages Messages can carry a `DeliverBy` timestamp that marks when they become stale. The expiry middleware checks this timestamp before any deserialization or handler work runs. If the current time exceeds `DeliverBy`, the message is silently dropped and marked as consumed - no exception, no dead-letter, no handler invocation. ### Set message expiry on publish C# ``` await bus.PublishAsync( new PriceQuoteEvent { Ticker = "MSFT", Price = 425.30m }, new PublishOptions { ExpirationTime = DateTimeOffset.UtcNow.AddMinutes(5) }, cancellationToken); ``` The `ExpirationTime` on `PublishOptions` maps to the `DeliverBy` envelope field. When the message sits in a queue longer than five minutes, the expiry middleware drops it on arrival. ### Set message expiry on send C# ``` await bus.SendAsync( new ProcessPaymentCommand { OrderId = orderId, Amount = 99.99m }, new SendOptions { ExpirationTime = DateTimeOffset.UtcNow.AddSeconds(30) }, cancellationToken); ``` For time-sensitive commands, a short expiry prevents stale operations from executing after their validity window. ## Retry failed messages When a handler throws a transient exception - a database timeout, an HTTP 503, temporary lock contention - retrying the same operation a few hundred milliseconds later often succeeds. The retry middleware re-runs the handler in-process without releasing the concurrency slot or leaving the consumer pipeline. Retry lives in the **consumer pipeline**, not the receive pipeline. This matters for two reasons: 1. **Only handler failures are retried.** Deserialization errors, unknown message types, and routing failures are almost always permanent. Retrying them wastes time. 2. **Multi-consumer correctness.** When one endpoint routes a message to multiple consumers, only the failing consumer retries. The others are unaffected. ### Add retry with defaults C# ``` builder.Services .AddMessageBus() .AddResilience() .AddEventHandler() .AddRabbitMQ(); ``` The parameterless `AddResilience()` registers a catch-all `Default()` rule with both retry and redelivery enabled. Retry defaults to 3 attempts, 200 ms base delay, exponential backoff, jitter enabled, and a 30-second maximum delay. These defaults handle the majority of transient failures without tuning. ### Customize retry behavior C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { policy.Default() .Retry(5, TimeSpan.FromSeconds(1), RetryBackoffType.Exponential) .ThenRedeliver(); }) .AddEventHandler() .AddRabbitMQ(); ``` ### Use explicit retry intervals When you need precise control over each delay, pass an array of intervals. The array length determines the number of retries. C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { policy.Default().Retry( [ TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(500), TimeSpan.FromSeconds(2) ]).ThenRedeliver(); }) .AddEventHandler() .AddRabbitMQ(); ``` ### Handle different exceptions differently Not every exception should be retried. Validation errors, authorization failures, and other permanent errors waste retry budget. Use `AddResilience` to define per-exception handling strategies. C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { // Never retry validation failures - route to error endpoint policy.On().DeadLetter(); // Never retry non-transient database errors policy.On(ex => !ex.IsTransient).DeadLetter(); // Transient database errors get more retries policy.On(ex => ex.IsTransient) .Retry(5, TimeSpan.FromMilliseconds(200)) .ThenRedeliver(); // Everything else: retry then redeliver policy.Default().Retry().ThenRedeliver(); }) .AddEventHandler() .AddRabbitMQ(); ``` Exception matching respects inheritance. `On()` also matches `NpgsqlException` and any other `DbException` subclass. When multiple rules match, the most specific type wins - the same precedence as C# `catch` blocks. For the full API including predicates, conditional policies, and escalation chains, see [Exception Policies](https://chillicream.com/docs/mocha/exception-policies). ### Override retry per consumer The bus-level exception policy applies to all consumers. Override it for specific consumers that need different behavior. See [Handlers and Consumers](https://chillicream.com/docs/mocha/handlers-and-consumers) for the full consumer configuration API. C# ``` builder.Services .AddMessageBus() .AddResilience() // bus-level: default retry + redelivery for all consumers .AddEventHandler(consumer => { // This consumer: 10 retries with longer delay, then redeliver consumer.AddResilience(policy => { policy.Default() .Retry(10, TimeSpan.FromSeconds(2)) .ThenRedeliver(); }); }) .AddEventHandler(consumer => { // This consumer: skip retry, redeliver only consumer.AddResilience(policy => { policy.Default().Redeliver(); }); }) .AddRabbitMQ(); ``` Consumer-level policies replace the bus-level policies entirely for that consumer - they are not merged. If you need a bus-level rule to also apply at the consumer level, include it in the consumer-level configuration. ### Retry defaults reference | Setting | Default value | | --------- | ------------- | | Attempts | 3 | | Delay | 200 ms | | Backoff | Exponential | | Jitter | Enabled | | Max delay | 30 seconds | #### RetryBackoffType values | Value | Formula | Example (Delay = 200 ms) | | ----------- | ------------------------ | ------------------------ | | Constant | Delay | 200 ms, 200 ms, 200 ms | | Linear | Delay \* attempt | 200 ms, 400 ms, 600 ms | | Exponential | Delay \* 2^(attempt - 1) | 200 ms, 400 ms, 800 ms | ## Redeliver failed messages Retry handles transient blips that resolve in milliseconds. Some failures take longer - a downstream service is deploying, a database is recovering from a failover, an external API is rate-limiting. For these, you need **redelivery**: reschedule the message for later delivery through the transport. Redelivery lives in the **receive pipeline**, not the consumer pipeline. This matters for two reasons: 1. **Single decision point.** When an endpoint has multiple consumers, you want one redelivery decision per message, not one per consumer. 2. **Releases the concurrency slot.** Retry holds the slot while waiting. Redelivery returns the message to the transport and frees the slot for other messages. Redelivery uses Mocha's [scheduling infrastructure](https://chillicream.com/docs/mocha/scheduling) to schedule the message for future delivery. The message re-enters the full receive pipeline when it arrives - fresh routing, fresh consumer invocations, fresh retry attempts. ``` Receive pipeline: Fault -> Redelivery -> CircuitBreaker -> ... (rest of pipeline) All retries exhausted -> exception propagates to Redelivery -> redelivery attempts remaining -> schedule for later delivery -> redelivery exhausted -> exception propagates to Fault -> error endpoint ``` ### Add redelivery with defaults Redelivery is included when you call `AddResilience()` without arguments or when you chain `.ThenRedeliver()` in an escalation chain. C# ``` builder.Services .AddMessageBus() .AddResilience() // Default() rule enables both retry and redelivery .AddEventHandler() .AddRabbitMQ(); ``` The default redelivery schedule is three attempts at 5 minutes, 15 minutes, and 30 minutes with jitter enabled and a 1-hour maximum delay. ### Use custom redelivery intervals C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { policy.Default().Retry().ThenRedeliver( [ TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(30) ]); }) .AddEventHandler() .AddRabbitMQ(); ``` The array length determines the number of redelivery attempts. Each element is the delay before that attempt. ### Use calculated redelivery delays Instead of explicit intervals, configure a number of attempts and a base delay: C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { policy.Default() .Retry() .ThenRedeliver(5, TimeSpan.FromMinutes(2)); }) .AddEventHandler() .AddRabbitMQ(); ``` ### Redeliver without retry To skip retry entirely and go straight to redelivery, use `.Redeliver()` directly: C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { policy.Default().Redeliver(); }) .AddEventHandler() .AddRabbitMQ(); ``` When `.Redeliver()` is called without `.Retry()` first, retry is disabled for that rule. The handler failure goes straight to redelivery scheduling. ### Override redelivery per endpoint Override exception policies at the transport level for more granular control. Transport-level policies replace the bus-level policies entirely for all endpoints on that transport. C# ``` builder.Services .AddMessageBus() .AddResilience() // bus-level default .AddRabbitMQ(transport => { transport.AddResilience(policy => { // Override for this transport: longer redelivery intervals policy.Default().Retry().ThenRedeliver( [ TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(30), TimeSpan.FromHours(1) ]); }); }); ``` Disable redelivery for a specific transport by configuring retry-only: C# ``` transport.AddResilience(policy => { policy.Default().Retry(); }); ``` ### Redelivery defaults reference | Setting | Default value | | --------- | --------------------- | | Intervals | 5 min, 15 min, 30 min | | Jitter | Enabled | | Max delay | 1 hour | ## Combine retry and redelivery The two tiers compose naturally through escalation chains. When both are configured, a message goes through immediate retry first. If all retries are exhausted, the exception propagates up from the consumer pipeline to the receive pipeline, where redelivery catches it and schedules the message for later delivery. On the next delivery round, the full retry cycle runs again. ``` Message arrives | +- Consumer pipeline: Retry | +- Attempt 1 -> handler throws -> retry | +- Attempt 2 -> handler throws -> retry | +- Attempt 3 -> handler throws -> retry | +- Attempt 4 -> handler throws -> retries exhausted, exception propagates | +- Receive pipeline: Redelivery | +- Schedule message for delivery in 5 minutes | +- ... 5 minutes later, message re-enters pipeline ... | +- Consumer pipeline: Retry | +- Attempt 1 -> handler throws -> retry | +- ... | +- Attempt 4 -> retries exhausted, exception propagates | +- Receive pipeline: Redelivery | +- Schedule message for delivery in 15 minutes | +- ... continues until redelivery attempts exhausted ... | +- Fault middleware -> error endpoint (dead letter) ``` The total number of handler invocations before a message reaches the error endpoint: ``` Total attempts = (retry attempts + 1) x (redelivery attempts + 1) ``` With the defaults (3 retries, 3 redeliveries): `(3 + 1) x (3 + 1) = 16` total handler invocations. C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { policy.Default() .Retry(5, TimeSpan.FromSeconds(1), RetryBackoffType.Exponential) .ThenRedeliver( [ TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30) ]); }) .AddEventHandler() .AddRabbitMQ(); ``` With this configuration: `(5 + 1) x (3 + 1) = 24` total handler invocations before dead-letter. Note Request/reply messages skip redelivery. Redelivery does not apply to request/reply messages. The caller is waiting synchronously for a response - scheduling the message for delivery minutes later would cause a timeout. When a request/reply handler fails after retry exhaustion, the exception propagates directly to the fault middleware, which sends a `NotAcknowledgedEvent` back to the caller. Immediate retry still applies to request/reply handlers. ## Inspect retry state in handlers Handlers can access the current retry state through the context features. This is useful for graceful degradation - trying a primary path on the first attempt and falling back to an alternative on subsequent retries. C# ``` public class PaymentConsumer(ILogger logger) : IConsumer { public async ValueTask ConsumeAsync(IConsumeContext context) { var state = context.Features.Get(); if (state is { ImmediateRetryCount: > 2 }) { logger.LogWarning( "Primary gateway failed after {Retries} retries, using fallback", state.ImmediateRetryCount); await ProcessViaFallbackGatewayAsync(context.Message); return; } if (state is { DelayedRetryCount: > 0 }) { logger.LogWarning( "Processing after {Redeliveries} redeliveries", state.DelayedRetryCount); } await ProcessPaymentAsync(context.Message); } } ``` `RetryFeature` is `null` when `AddResilience()` is not configured. When present, it exposes two properties: | Property | Type | Description | | ------------------- | ---- | ---------------------------------------------------------------------------------------- | | ImmediateRetryCount | int | Number of immediate retries so far in this delivery round. 0 on the initial attempt. | | DelayedRetryCount | int | Number of redelivery rounds completed. Read from the delayed-retry-count message header. | ### Retry and sagas [Sagas](https://chillicream.com/docs/mocha/sagas) are consumers, so retry and redelivery apply automatically - no special configuration needed. Each saga handler invocation runs inside a saga transaction. If the handler throws, the transaction is never committed and all state changes are discarded. On the next retry attempt, the saga state is loaded fresh from the store. This means retry is safe for sagas by default. You do not need to worry about partial state mutations leaking between retry attempts. Redelivery is also safe. When a redelivered message arrives, a new transaction starts and the saga state is loaded at whatever state the last successful commit left it. ## Troubleshoot retry and redelivery ### "My handler runs more times than expected" Check both retry and redelivery. With both configured, the total handler invocations are `(retry attempts + 1) x (redelivery attempts + 1)`. Three retries with three redeliveries means 16 total invocations, not 6\. Use the [formula above](#combine-retry-and-redelivery) to calculate the exact count. ### "Retry does not work for my consumer" `AddResilience()` must be called at the bus level (or on the specific consumer) to register the retry middleware in the consumer pipeline. Adding a consumer-level policy without a bus-level or consumer-level `AddResilience()` call has no effect - the middleware is not in the pipeline. ### "Redelivery fails at startup" Redelivery uses Mocha's [scheduling infrastructure](https://chillicream.com/docs/mocha/scheduling). If your transport does not support scheduling or the scheduling store is not configured, redelivery cannot schedule messages for later delivery. Check that your transport is configured with scheduling support. ### "Validation exceptions are being retried" Use `AddResilience` to route permanent failures directly to the error endpoint: C# ``` builder.Services .AddMessageBus() .AddResilience(policy => { policy.On().DeadLetter(); policy.Default().Retry().ThenRedeliver(); }) .AddEventHandler() .AddRabbitMQ(); ``` See [Exception Policies](https://chillicream.com/docs/mocha/exception-policies) for the full per-exception configuration API, including predicates, terminal actions, and escalation chains. ### "Request/reply messages are not redelivered" This is by design. Redelivery skips request/reply messages because the caller is waiting for a synchronous response. Immediate retry still applies. See the [note above](#combine-retry-and-redelivery) for details. ## Limit concurrency The concurrency limiter middleware restricts how many messages a receive endpoint processes in parallel. C# ``` builder.Services .AddMessageBus() .AddConcurrencyLimiter(opts => opts.MaxConcurrency = 10) .AddEventHandler() .AddRabbitMQ(); ``` The default `MaxConcurrency` is `Environment.ProcessorCount * 2`. Set it lower when your handlers access shared resources with limited capacity (database connection pools, external APIs with rate limits), or higher when handlers are I/O-bound and the resource can handle more parallel work. The concurrency limiter is enabled by default. To disable it for a specific scope, set `Enabled` to `false`: C# ``` builder.Services .AddMessageBus() .AddConcurrencyLimiter(opts => opts.Enabled = false) .AddEventHandler() .AddRabbitMQ(); ``` ### Configuration scoping Both the concurrency limiter and circuit breaker resolve configuration using scope precedence: **endpoint > transport > bus**. The most specific scope wins. Configure at the bus level to set a global default: C# ``` builder.Services .AddMessageBus() .AddConcurrencyLimiter(opts => opts.MaxConcurrency = 10) .AddRabbitMQ(); ``` Override at the transport or endpoint level for more granular control. Transport and endpoint descriptors accept the same `.AddConcurrencyLimiter()` extension: C# ``` builder.Services .AddMessageBus() .AddConcurrencyLimiter(opts => opts.MaxConcurrency = 20) // bus-level default .AddRabbitMQ(transport => { transport.AddConcurrencyLimiter(opts => opts.MaxConcurrency = 5); // override for RabbitMQ }); ``` ## Configure the circuit breaker The circuit breaker middleware stops processing messages when the failure rate exceeds a threshold. It uses [Polly](https://github.com/App-vNext/Polly) internally and follows the standard circuit breaker pattern: **closed** (normal), **open** (rejecting), **half-open** (testing recovery). The circuit breaker is implemented as the `CircuitBreaker` middleware in the receive pipeline. See [Middleware and Pipelines](https://chillicream.com/docs/mocha/middleware-and-pipelines) for positioning and customization. C# ``` builder.Services .AddMessageBus() .AddCircuitBreaker(opts => { opts.FailureRatio = 0.5; opts.MinimumThroughput = 10; opts.SamplingDuration = TimeSpan.FromSeconds(10); opts.BreakDuration = TimeSpan.FromSeconds(30); }) .AddEventHandler() .AddRabbitMQ(); ``` When the circuit opens, the middleware pauses message processing for `BreakDuration` before allowing a test message through. If the test succeeds, the circuit closes. If it fails, the circuit stays open for another break period. ### How the circuit breaker evaluates failures 1. During the `SamplingDuration` window, the breaker counts successes and failures. 2. After `MinimumThroughput` messages have been processed, it evaluates the `FailureRatio`. 3. If the ratio of failures to total messages exceeds `FailureRatio`, the circuit opens. 4. After `BreakDuration`, the circuit transitions to half-open and allows one message through. 5. If that message succeeds, the circuit closes. If it fails, the circuit reopens. ### Two circuit breaker scopes Mocha includes two separate circuit breaker middlewares in the default pipeline: **Receive-level circuit breaker** (`CircuitBreaker`): Runs inside the pipeline after the dead-letter and fault middlewares. It monitors handler-level failures. Configure it with `.AddCircuitBreaker()` on the host builder. **Transport-level circuit breaker** (`TransportCircuitBreaker`): Runs at the very start of the pipeline, before the concurrency limiter. It monitors transport-level connectivity failures with a lower default failure ratio (10%). Configure it through the transport's options. The transport breaker protects against cascading failures when the broker itself is degraded. The receive-level breaker protects against application-level handler failures. Both use Polly's `CircuitBreakerStrategyOptions` internally. ## Guarantee delivery with the transactional outbox The transactional outbox solves the dual-write problem: when your handler writes to a database and publishes a message, either operation can fail independently. - If the database commits but the publish fails, the event is lost - downstream consumers never see it. - If the publish succeeds but the database rolls back, the event describes state that never existed. The outbox writes messages to the same database transaction as your business data. A background processor picks up persisted messages and dispatches them to the transport, providing at-least-once delivery guarantees. See [Transactional Outbox](https://microservices.io/patterns/data/transactional-outbox.html) and [Guaranteed Delivery](https://www.enterpriseintegrationpatterns.com/patterns/messaging/GuaranteedMessaging.html) for the canonical pattern descriptions. ### Set up the Postgres outbox **1\. Add the NuGet packages.** Bash ``` dotnet add package Mocha.EntityFrameworkCore dotnet add package Mocha.EntityFrameworkCore.Postgres ``` **2\. Add the `OutboxMessage` entity to your DbContext.** C# ``` using Microsoft.EntityFrameworkCore; using Mocha.Outbox; public class AppDbContext : DbContext { public DbSet OutboxMessages => Set(); // Your existing DbSets public DbSet Orders => Set(); } ``` `OutboxMessage` has four columns: `Id` (Guid), `Envelope` (JsonDocument), `TimesSent` (int), and `CreatedAt` (DateTime). EF Core discovers the table name, schema, and column mappings from your model automatically. **3\. Register the outbox and transaction middleware.** C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEntityFramework(p => { p.UsePostgresOutbox(); p.UseTransaction(); }) .AddRabbitMQ(); ``` | Call | Purpose | | ------------------------------ | -------------------------------------------------------------------------------------------------- | | AddEntityFramework() | Registers your DbContext with the bus for persistence features. | | UsePostgresOutbox() | Registers the Postgres outbox processor, background worker, and IMessageOutbox. | | UseTransaction() | Wraps each consumer invocation in a database transaction (commit on success, rollback on failure). | **4\. Publish inside a transaction.** With the outbox enabled, calls to `PublishAsync`, `SendAsync`, and `ReplyAsync` inside a handler persist the message envelope to the outbox table within the same database transaction. The outbox dispatch middleware intercepts these calls and writes to `IMessageOutbox` instead of the transport. C# ``` public class OrderPlacedHandler(AppDbContext db, IMessageBus bus) : IEventHandler { public async ValueTask HandleAsync( OrderPlacedEvent message, CancellationToken cancellationToken) { var invoice = new Invoice { OrderId = message.OrderId, Amount = message.Amount }; db.Invoices.Add(invoice); // This write goes to the outbox table, not directly to the transport await bus.PublishAsync( new InvoiceCreatedEvent { InvoiceId = invoice.Id }, cancellationToken); await db.SaveChangesAsync(cancellationToken); // Transaction commits both the invoice AND the outbox message atomically } } ``` After the transaction commits, the outbox processor detects the new message (via EF Core interceptors that signal on save and transaction commit) and dispatches it to the transport. Note The outbox guarantees at-least-once delivery. Your handlers may be invoked more than once for the same message if the outbox dispatches successfully but the transport acknowledgment is lost before the message is deleted from the outbox table. You can handle this in two ways: design handlers to be idempotent manually (see the [Idempotent Consumer](https://microservices.io/patterns/communication-style/idempotent-consumer.html) pattern), or enable the [inbox](#deduplicate-messages-with-the-transactional-inbox) on the receiving side to let Mocha deduplicate automatically. ### Use execution strategy resilience For transient database failures such as connection drops or deadlocks, add execution strategy wrapping: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEntityFramework(p => { p.UsePostgresOutbox(); p.UseResilience(); // Wraps consumer execution with the EF Core execution strategy p.UseTransaction(); }) .AddRabbitMQ(); ``` `UseResilience()` wraps the consumer middleware pipeline in the DbContext's configured execution strategy, enabling automatic retries for transient database errors. ### How the outbox processor works The outbox processor is a background hosted service (`IHostedService`). When the EF Core interceptors detect a `SaveChanges` or transaction commit, they signal the processor through `IOutboxSignal`. The processor reads pending `OutboxMessage` rows, deserializes the envelope, and dispatches each message through the bus's dispatch pipeline. The `TimesSent` column tracks dispatch attempts. If dispatch fails, the processor retries on the next signal. Messages are deleted from the outbox table after successful dispatch. ### Skip the outbox for specific dispatches Some messages - like internal system events or replies that do not need durability - can bypass the outbox. The outbox middleware checks for an `OutboxMiddlewareFeature` on the dispatch context. Messages with `SkipOutbox = true` pass straight through to the transport. The outbox middleware also only intercepts messages of kind `Publish`, `Send`, `Reply`, or `Fault`. Other message kinds pass through without outbox persistence. ## Deduplicate messages with the transactional inbox The transactional outbox guarantees at-least-once delivery. The transactional inbox completes the picture: it provides exactly-once processing by deduplicating messages on the receiving side. When a transport redelivers a message - because of a broker retry, a network hiccup, or an outbox re-dispatch - the inbox detects the duplicate `MessageId` and silently skips it. Your handler never runs twice for the same message. Deduplication is scoped per consumer type: when a message is routed to multiple handlers, each handler independently claims and processes the message. The inbox uses a composite key of `(MessageId, ConsumerType)` so that handler A and handler B each process the same message exactly once, even though they share the same inbox table. The inbox middleware runs in the **consumer pipeline**, after the transaction middleware. This means the inbox claim INSERT participates in the same database transaction as the handler's business data. Both commit or rollback atomically: if the process crashes between the claim and the commit, the claim is rolled back and the message can be safely redelivered. Messages without a `MessageId` pass through the inbox without deduplication - there is no identifier to check against. See [Idempotent Consumer](https://microservices.io/patterns/communication-style/idempotent-consumer.html) for the canonical description of this pattern. ### Set up the Postgres inbox **1\. Add the NuGet packages.** Bash ``` dotnet add package Mocha.EntityFrameworkCore dotnet add package Mocha.EntityFrameworkCore.Postgres ``` These are the same packages used by the outbox. If you already have them installed for the outbox, skip this step. **2\. Add the `InboxMessage` entity to your DbContext.** C# ``` using Microsoft.EntityFrameworkCore; using Mocha.Inbox; public class AppDbContext : DbContext { public DbSet InboxMessages => Set(); // Your existing DbSets public DbSet Orders => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.AddPostgresInbox(); } } ``` `InboxMessage` has four columns: `MessageId` (string), `ConsumerType` (string, the handler type name), `MessageType` (string, nullable, for diagnostics), and `ProcessedAt` (DateTime, defaults to `NOW()`). The primary key is the composite `(MessageId, ConsumerType)`, enabling per-handler deduplication when a message is routed to multiple consumers. **3\. Register the inbox middleware.** C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEntityFramework(p => { p.UseTransaction(); p.UsePostgresInbox(); }) .AddRabbitMQ(); ``` | Call | Purpose | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | AddEntityFramework() | Registers your DbContext with the bus for persistence features. | | UsePostgresInbox() | Registers the Postgres inbox, background cleanup worker, and IMessageInbox. Inserts the inbox consumer middleware. | | UseTransaction() | Wraps each consumer invocation in a database transaction (commit on success, rollback on failure). | **4\. Combine outbox and inbox for full exactly-once processing.** When you use both together, the outbox guarantees delivery and the inbox guarantees deduplication: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEntityFramework(p => { p.UsePostgresOutbox(); p.UseTransaction(); p.UsePostgresInbox(); }) .AddRabbitMQ(); ``` With this configuration, the inbox record and your business data are committed in the same database transaction. If the transaction rolls back, the inbox entry is not persisted and the message can be reprocessed on the next delivery attempt. ### Configure inbox retention The inbox stores a record for every processed message. A background worker (`MessageBusInboxWorker`) periodically cleans up old records to prevent unbounded table growth. C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEntityFramework(p => { p.UseTransaction(); p.UsePostgresInbox(opts => { opts.RetentionPeriod = TimeSpan.FromDays(14); opts.CleanupInterval = TimeSpan.FromMinutes(30); }); }) .AddRabbitMQ(); ``` | Option | Default | Description | | --------------- | ------- | -------------------------------------------------------------------------------------------------------- | | RetentionPeriod | 7 days | How long processed message records are kept. Messages older than this are deleted by the cleanup worker. | | CleanupInterval | 1 hour | How often the background worker runs the cleanup sweep. | Set `RetentionPeriod` long enough to cover the maximum redelivery window of your transport. If your transport can redeliver messages up to 3 days after initial delivery, a 7-day retention period provides a comfortable safety margin. The cleanup worker deletes expired rows in batches to avoid long-running locks on the inbox table. ### Skip the inbox for specific messages Some messages do not need deduplication - internal system events, heartbeats, or messages from transports that guarantee exactly-once delivery natively. The inbox middleware checks for an `InboxMiddlewareFeature` on the consume context. Messages with `SkipInbox = true` pass straight through without an inbox lookup. Set the feature from a custom consumer middleware that runs before the inbox: C# ``` builder.Services .AddMessageBus(bus => { bus.UseConsume( new ConsumerMiddlewareConfiguration( static (_, next) => ctx => { var feature = ctx.Features.GetOrSet(); feature.SkipInbox = true; return next(ctx); }, "SkipInboxCheck"), before: "Inbox"); }) .AddEventHandler() .AddEntityFramework(p => { p.UseTransaction(); p.UsePostgresInbox(); }) .AddRabbitMQ(); ``` `UseConsume(..., before: "Inbox")` inserts your middleware immediately before the inbox middleware in the consumer pipeline. The `InboxMiddlewareFeature` is a pooled feature that resets automatically between messages. ### How the inbox cleanup worker works The inbox cleanup worker is a background hosted service (`IHostedService`). It runs in a continuous loop: wait for `CleanupInterval`, then delete all inbox records where `ProcessedAt` is older than `RetentionPeriod`. Deletions happen in batches to minimize lock contention. The worker logs at `Information` level when records are deleted and at `Error` level if cleanup fails. ## Next steps Your messaging pipeline now handles exceptions per-type with retry and redelivery policies, limits concurrency, breaks circuits on repeated failures, guarantees delivery through the outbox, and deduplicates messages through the inbox. To monitor your messaging system, see [Observability](https://chillicream.com/docs/mocha/observability). - [**Exception Policies**](https://chillicream.com/docs/mocha/exception-policies) \- Configure per-exception handling with composable retry, redelivery, and terminal actions. - [**Handlers and Consumers**](https://chillicream.com/docs/mocha/handlers-and-consumers) \- Configure per-consumer exception policy overrides and understand handler exception behavior. - [**Scheduling**](https://chillicream.com/docs/mocha/scheduling) \- Configure the scheduling infrastructure that redelivery uses for delayed message delivery. - [**Middleware and Pipelines**](https://chillicream.com/docs/mocha/middleware-and-pipelines) \- Write custom middleware, control pipeline ordering, and understand the three pipeline stages. - [**Sagas**](https://chillicream.com/docs/mocha/sagas) \- Coordinate multi-step workflows with state machine sagas that use compensation when steps fail. - [**Observability**](https://chillicream.com/docs/mocha/observability) \- Trace message flows across services and monitor pipeline health with OpenTelemetry. > **Runnable examples:** [OutboxInbox](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Reliability/OutboxInbox), [CircuitBreaker](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Reliability/CircuitBreaker) > > **Full demo:** All three Demo services ([Catalog](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Catalog), [Billing](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Billing), [Shipping](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Shipping)) use the PostgreSQL transactional outbox and inbox with `UseTransaction()` and `UseResilience()` for reliable, exactly-once message processing. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/reliability.md) Maintained by ChilliCream. Last updated on **June 30, 2026** by **Tobias Tengler** --- # Routing and Endpoints - Mocha > Understand how Mocha discovers endpoints, builds topology, and routes messages using naming conventions - and how to override any of it. Canonical source: https://chillicream.com/docs/mocha/routing-and-endpoints An endpoint is the combination of a transport address (a queue or exchange) and a pipeline that processes messages. Mocha distinguishes between receive endpoints (which consume) and dispatch endpoints (which produce). Every handler you register becomes a receive endpoint; every message you publish or send is dispatched through a dispatch endpoint. By default, Mocha creates endpoints automatically from your handler and message types using naming conventions. Most applications never touch routing configuration directly - but you can configure the topology yourself completely when the defaults don't fit. This page explains what those conventions do, how to verify they produce the topology you expect, and how to override them when the defaults don't fit. ## The endpoint model When you register handlers and pick a transport, Mocha wires up both sides of the messaging connection automatically: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddRequestHandler() .AddRabbitMQ(); ``` That registration produces: - A **receive endpoint** named `my-service.order-placed` (subscribe route, bound to `OrderPlacedHandler`) - A **receive endpoint** named `get-order-status` (request route, bound to `GetOrderStatusHandler`) - A **dispatch endpoint** for publishing `OrderPlacedEvent` - A **dispatch endpoint** for sending `GetOrderStatusRequest` - A **reply receive endpoint** for inbound responses - A **reply dispatch endpoint** for outbound responses - **Error endpoints** (`_error` suffix) for each receive endpoint - destination of the `Fault` middleware when a handler throws - **Skipped endpoints** (`_skipped` suffix) for each receive endpoint - destination of the `DeadLetter` middleware when no consumer matched the message All derived from your handler types and message types through naming conventions. This is the default behavior: you declare what you handle, and the framework derives the endpoints and routes from those declarations. You can configure everything manually when you need to override a convention. ## How routing works Mocha maintains two kinds of routes that work together to move messages between services. ### Inbound routes An inbound route connects a message type to a receive endpoint. When you register a handler with `.AddEventHandler()` or `.AddRequestHandler()`, Mocha creates an inbound route that tells the transport which messages to deliver to which consumer. | Handler interface | Route kind | Endpoint type | | ----------------------------------------- | ---------- | --------------------------------------------- | | IEventHandler, IBatchEventHandler | Subscribe | Queue bound to an exchange or topic (fan-out) | | IEventRequestHandler | Send | Dedicated queue (point-to-point) | | IEventRequestHandler | Request | Dedicated queue (point-to-point) | ### Outbound routes An outbound route connects a message type to a dispatch endpoint. When you call `bus.PublishAsync()` or `bus.SendAsync()`, Mocha looks up the outbound route for the message type and dispatches through the corresponding endpoint. **Routing priority:** Mocha resolves outbound routes in this order: 1. If an explicit route is registered with `AddMessage()`, use it. 2. Otherwise, derive the endpoint name from naming conventions. When a message goes somewhere unexpected, open the topology visualizer first - it shows you the complete routing picture at a glance. If you need to dig deeper, check for an explicit `AddMessage()` registration, then check what the conventions produce. ### Startup-time topology Mocha resolves all endpoints and builds broker topology when the bus starts, not when the first message is sent. If your exchange or queue configuration is invalid - a mis-spelled exchange name, an incompatible binding - you will know at startup. Topology errors surface immediately, not silently on the first `SendAsync` call. This design is reflected in the [Message Endpoint](https://www.enterpriseintegrationpatterns.com/patterns/messaging/MessageEndpoint.html) pattern: an endpoint bridges your application code to the messaging infrastructure, and that bridge is established at initialization time. ## Naming conventions Mocha derives endpoint names automatically from your handler and message types. PascalCase type names become kebab-case; common suffixes (`Handler`, `Consumer`, `Command`, `Event`, `Message`, `Query`, `Response`) are stripped. ### Set the service name For subscribe (pub/sub) endpoints, the naming convention prefixes the endpoint name with the service name: C# ``` builder.Services .AddMessageBus() .Host(h => h.ServiceName("order-service")) .AddEventHandler() .AddRabbitMQ(); ``` The receive endpoint is named `order-service.order-placed`. Without the `.Host()` call, the service name defaults to the `SERVICE_NAME` or `OTEL_SERVICE_NAME` environment variable, or falls back to the entry assembly name. > **Why the service prefix?** > > Events use fan-out delivery: a single published message is delivered to every subscribing service. For fan-out to work correctly with point-to-point queues, each subscribing service needs its own queue. Without a service-specific prefix, two services consuming the same event would share a single queue and compete for messages - each service would only process half the events. > > The service prefix is what makes each service's queue unique. See [Point-to-Point Channel](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PointToPointChannel.html) for the full explanation of why fan-out and point-to-point channels work this way. ### Receive endpoint naming For **subscribe** routes (event handlers), the endpoint name combines the service name with the handler name: | Handler type | Service name | Endpoint name | | ----------------------- | ------------ | -------------------------- | | OrderPlacedEventHandler | catalog | catalog.order-placed-event | | BillingHandler | billing | billing.billing | | OrderAuditConsumer | audit | audit.order-audit | The `Handler` and `Consumer` suffixes are stripped. The service name prefix ensures each service gets its own queue for the same event type. For **send** and **request** routes, the endpoint name comes from the message type directly, without a service prefix: | Message type | Endpoint name | | ----------------------- | ----------------- | | ReserveInventoryCommand | reserve-inventory | | ProcessRefundCommand | process-refund | | GetProductRequest | get-product | Send endpoints are shared across services: any service sending `ReserveInventoryCommand` dispatches to the same `reserve-inventory` queue. There is only one destination for a command - that's the point-to-point guarantee. ### Publish endpoint naming For publish (fan-out) endpoints, the name includes the message namespace in kebab-case: | Message type | Namespace | Endpoint name | | --------------------- | --------------------- | --------------------------------------- | | OrderPlacedEvent | Demo.Contracts.Events | demo.contracts.events.order-placed | | PaymentCompletedEvent | Demo.Contracts.Events | demo.contracts.events.payment-completed | ### Special endpoint names | Purpose | Name pattern | Example | | ------------- | ------------------- | ----------------------------------------- | | Error queue | {endpoint}\_error | catalog.order-placed-event\_error | | Skipped queue | {endpoint}\_skipped | catalog.order-placed-event\_skipped | | Reply queue | response-{guid:N} | response-3f2504e04f8911d39a0c0305e82c3301 | The two failure-side endpoints are populated by different middlewares: - **Error queue (`_error`)** receives messages whose handler threw an exception. The `Fault` middleware (`ReceiveFaultMiddleware`) catches the exception, attaches `fault-*` headers (exception type, message, stack trace, timestamp), and forwards the original envelope to the configured `ErrorEndpoint`. - **Skipped queue (`_skipped`)** receives messages that completed the pipeline without any consumer marking them as consumed. The `DeadLetter` middleware (`ReceiveDeadLetterMiddleware`) re-dispatches the original envelope to the configured `SkippedEndpoint`. Reply queues are temporary, per-instance queues used for request/reply correlation. ## Customize outbound routes To override where a message is sent or published, use `AddMessage()` with a route configuration: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddMessage(m => { m.Publish(r => r.ToExchange("custom-orders-exchange")); }) .AddRabbitMQ(); ``` `OrderPlacedEvent` now publishes to `custom-orders-exchange` instead of the convention-derived name. This is an explicit route - it takes priority over naming conventions. The receive endpoint is unaffected; it still subscribes based on the handler's message type. For send (point-to-point) routes: C# ``` builder.Services .AddMessageBus() .AddMessage(m => { m.Send(r => r.ToQueue("payment-processing-queue")); }) .AddRabbitMQ(); ``` Use these extension methods to target specific destination types when configuring outbound routes: | Method | URI Scheme | Example | | ---------------- | ---------- | ------------------------------- | | ToQueue(name) | queue: | r.ToQueue("payment-queue") | | ToExchange(name) | exchange: | r.ToExchange("events-exchange") | | ToTopic(name) | topic: | r.ToTopic("orders.placed") | The URI schemes (`queue:`, `exchange:`, `topic:`) tell Mocha what kind of transport entity to target. `queue:` addresses a point-to-point queue directly. `exchange:` addresses a fan-out exchange (RabbitMQ) or equivalent. `topic:` addresses a topic-based routing entity. The transport interprets these schemes and maps them to its native concepts. To bypass routing entirely and send to a specific address at call time, pass a `SendOptions`: C# ``` await bus.SendAsync(new ReserveInventoryCommand { OrderId = orderId, ProductId = productId, Quantity = 3 }, new SendOptions { Endpoint = new Uri("rabbitmq://custom-inventory-queue") }, cancellationToken); ``` ## Customize queues and binding Use `transport.Queue("name")` as the primary API when you need to customize receive topology. The queue builder starts with the queue as the unit of configuration. It can declare the queue, bind handlers or consumers, opt into convention bindings, and configure receive settings in one place. Calling `Queue("name")` by itself creates an infrastructure queue. Adding `Handler()`, `Consumer()`, or `Receives()` materializes a receive endpoint for that queue. C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEventHandler() .AddRabbitMQ(transport => { transport.BindExplicitly(); transport.Queue("combined-orders") .BindImplicitly() .MaxConcurrency(5) .FaultEndpoint("order-errors") .SkippedEndpoint("order-skipped") .Handler() .Handler(); }); ``` Both handlers now consume from the same `combined-orders` queue. Without explicit transport binding, they would each get their own convention-derived endpoint. ### Bind handlers or message types Use `.Handler()` or `.Consumer()` when you know the concrete handler or consumer types that should run on the queue: C# ``` transport.Queue("combined-orders") .BindImplicitly() .Handler() .Handler(); transport.Queue("audit-orders") .BindImplicitly() .Consumer(); ``` Use `.Receives()` when the queue should receive a message type and Mocha should connect all registered handlers for that message type: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEventHandler() .AddRabbitMQ(transport => { transport.BindExplicitly(); transport.Queue("all-orders") .BindImplicitly() .Receives(); }); ``` Both `OrderPlacedHandler` and `OrderAuditHandler` now receive from the `all-orders` queue. This is topology-first design: you declare what messages a queue handles, and Mocha wires up all registered handlers. Use `.Handler()` when you know which handler types to bind. Use `.Receives()` when you care about the message type and want all handlers for that type automatically connected. The same queue can use both approaches: C# ``` transport.Queue("orders") .BindImplicitly() .Receives() .Handler(); ``` If a message type is declared with `.Receives()` but no handler is registered, Mocha throws an exception at startup. ### Understand implicit and explicit binding Binding has two scopes: transport and queue. At the transport scope, `BindImplicitly()` is the default. The transport auto-discovers registered handlers, creates convention-named queues, and adds the convention-derived exchange, topic, or subscription bindings for the messages each handler consumes. C# ``` builder.Services .AddMessageBus() .AddEventHandler() // -> my-service.order-placed .AddEventHandler() // -> my-service.payment-received .AddRabbitMQ(transport => { transport.BindImplicitly(); // default }); ``` `BindExplicitly()` at the transport scope turns off that auto-discovery. Use it when the queues you define with `Queue("name")` should be the complete receive topology for that transport. At the queue scope, `BindImplicitly()` keeps convention-derived source bindings for the message types handled by that queue. This is the common combination for custom queue names: C# ``` transport.BindExplicitly(); transport.Queue("order-processing") .BindImplicitly() .Receives(); ``` `BindExplicitly()` at the queue scope suppresses convention-derived source bindings for that queue. Use it when you bind the queue to a specific source yourself, for example with `BindFrom(...)` or a transport-specific topology declaration. C# ``` transport.BindExplicitly(); transport.Queue("regional-orders") .BindExplicitly() .BindFrom(new Uri("exchange:region-events"), "eu.*") .Handler(); ``` ### Configure convention endpoints Use `transport.Handler()` or `transport.Consumer()` at the end of the transport configuration when you want to keep the convention-derived queue name and only tune the endpoint for one handler or consumer. This is useful for small endpoint changes, but `Queue("name")` is the preferred surface when the queue name, queue topology, or multiple handler bindings are part of the customization. C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(rabbit => { rabbit.Handler() .ConfigureEndpoint(ep => ep .MaxConcurrency(5) .FaultEndpoint("order-errors") .SkippedEndpoint("order-skipped")); }); ``` The same pattern works for consumers: C# ``` rabbit.Consumer() .ConfigureEndpoint(ep => ep.MaxConcurrency(3)); ``` Inside `ConfigureEndpoint()`, you have access to transport-specific settings. To set prefetch on a RabbitMQ endpoint: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(rabbit => { rabbit.Handler() .ConfigureEndpoint(ep => ep.MaxPrefetch(50)); }); ``` For PostgreSQL, configure the batch size: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddPostgres(transport => { transport.Handler() .ConfigureEndpoint(ep => ep.MaxBatchSize(100)); }); ``` See the [RabbitMQ](https://chillicream.com/docs/mocha/transports/rabbitmq), [PostgreSQL](https://chillicream.com/docs/mocha/transports/postgres), and [InMemory](https://chillicream.com/docs/mocha/transports/in-memory) transport pages for the full set of transport-specific queue and endpoint settings. ### Multi-transport handler routing In a multi-transport setup, `Handler()` also determines which transport owns the handler. Mark one transport as the default with `.IsDefaultTransport()`, then claim specific handlers on other transports: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEventHandler() .AddRabbitMQ(r => r.IsDefaultTransport()) // default for unclaimed handlers .AddInMemory(m => m.Handler()); // AuditHandler claimed by InMemory // OrderPlacedHandler → RabbitMQ (default, implicit) // AuditHandler → InMemory (claimed) ``` A claimed handler is bound to the claiming transport regardless of which transport is the default. Unclaimed handlers fall through to the default transport. This is the recommended pattern for multi-transport routing - it avoids `BindExplicitly()` and keeps the configuration minimal. ### Temporary endpoints and per-instance identity Call `Temporary()` on a receive endpoint or queue descriptor to scope its underlying infrastructure to the lifetime of the consuming process instead of provisioning it durably: C# ``` transport.Queue($"tenant-events-{instanceId}") .Temporary() .Receives(); ``` `instanceId` here is a value your host or application supplies - a process GUID, a pod name, an assigned worker ID. Mocha does not generate or append an instance identity to endpoint or queue names on your behalf. `Queue(name)` always uses the complete string you pass as the endpoint name and as the broker entity name; `GetReceiveEndpointName` applies the same naming conventions described above regardless of `Temporary()`, and never appends an instance segment. `Temporary()` is a lifecycle intent, not a message setting. It controls how long the endpoint's backing queue exists, not how long an individual message on that queue lives - do not confuse it with a message TTL. A uniquely named temporary queue binds to its publish topic or exchange the same way any other subscribe endpoint does. If every running instance calls `Queue($"tenant-events-{instanceId}")` with its own `instanceId`, each instance gets its own queue bound to the same source, so every live instance receives a copy of every published message. If two instances instead pass the same queue name, they share one queue and become competing consumers on it - each message goes to only one of them. Each transport maps `Temporary()` to a different native mechanism: | Transport | Mapping | | ----------------- | --------------------------------------------------------------------------------------------- | | Azure Service Bus | AutoDeleteOnIdle on the queue (24-hour default, Temporary(TimeSpan) for a custom idle window) | | RabbitMQ | A non-durable, auto-delete queue | | PostgreSQL | AutoDelete on the queue row, cascaded from the owning consumer's heartbeat and expiry | | InMemory | API parity only - a temporary queue's lifetime is the hosting process's own runtime disposal | See the transport pages under [Transports](https://chillicream.com/docs/mocha/transports) for the full mapping, defaults, and conflict-detection behavior for each transport. ### Dispatch endpoints For outbound endpoints, use `DispatchEndpoint("name")`: C# ``` builder.Services .AddMessageBus() .AddRabbitMQ(transport => { transport.DispatchEndpoint("custom-dispatch") .Publish() .Send(); }); ``` ## Scope precedence Configuration in Mocha follows a three-level scope hierarchy: **bus > transport > endpoint**. The most specific scope wins. ``` Bus (global defaults) -> Transport (transport-specific overrides) -> Endpoint (per-endpoint overrides) ``` This applies to middleware pipelines, circuit breakers, concurrency limiters, and any feature that can be configured at multiple levels: C# ``` builder.Services .AddMessageBus() .AddConcurrencyLimiter(opts => opts.MaxConcurrency = 20) // bus-level default .AddRabbitMQ(transport => { transport.AddConcurrencyLimiter(opts => opts.MaxConcurrency = 10); // transport override transport.Endpoint("high-throughput") .MaxConcurrency(50); // endpoint override }); ``` The `high-throughput` endpoint processes 50 messages concurrently. All other RabbitMQ endpoints use 10\. Endpoints on other transports use the bus default of 20. The middleware pipeline is compiled per-endpoint from the same three layers: bus middleware runs first, then transport middleware, then endpoint middleware. This means a retry policy registered at the bus level applies everywhere, but you can add an extra circuit breaker only for a specific endpoint. Other pages in this documentation reference this scope hierarchy as the canonical model - it governs middleware, reliability features, and observability configuration uniformly. ## Next steps Your routing and endpoint configuration is set. From here: - [**Middleware and Pipelines**](https://chillicream.com/docs/mocha/middleware-and-pipelines) \- Write custom middleware, control pipeline ordering, and understand how the three pipeline stages interact. Want to customize the processing pipeline? That's the next page. - [**Reliability**](https://chillicream.com/docs/mocha/reliability) \- Configure fault handling, circuit breakers, concurrency limits, the transactional outbox, and the idempotent inbox. - [**Transports**](https://chillicream.com/docs/mocha/transports) \- Dive into transport-specific configuration for RabbitMQ and InMemory. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/routing-and-endpoints.md) Maintained by ChilliCream. Last updated on **August 23, 2026** by **PascalSenn** --- # Sagas - Mocha > Learn how to orchestrate long-running business processes with saga state machines in Mocha, including state management, persistence, and parallel coordination. Canonical source: https://chillicream.com/docs/mocha/sagas The saga pattern was introduced by Garcia-Molina & Salem in 1987 as a way to manage long-lived transactions without holding distributed locks. In a messaging system, sagas implement the [Process Manager pattern](https://www.enterpriseintegrationpatterns.com/patterns/messaging/ProcessManager.html) \- they coordinate a sequence of messages, track state across them, and drive a business process to completion. Mocha sagas use orchestration-style coordination: one central state machine issues commands and waits for replies, rather than choreographing services through shared events. See [microservices.io](https://microservices.io/patterns/data/saga.html) and the [Microsoft Azure Architecture: Saga pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/saga) for broader context on the pattern. A saga loads or creates state when a message arrives, applies the configured transition, dispatches any side-effects (publish or send), and persists the result. When the saga reaches a final state, its persisted state is deleted and an optional response is sent back to the originator. ## When to use sagas vs. handlers | Scenario | Use a handler | Use a saga | | -------------------------------------------- | ------------- | ---------- | | Single message in, single action out | Yes | No | | Process completes in one step | Yes | No | | Process spans multiple messages over time | No | Yes | | Need to coordinate parallel operations | No | Yes | | Need persistent state across failures | No | Yes | | Need to send a response after multiple steps | No | Yes | Handlers are simpler and appropriate when a single message triggers a single action. Sagas add value when the workflow requires multiple steps, waits for replies, or must survive process restarts. A common mistake is using a saga for work that fits in a handler. If your handler calls `SendAsync` and does not need to wait for a reply before responding, a plain handler is sufficient. Reach for a saga when the process must pause and resume based on future messages. ## State machine diagram The refund saga built in the tutorial below has three states. Each arrow is labeled with the message that triggers the transition: This is the shape you will build in the tutorial. Keep this diagram in mind as you work through the steps - each code block maps to one of these transitions. Warning **messages can arrive out of order.** In a distributed system, message delivery order is not guaranteed. If your saga handles two message types that could both initiate the process, configure both as initial transitions. Do not assume the first message you define in code is the first message the saga will receive in production. ## Tutorial: Build a refund saga By the end of this section, you will have a working saga that receives a refund request, sends a command to the billing service, and returns a response when the refund completes. ### Define the saga state Saga state must extend `SagaStateBase`. This base class provides `Id` (unique saga instance identifier), `State` (current state name), `Errors` (error history), and `Metadata` (custom key-value data). C# ``` using Mocha.Sagas; namespace MyApp.Sagas; public class RefundSagaState : SagaStateBase { // Order information captured when the saga starts public required Guid OrderId { get; init; } public required decimal Amount { get; init; } public required string CustomerId { get; init; } public required string Reason { get; init; } // Results populated during processing public Guid? RefundId { get; set; } public decimal? RefundedAmount { get; set; } public string? FailureReason { get; set; } // Factory method to create state from the initiating request public static RefundSagaState FromQuickRefund(RequestQuickRefundRequest request) => new() { OrderId = request.OrderId, Amount = request.Amount, CustomerId = request.CustomerId, Reason = request.Reason }; // Helper to create the command sent to billing public ProcessRefundCommand ToProcessRefund() => new() { OrderId = OrderId, Amount = Amount, Reason = Reason, CustomerId = CustomerId }; } ``` Properties marked `required` are set when the saga starts. Mutable properties (`set`) are updated as transitions execute. The factory method converts the initiating message into the state object. Helper methods create the commands the saga dispatches to other services. ### Define the message contracts C# ``` using Mocha; namespace MyApp.Messages; // The request that starts the saga (implements IEventRequest for request/reply) public sealed class RequestQuickRefundRequest : IEventRequest { public required Guid OrderId { get; init; } public required decimal Amount { get; init; } public required string CustomerId { get; init; } public required string Reason { get; init; } } // The response returned when the saga completes public sealed class QuickRefundResponse { public required Guid OrderId { get; init; } public required bool Success { get; init; } public Guid? RefundId { get; init; } public decimal? RefundedAmount { get; init; } public string? FailureReason { get; init; } public required DateTimeOffset CompletedAt { get; init; } } // Command sent by the saga to the billing service public sealed class ProcessRefundCommand : IEventRequest { public required Guid OrderId { get; init; } public required decimal Amount { get; init; } public required string Reason { get; init; } public required string CustomerId { get; init; } } // Response from the billing service public sealed class ProcessRefundResponse { public required Guid RefundId { get; init; } public required Guid OrderId { get; init; } public required decimal Amount { get; init; } public required bool Success { get; init; } public string? FailureReason { get; init; } public required DateTimeOffset ProcessedAt { get; init; } } ``` ### Define the saga Subclass `Saga` and override `Configure` to define the state machine. C# ``` using Mocha.Sagas; using MyApp.Messages; namespace MyApp.Sagas; public sealed class QuickRefundSaga : Saga { // State name constants private const string AwaitingRefund = nameof(AwaitingRefund); private const string Completed = nameof(Completed); protected override void Configure(ISagaDescriptor descriptor) { // 1. Initial state: receive the refund request, create state, send command to billing descriptor .Initially() .OnRequest() .StateFactory(RefundSagaState.FromQuickRefund) .Send((_, state) => state.ToProcessRefund()) .TransitionTo(AwaitingRefund); // 2. Awaiting refund: handle the billing service's reply descriptor .During(AwaitingRefund) .OnReply() .Then((state, response) => { if (response.Success) { state.RefundId = response.RefundId; state.RefundedAmount = response.Amount; } else { state.FailureReason = response.FailureReason ?? "Refund processing failed"; } }) .TransitionTo(Completed); // 3. Final state: build and send the response back to the original requester descriptor .Finally(Completed) .Respond(state => new QuickRefundResponse { OrderId = state.OrderId, Success = state.RefundId.HasValue, RefundId = state.RefundId, RefundedAmount = state.RefundedAmount, FailureReason = state.FailureReason, CompletedAt = DateTimeOffset.UtcNow }); } } ``` The state machine has three states: 1. **Initial** \- receives `RequestQuickRefundRequest`, creates `RefundSagaState`, sends `ProcessRefundCommand` to billing, transitions to `AwaitingRefund`. 2. **AwaitingRefund** \- receives `ProcessRefundResponse` from billing, updates state with the result, transitions to `Completed`. 3. **Completed** (final) - builds `QuickRefundResponse` from state and sends it back to the original caller. The persisted saga state is then deleted. ### Register the saga C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddMessageBus() .AddSaga() .AddRabbitMQ(); var app = builder.Build(); app.Run(); ``` `.AddSaga()` registers the saga with the bus. The saga's consumer is created automatically and listens for the message types defined in the state machine transitions. Tip Sagas are discovered automatically by the source generator. If you use `Add{ModuleName}()` from [Handler Registration](https://chillicream.com/docs/mocha/handler-registration), you do not need to register sagas manually - the source generator calls `AddSaga()` for every concrete `Saga` subclass it finds. ### Trigger the saga From the sender side, use `RequestAsync` to start the saga and await the final response: C# ``` var response = await bus.RequestAsync( new RequestQuickRefundRequest { OrderId = orderId, Amount = 49.99m, CustomerId = "customer-42", Reason = "Defective product" }, cancellationToken); Console.WriteLine($"Refund {response.RefundId}: success={response.Success}"); ``` Expected output: ``` info: Mocha.Sagas.Saga[0] Created saga state QuickRefundSaga 3f2504e0-4f89-11d3-9a0c-0305e82c3301 info: Mocha.Sagas.Saga[0] Entering state QuickRefundSaga Initial info: Mocha.Sagas.Saga[0] Sending event QuickRefundSaga ProcessRefundCommand info: Mocha.Sagas.Saga[0] Transitioning state QuickRefundSaga AwaitingRefund by event ProcessRefundResponse info: Mocha.Sagas.Saga[0] Entering state QuickRefundSaga Completed info: Mocha.Sagas.Saga[0] Replying to saga QuickRefundSaga ... QuickRefundResponse info: Mocha.Sagas.Saga[0] Saga completed QuickRefundSaga 3f2504e0-4f89-11d3-9a0c-0305e82c3301 Refund d4c3b2a1-...: success=True ``` The saga receives the request, sends a command to billing, waits for the reply, builds a response, and completes. The caller's `RequestAsync` resolves with the typed `QuickRefundResponse`. ## How-to guides ### Coordinate parallel operations When a saga needs to wait for multiple replies before proceeding, model each combination as a separate state. The `ReturnProcessingSaga` demonstrates this pattern - after inspection, it sends both a refund command and a restock command in parallel, then handles whichever reply arrives first. C# ``` public sealed class ReturnProcessingSaga : Saga { private const string AwaitingInspection = nameof(AwaitingInspection); private const string AwaitingBothReplies = nameof(AwaitingBothReplies); private const string RestockDoneAwaitingRefund = nameof(RestockDoneAwaitingRefund); private const string RefundDoneAwaitingRestock = nameof(RefundDoneAwaitingRestock); private const string Completed = nameof(Completed); protected override void Configure(ISagaDescriptor descriptor) { // Start: package received, send inspection command descriptor .Initially() .OnEvent() .StateFactory(RefundSagaState.FromReturnPackageReceived) .Send((_, state) => state.ToInspectReturn()) .TransitionTo(AwaitingInspection); // After inspection: send refund AND restock in parallel descriptor .During(AwaitingInspection) .OnReply() .Then((state, response) => state.InspectionResult = response.Result) .Send((_, state) => state.ToRestockInventory()) .Send((_, state) => state.ToProcessRefund()) .TransitionTo(AwaitingBothReplies); // Restock arrives first descriptor .During(AwaitingBothReplies) .OnReply() .Then((state, response) => { state.InventoryRestocked = response.Success; state.QuantityRestocked = response.QuantityRestocked; }) .TransitionTo(RestockDoneAwaitingRefund); // Refund arrives first descriptor .During(AwaitingBothReplies) .OnReply() .Then((state, response) => { if (response.Success) { state.RefundId = response.RefundId; state.RefundedAmount = response.Amount; } else { state.FailureReason = response.FailureReason; } }) .TransitionTo(RefundDoneAwaitingRestock); // Second reply arrives: restock after refund descriptor .During(RefundDoneAwaitingRestock) .OnReply() .Then((state, response) => { state.InventoryRestocked = response.Success; state.QuantityRestocked = response.QuantityRestocked; }) .TransitionTo(Completed); // Second reply arrives: refund after restock descriptor .During(RestockDoneAwaitingRefund) .OnReply() .Then((state, response) => { if (response.Success) { state.RefundId = response.RefundId; state.RefundedAmount = response.Amount; } else { state.FailureReason = response.FailureReason; } }) .TransitionTo(Completed); // Done descriptor.Finally(Completed); } } ``` The key insight: `AwaitingBothReplies` has two transitions, one for each reply type. Whichever arrives first moves the saga to a "one done, waiting for the other" state. The second reply completes the saga. This avoids race conditions without explicit locking. ### Start a saga from an event Not all sagas begin with a request/reply. Use `.OnEvent()` instead of `.OnRequest()` when the saga is initiated by a published event. C# ``` descriptor .Initially() .OnEvent() .StateFactory(RefundSagaState.FromReturnPackageReceived) .Send((_, state) => state.ToInspectReturn()) .TransitionTo(AwaitingInspection); ``` When using `.OnEvent()`, the saga does not capture a reply address. No response is sent when the saga completes. Use `.OnRequest()` when the caller expects a response via `RequestAsync`. ### Publish events from transitions To publish an event as a side-effect of a transition, use `.Publish()` on the transition descriptor: C# ``` descriptor .During(AwaitingRefund) .OnReply() .Then((state, response) => { state.RefundId = response.RefundId; state.RefundedAmount = response.Amount; }) .Publish((_, state) => new RefundCompletedEvent { OrderId = state.OrderId, RefundId = state.RefundId!.Value, Amount = state.RefundedAmount!.Value }) .TransitionTo(Completed); ``` `.Publish()` dispatches the event after the transition action runs. The saga automatically adds the `saga-id` header to published messages so downstream consumers can correlate them back to the saga instance. ### Publish or send on state entry To dispatch messages when entering a state (not tied to a specific transition), use `.OnEntry()` on the state descriptor: C# ``` descriptor .During(AwaitingInspection) .OnEntry() .Publish(state => new InspectionStartedEvent { OrderId = state.OrderId, ReturnId = state.ReturnId!.Value }); ``` On-entry actions run every time the saga enters that state, regardless of which transition caused it. ### Handle faults and compensation #### Compensating transactions When a service-spanning process fails partway through, you cannot roll back completed steps the way a database transaction would. Instead, you issue compensating transactions - commands that logically undo the work already done. For example, if a refund succeeds but restocking fails, you may need to reverse the refund. This is a fundamental property of distributed systems: without distributed ACID isolation, partial failure is always possible. The [Microsoft Azure Architecture documentation](https://learn.microsoft.com/en-us/azure/architecture/patterns/saga) classifies transactions in a saga as compensable (can be undone), pivot (the point of no return), or retryable (will eventually succeed). Design your `OnFault()` handlers with this taxonomy in mind. #### Implement fault handling When a command sent by a saga fails, the receiving service sends a `NotAcknowledgedEvent` back instead of the expected reply. Use `OnFault()` to define a transition that handles this case and runs compensation logic. C# ``` descriptor .During(AwaitingRefund) .OnFault() .Then((state, fault) => { state.FailureReason = fault.ErrorMessage; state.FailureStage = "Refund"; }) .Send((_, state) => new ReverseChargeCommand { OrderId = state.OrderId, Amount = state.Amount }) .TransitionTo("Compensating"); ``` `OnFault()` is an extension method on `ISagaStateDescriptor` that registers a transition for `NotAcknowledgedEvent`. The fault record carries `ErrorCode`, `ErrorMessage`, `CorrelationId`, and `MessageId` so your compensation logic can identify what failed. For sagas that send multiple commands in parallel, add `OnFault()` transitions in each waiting state. This ensures you can compensate regardless of which step failed. ### Handle timeouts Long-running sagas may need to protect against the case where an expected reply never arrives. Use `OnTimeout()` to define what happens when the saga has been waiting too long. #### Schedule a timeout when entering a waiting state Register the timeout as an on-entry action on the state that needs a deadline: C# ``` descriptor .During(AwaitingRefund) .OnEntry() .ScheduleTimeout(TimeSpan.FromMinutes(5)); // fire SagaTimedOutEvent after 5 minutes descriptor .During(AwaitingRefund) .OnTimeout() .Then((state, _) => { state.FailureReason = "Refund timed out after waiting 5 minutes"; }) .Send((_, state) => new ReverseChargeCommand { OrderId = state.OrderId, Amount = state.Amount }) .TransitionTo("TimedOut"); descriptor.Finally("TimedOut") .Respond(state => new QuickRefundResponse { OrderId = state.OrderId, Success = false, FailureReason = state.FailureReason, CompletedAt = DateTimeOffset.UtcNow }); ``` When the saga transitions out of `AwaitingRefund` via a normal reply, the scheduled timeout is cancelled automatically. If the timeout fires first, `OnTimeout()` runs the configured transition - in this case, dispatching compensation and moving to `TimedOut`. #### What to do when a timeout fires A fired timeout means the downstream service did not reply within the expected window. Common responses: - Transition to a compensation state and issue undo commands for completed steps. - Transition to a final state and send a failure response back to the original caller. - Transition to a retry state and re-send the original command (only if idempotent). Do not leave the saga in the same waiting state after a timeout. The saga must advance so it does not wait forever. ### Configure saga persistence with Postgres By default, saga state is stored in memory and lost on restart. For production, use a persistent store. Mocha provides a Postgres-backed saga store via Entity Framework Core. **1\. Add the EF Core entity to your `DbContext`.** C# ``` using Microsoft.EntityFrameworkCore; using Mocha.Sagas.EfCore; public class CatalogDbContext : DbContext { public DbSet SagaStates => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { // Configure the saga state table modelBuilder.AddPostgresSagas(); } } ``` **2\. Register the Postgres saga store with the bus.** C# ``` builder.Services .AddMessageBus() .AddSaga() .AddSaga() .AddEntityFramework(p => { p.AddPostgresSagas(); }) .AddRabbitMQ(); ``` **3\. Create an EF Core migration.** Bash ``` dotnet ef migrations add Sagas dotnet ef database update ``` ### Create a saga with the fluent API For simple sagas that do not need a dedicated class, use `Saga.Create()`: C# ``` var saga = Saga.Create(descriptor => { descriptor .Initially() .OnRequest() .StateFactory(RefundSagaState.FromQuickRefund) .Send((_, state) => state.ToProcessRefund()) .TransitionTo("AwaitingRefund"); descriptor .During("AwaitingRefund") .OnReply() .Then((state, response) => { state.RefundId = response.RefundId; state.RefundedAmount = response.Amount; }) .TransitionTo("Completed"); descriptor .Finally("Completed") .Respond(state => new QuickRefundResponse { OrderId = state.OrderId, Success = state.RefundId.HasValue, RefundId = state.RefundId, RefundedAmount = state.RefundedAmount, FailureReason = state.FailureReason, CompletedAt = DateTimeOffset.UtcNow }); }); ``` This produces the same state machine as the class-based approach. The fluent API is useful for tests and prototyping. ## Concurrency If two messages for the same saga instance arrive simultaneously - for example, two parallel replies landing within milliseconds of each other - one succeeds and the other retries. The Postgres saga store uses optimistic concurrency with a version column. The second writer detects the version mismatch, reloads the latest state, and retries the transition. This retry is automatic and transparent. You do not need to handle it in your saga code. However, if a saga endpoint processes a very high volume of concurrent messages for the same instance, retry contention can add latency. In that case, consider hosting high-concurrency sagas on dedicated endpoints with constrained parallelism. ## How saga correlation works When a saga sends a command, Mocha attaches a `saga-id` header to the outgoing message. When the reply arrives, the saga runtime reads this header to find the existing saga instance and load its persisted state. For event-initiated sagas, correlation uses the `ICorrelatable` interface: C# ``` using Mocha.Sagas; public sealed record SagaTimedOutEvent(Guid SagaId) : ICorrelatable { public Guid? CorrelationId => SagaId; } ``` Messages that implement `ICorrelatable` are matched to saga instances by their `CorrelationId`. This is how sagas handle events that are not direct replies to commands the saga sent. The correlation lookup order is: 1. Check if the message implements `ICorrelatable` and has a non-null `CorrelationId`. 2. Check the message headers for a `saga-id` header. 3. If neither is found, treat the message as an initiating event and create a new saga instance. ## Timeouts A saga that waits for a message that never arrives will stay in its current state forever. Timeouts ensure every saga eventually completes - either through normal processing or by timing out. Mocha provides a saga-level `Timeout()` API that sets a single deadline for the entire saga instance. The timeout is scheduled when the saga is created and automatically cancelled when the saga reaches any final state. > **Prerequisites:** `Timeout()` schedules a `SagaTimedOutEvent` through the same scheduled message store used by `ScheduleSendAsync` \- the PostgreSQL transport's own store, or the `UsePostgresScheduling()` fallback for transports without one (InMemory, RabbitMQ). If no store is available when the saga is created, the timeout is silently skipped: the saga proceeds without an automatic timeout and only receives a `SagaTimedOutEvent` if one is sent manually. See [Scheduling: Set up store-based scheduling](https://chillicream.com/docs/mocha/scheduling#set-up-store-based-scheduling-for-rabbitmq) for setup instructions. ### Configure a saga-level timeout Call `Timeout()` on the saga descriptor to set a deadline that applies to the entire saga instance: C# ``` protected override void Configure(ISagaDescriptor descriptor) { // 30-minute timeout - saga will time out if it doesn't reach a final state descriptor.Timeout(TimeSpan.FromMinutes(30)) .Respond(state => new OrderTimedOutResponse(state.Id)); descriptor.Initially() .OnEvent() .StateFactory(e => new OrderState { OrderId = e.OrderId }) .TransitionTo("AwaitingPayment"); // Handle timeout in any non-final state descriptor.DuringAny() .OnTimeout() .TransitionTo(StateNames.TimedOut); descriptor.During("AwaitingPayment") .OnEvent() .TransitionTo("Completed"); descriptor.Finally("Completed"); } ``` `Timeout(TimeSpan)` does three things: 1. Creates a "Timed Out" final state (named `StateNames.TimedOut`). 2. Returns an `ISagaFinalStateDescriptor` so you can chain `.Respond()` to send a response when the saga times out. 3. Tells the saga framework to schedule a timeout event when a new saga instance is created. The timeout clock starts when the saga is created - that is, when the first event arrives and a new instance is provisioned. If the saga reaches any final state before the deadline, the pending timeout is automatically cancelled. If the timeout fires, a `SagaTimedOutEvent` is delivered to the saga instance. Use `OnTimeout()` on a state descriptor to define what happens when the timeout arrives. Handle it the same way you handle any other event - run `.Then()` actions, dispatch commands, or transition to a different state. ### Key behaviors | Behavior | Detail | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Scope | Per-saga instance, not per-state. The deadline covers the entire saga lifetime. | | Duration | Fixed at configuration time via TimeSpan. | | Auto-cancellation | When the saga reaches any final state (normal completion, error final state, etc.), the pending timeout is cancelled. | | Late delivery | If the timeout fires after the saga was already deleted, the event is silently dropped. | | Missing handler | If no OnTimeout() handler is configured for the current state, the saga throws an execution error. See [troubleshooting](#timeout-troubleshooting) below. | | Recommended pattern | DuringAny().OnTimeout() handles the timeout regardless of which state the saga is in. | | Response on timeout | Chain .Respond() on the timed-out final state to send a response back to the original requester. | | Scheduling store | Requires a scheduled message store to schedule the timeout: the PostgreSQL transport's own store, or the UsePostgresScheduling() fallback for transports without one (InMemory, RabbitMQ). Without a store, the timeout is silently skipped rather than throwing - see [Scheduling](https://chillicream.com/docs/mocha/scheduling#set-up-store-based-scheduling-for-rabbitmq) for setup. | ### Timeout troubleshooting **Timeout never fires.**Verify that a scheduled message store is configured for the saga's transport: either the PostgreSQL transport's own store (registered automatically by `AddPostgres()`) or the `UsePostgresScheduling()` fallback. Without one, the saga catches the resulting `NotSupportedException` and silently proceeds without scheduling a timeout - no error is raised, so a saga you expect to time out will simply never receive a `SagaTimedOutEvent`. **"SagaExecutionException: No transition defined for SagaTimedOutEvent."**You configured `Timeout()` but did not add an `OnTimeout()` handler. Add a catch-all handler: C# ``` descriptor.DuringAny() .OnTimeout() .TransitionTo(StateNames.TimedOut); ``` ## API reference ### Saga descriptor | Method | Available on | Parameters | Description | | --------- | ----------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | Initially | ISagaDescriptor | \- | Returns the initial state descriptor for defining transitions that create new saga instances. | | During | ISagaDescriptor | string stateName | Returns a state descriptor for defining transitions in the named state. | | DuringAny | ISagaDescriptor | \- | Returns a catch-all state descriptor whose transitions apply to all non-initial, non-final states. | | Finally | ISagaDescriptor | string stateName | Declares a final state. When the saga enters this state, persisted state is deleted and an optional response is sent. | | Timeout | ISagaDescriptor | TimeSpan timeout | Creates a timed-out final state and schedules automatic timeout on saga creation. Returns ISagaFinalStateDescriptor for chaining .Respond(). | ### State transitions | Method | Available on | Parameters | Description | | ------------ | ---------------------------- | ---------- | --------------------------------------------------------------------------------------- | | OnEvent | ISagaStateDescriptor | \- | Registers a transition triggered by a published event. | | OnRequest | ISagaStateDescriptor | \- | Registers a transition triggered by a request message (captures reply address). | | OnReply | ISagaStateDescriptor | \- | Registers a transition triggered by a reply to a previously sent command. | | OnFault | ISagaStateDescriptor | \- | Registers a transition triggered by a NotAcknowledgedEvent. | | OnTimeout | ISagaStateDescriptor | \- | Registers a transition for SagaTimedOutEvent. Sugar for OnRequest(). | ### Transition actions | Method | Available on | Parameters | Description | | ------------ | ----------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------- | | StateFactory | ISagaTransitionDescriptor | Func | Creates new saga state from the initiating event. Required on initial transitions. | | Then | ISagaTransitionDescriptor | Action | Runs a synchronous action to update saga state. | | Send | ISagaTransitionDescriptor | Func | Sends a command as a side-effect of the transition. | | Publish | ISagaTransitionDescriptor | Func | Publishes an event as a side-effect of the transition. | | TransitionTo | ISagaTransitionDescriptor | string stateName | Moves the saga to the named state. | ### Final state | Method | Available on | Parameters | Description | | ------- | --------------------------------- | -------------------- | --------------------------------------------------------------------------------- | | Respond | ISagaFinalStateDescriptor | Func | Sends a response to the original requester when the saga enters this final state. | ### Lifecycle | Method | Available on | Parameters | Description | | ------------- | ---------------------------- | ---------- | ------------------------------------------------------------------------------------------ | | OnEntry | ISagaStateDescriptor | \- | Returns a lifecycle descriptor for actions that run every time the saga enters the state. | | WhenCompleted | ISagaDescriptor | \- | Returns a lifecycle descriptor for actions that run when the saga reaches any final state. | ## Troubleshooting **Saga state is lost on restart.**Saga state is stored in memory by default. For production, configure a persistent store. See [Configure saga persistence with Postgres](#configure-saga-persistence-with-postgres). **"No transition defined" exception.**The saga received a message in a state that has no matching transition. Verify that every state has transitions for all expected message types. Use `DuringAny()` for catch-all transitions that apply across all non-final states. **Saga never completes.**Check that the downstream service is running and sending replies. If the saga is waiting for a reply that never arrives, consider adding a [timeout](#timeouts) to ensure the saga eventually reaches a final state. **Two messages arrive for the same saga instance simultaneously.**The Postgres saga store uses optimistic concurrency. The second writer detects the version mismatch, reloads state, and retries automatically. See [Concurrency](#concurrency). ## Next steps Understand how transports work in [Transports](https://chillicream.com/docs/mocha/transports). > **Runnable examples:** [BasicSaga](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Sagas/BasicSaga), [ParallelSaga](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Sagas/ParallelSaga) > > **Full demo:** [Demo.Catalog](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Catalog) implements two production-style sagas: `QuickRefundSaga` (a simple two-state refund flow) and `ReturnProcessingSaga` (a complex multi-state saga with parallel inspection, restocking, and refund steps). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/sagas.md) Maintained by ChilliCream. Last updated on **July 13, 2026** by **PascalSenn** --- # Scheduling - Mocha > Schedule messages for future delivery in Mocha using absolute times or relative delays, with durable Postgres or in-memory persistence. Canonical source: https://chillicream.com/docs/mocha/scheduling Sometimes a message should not be delivered right now. A welcome email goes out 30 minutes after signup. A payment retry fires 24 hours after the first failure. A saga timeout triggers if no response arrives within 5 minutes. Scheduling lets you hand a message to the bus with a future delivery time, and the infrastructure takes care of the rest. If plans change, you can cancel a scheduled message before it is dispatched. C# ``` var result = await bus.SchedulePublishAsync( new SendWelcomeEmail { UserId = userId }, DateTimeOffset.UtcNow.AddMinutes(30), cancellationToken); // result.Token can be used to cancel the message later ``` The call returns immediately with a `SchedulingResult`. The message is persisted and delivered when the scheduled time arrives. If you need to revoke the message before delivery, pass the token to `CancelScheduledMessageAsync`. ## Schedule a message Mocha provides scheduling methods on `IMessageBus` for scheduling with an absolute `DateTimeOffset`. ### Schedule with an absolute time Use a `DateTimeOffset` when you know the exact delivery time: C# ``` var scheduledTime = DateTimeOffset.UtcNow.AddHours(24); // Schedule a publish (fan-out to all subscribers) var publishResult = await bus.SchedulePublishAsync( new PaymentRetryEvent { OrderId = orderId }, scheduledTime, cancellationToken); // Schedule a send (directed to a single handler) var sendResult = await bus.ScheduleSendAsync( new CleanupExpiredSessionsCommand { CutoffTime = cutoff }, scheduledTime, cancellationToken); ``` Both methods return a `SchedulingResult` with a `Token` you can use for cancellation and an `IsCancellable` flag that tells you whether cancellation is supported by the current scheduling infrastructure. ### Schedule with options The scheduling methods also accept an options overload. If you need to combine scheduling with other options like expiration or custom headers, pass a `PublishOptions` or `SendOptions` struct: C# ``` var result = await bus.SchedulePublishAsync( new PaymentRetryEvent { OrderId = orderId }, DateTimeOffset.UtcNow.AddHours(24), new PublishOptions { ExpirationTime = DateTimeOffset.UtcNow.AddHours(48), Headers = new Dictionary { ["priority"] = "high" } }, cancellationToken); ``` C# ``` var result = await bus.ScheduleSendAsync( new RetryPaymentCommand { PaymentId = paymentId }, DateTimeOffset.UtcNow.AddMinutes(30), new SendOptions { ExpirationTime = DateTimeOffset.UtcNow.AddHours(1) }, cancellationToken); ``` You can also set `ScheduledTime` directly on options when calling `PublishAsync` or `SendAsync`. This approach does not return a `SchedulingResult`, so you cannot cancel the message later: C# ``` await bus.PublishAsync( new PaymentRetryEvent { OrderId = orderId }, new PublishOptions { ScheduledTime = DateTimeOffset.UtcNow.AddHours(24), ExpirationTime = DateTimeOffset.UtcNow.AddHours(48), }, cancellationToken); ``` ## Cancel a scheduled message When a scheduled message is no longer needed, cancel it before the scheduled time arrives. The `SchedulingResult` returned by `SchedulePublishAsync` and `ScheduleSendAsync` contains the token you need. C# ``` // Schedule a payment reminder var result = await bus.SchedulePublishAsync( new PaymentReminderEvent { OrderId = orderId }, DateTimeOffset.UtcNow.AddHours(24), cancellationToken); // Customer pays before the reminder fires - cancel it var cancelled = await bus.CancelScheduledMessageAsync( result.Token!, cancellationToken); ``` `CancelScheduledMessageAsync` returns `true` if the message was successfully cancelled and `false` otherwise. ### When cancellation returns false A `false` return does not necessarily mean something went wrong. It means the message is no longer in the store: - **Already dispatched.** The scheduled time passed and the message was delivered. The cancellation window has closed. - **Already cancelled.** A previous call already removed the message. Cancelling twice is safe - the second call returns `false`. - **Token not found.** The token does not match any message in the store. ### SchedulingResult Every call to `SchedulePublishAsync` or `ScheduleSendAsync` returns a `SchedulingResult`: C# ``` var result = await bus.SchedulePublishAsync(message, scheduledTime, cancellationToken); if (result.IsCancellable) { // Store the token so you can cancel later await SaveTokenAsync(result.Token!); } ``` | Property | Type | Description | | ------------- | -------------- | --------------------------------------------------------------------------------------- | | Token | string? | An opaque token for cancelling this message, or null if cancellation is not supported. | | ScheduledTime | DateTimeOffset | The time at which the message is scheduled for delivery. | | IsCancellable | bool | true when the scheduling infrastructure supports cancellation and a token was assigned. | The scheduling middleware is always part of the dispatch pipeline. When you set a `ScheduledTime`, it resolves a scheduled message store for the current transport, the transport's own store first (for example the PostgreSQL or Azure Service Bus native store), then the `UsePostgresScheduling()` fallback store if one is registered. `IsCancellable` is `true` whenever a store was found and persisted the message. If no store is available for the transport, scheduling does not silently fall back to native delivery, it throws `NotSupportedException`. ### Real-world example: cancellable reminder A common pattern is scheduling a reminder that should be revoked when the user completes the expected action. C# ``` public class OrderService(IMessageBus bus, IOrderRepository orders) { public async Task PlaceOrderAsync(Order order, CancellationToken ct) { await orders.SaveAsync(order, ct); // Remind the customer to pay in 24 hours var result = await bus.SchedulePublishAsync( new PaymentReminderEvent { OrderId = order.Id }, DateTimeOffset.UtcNow.AddHours(24), ct); // Persist the token so we can cancel later if (result.IsCancellable) { order.ReminderToken = result.Token; await orders.SaveAsync(order, ct); } } public async Task ConfirmPaymentAsync(Guid orderId, CancellationToken ct) { var order = await orders.GetAsync(orderId, ct); // Payment received - cancel the reminder if (order.ReminderToken is not null) { await bus.CancelScheduledMessageAsync(order.ReminderToken, ct); order.ReminderToken = null; await orders.SaveAsync(order, ct); } } } ``` ## Set up store-based scheduling for RabbitMQ The PostgreSQL and Azure Service Bus transports handle scheduling natively with no extra setup. InMemory and RabbitMQ have no scheduling store of their own, so a scheduled dispatch on either one throws `NotSupportedException` unless you configure a fallback: a Postgres-backed message store that persists scheduled messages and dispatches them through a background worker. **1\. Add the NuGet packages.** Bash ``` dotnet add package Mocha.EntityFrameworkCore dotnet add package Mocha.EntityFrameworkCore.Postgres ``` **2\. Add the `ScheduledMessage` entity to your DbContext model.** C# ``` protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.AddPostgresScheduledMessages(); } ``` This maps the `ScheduledMessage` entity to a `scheduled_messages` table with columns for the envelope, scheduled time, retry count, and error tracking. **3\. Register the scheduling services.** C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEntityFramework(p => { p.UsePostgresScheduling(); }) .AddPostgres(connectionString); ``` | Call | Purpose | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | UsePostgresScheduling() | Registers everything needed for durable scheduling with Postgres, including the background worker and EF Core interceptors. | | AddPostgresScheduledMessages() | Adds the ScheduledMessage entity configuration to the EF Core model. | `UsePostgresScheduling()` sets up everything needed to persist scheduled messages in Postgres and dispatch them at the right time. Outgoing messages with a `ScheduledTime` are intercepted and written to the `scheduled_messages` table instead of being sent to the transport. A background worker continuously polls for due messages and dispatches them through the bus. EF Core interceptors signal the worker when `SaveChanges` or a transaction commit occurs, enabling low-latency wake-up. When `UsePostgresScheduling()` is configured, `SchedulePublishAsync` and `ScheduleSendAsync` return cancellable results with tokens you can pass to `CancelScheduledMessageAsync`. **4\. Create the database migration.** After adding the model configuration, generate and apply an EF Core migration: Bash ``` dotnet ef migrations add AddScheduledMessages dotnet ef database update ``` ## Transport scheduling behavior The PostgreSQL and Azure Service Bus transports have scheduling stores of their own. InMemory and RabbitMQ need the `UsePostgresScheduling()` fallback; without it, a scheduled dispatch on either transport throws `NotSupportedException`. | Transport | Scheduling type | Durability | Cancellation support | Setup required | | ----------------- | -------------------------------------- | ------------------------------- | ---------------------------------------- | ---------------------------------------- | | InMemory | None, requires UsePostgresScheduling() | Durable, via the fallback store | Yes, via the fallback store | UsePostgresScheduling() \+ EF Core model | | PostgreSQL | Native (transport-owned store) | Durable, survives restarts | Yes (token prefixed postgres-transport:) | None | | RabbitMQ | None, requires UsePostgresScheduling() | Durable, via the fallback store | Yes, via the fallback store | UsePostgresScheduling() \+ EF Core model | | Azure Service Bus | Native (ScheduleMessageAsync) | Durable, broker-managed | Yes (native) | None | **InMemory:** The transport has no scheduling store of its own. Scheduling a message throws `NotSupportedException` unless `UsePostgresScheduling()` is registered as a fallback. With the fallback configured, scheduled messages are persisted to Postgres and dispatched through a background worker, the same as RabbitMQ below. **PostgreSQL:** The transport handles scheduling natively through its own scheduled message store, registered automatically when you call `AddPostgres()`. When you set `ScheduledTime`, the transport writes a `scheduled_time` column alongside the message instead of sending it immediately, and only delivers it to consumers after the scheduled time has passed. No additional setup is required beyond the standard [PostgreSQL transport configuration](https://chillicream.com/docs/mocha/transports/postgres). Cancellation is supported - the returned token is prefixed `postgres-transport:` and can be passed to `CancelScheduledMessageAsync`. **RabbitMQ:** RabbitMQ does not support native message scheduling. Scheduling a message throws `NotSupportedException` unless you register `UsePostgresScheduling()` with an EF Core DbContext. Once configured, scheduled messages are persisted to a Postgres `scheduled_messages` table instead of reaching the RabbitMQ transport, and a background worker dispatches them at the scheduled time, routing through the RabbitMQ transport. Cancellation is fully supported - the returned token is prefixed `postgres-scheduler:` and the `SchedulingResult` contains it for use with `CancelScheduledMessageAsync`. **Azure Service Bus:** Azure Service Bus supports both **native scheduling and native cancellation** with no additional infrastructure. When you call `SchedulePublishAsync` or `ScheduleSendAsync`, the dispatch endpoint uses `ServiceBusSender.ScheduleMessageAsync` and the broker holds the message until the scheduled time. The returned `SchedulingResult.Token` encodes the entity path and the broker-assigned sequence number, so `CancelScheduledMessageAsync` can revoke the message via `ServiceBusSender.CancelScheduledMessageAsync` without a Postgres store, EF Core model, or background worker. `IsCancellable` is `true` for every scheduled ASB message. See the [Azure Service Bus transport](https://chillicream.com/docs/mocha/transports/azure-service-bus) page for the full setup. ### Retry behavior If a scheduled message fails to dispatch, the scheduler retries with exponential backoff. Each failed attempt increases the wait time before the next retry. After 10 attempts (the default `max_attempts`), the message is no longer eligible for dispatch. You can inspect failed messages by querying the `scheduled_messages` table and checking the `last_error` column. ### Multiple service instances When multiple instances of your service are running, each scheduled message is processed by exactly one instance. There is no risk of duplicate delivery from the scheduler. ### Outbox integration When both the transactional outbox and scheduling are configured, scheduled messages participate in the transaction correctly. Messages with a `ScheduledTime` are intercepted by the scheduler and never reach the outbox. Messages dispatched by the background worker skip both the scheduler and the outbox, going directly to the transport. See [Reliability](https://chillicream.com/docs/mocha/reliability) for outbox configuration. ## Schedule messages in sagas Saga transitions and lifecycle actions support scheduled message dispatch through dedicated extension methods. This is useful for saga timeouts, reminder patterns, and delayed side effects. ### Schedule in a transition C# ``` public class OrderSagaConfiguration : SagaConfiguration { public override void Configure() { x.Initially() .OnEvent() .StateFactory(_ => new OrderState()) .ScheduledPublish( TimeSpan.FromMinutes(30), state => new OrderReminderEvent { OrderId = state.OrderId }) .TransitionTo("AwaitingPayment"); x.During("AwaitingPayment") .OnEvent() .ScheduledSend( TimeSpan.FromHours(1), state => new GenerateInvoiceCommand { OrderId = state.OrderId }) .TransitionTo("Completed"); } } ``` ### Schedule in a lifecycle action Lifecycle descriptors (actions that run on saga creation, completion, or finalization) also support scheduling: C# ``` x.WhenCompleted() .ScheduledPublish( TimeSpan.FromDays(7), state => new OrderFeedbackRequestEvent { OrderId = state.OrderId }); ``` Both `ScheduledPublish` and `ScheduledSend` are available on `ISagaTransitionDescriptor` and `ISagaLifeCycleDescriptor`. The factory receives the current saga state and returns the message to schedule. For automatic saga timeouts that cancel themselves on completion, see [Timeouts](https://chillicream.com/docs/mocha/sagas#timeouts) in the Sagas guide. See [Sagas](https://chillicream.com/docs/mocha/sagas) for the full saga configuration guide. ## Troubleshooting **Scheduled messages are not being delivered.**Check that the background worker is running. Look for `Scheduler sleeping until ...` log entries at `Information` level. If there are no log entries, verify that `UsePostgresScheduling()` is registered in your service configuration. This applies to InMemory and RabbitMQ - neither has a scheduling store of its own, so both need the fallback to deliver scheduled messages at all. **Messages are delivered immediately instead of at the scheduled time.**Messages scheduled for a time in the past are dispatched immediately. Verify that your `ScheduledTime` is in the future. **"Could not deserialize message body" errors in logs.**The dispatcher could not parse the stored envelope. This can happen if the message type was renamed or removed after the message was scheduled. The dispatcher drops messages it cannot deserialize and logs at `Critical` level. **Scheduled messages fail repeatedly.**The dispatcher records each failure in the `last_error` column and retries with exponential backoff. After 10 attempts, the message is no longer eligible for dispatch. Query the `scheduled_messages` table and inspect the `last_error` column for diagnostics: SQL ``` SELECT id, scheduled_time, times_sent, last_error FROM scheduled_messages WHERE times_sent >= max_attempts; ``` **Multiple service instances dispatch the same message.**This does not happen. The dispatcher uses row-level locking to ensure each message is processed by exactly one instance. **Cancellation returns false even though I have a valid token.**The message was already dispatched before the cancellation request reached the store. Once the background worker picks up a message and delivers it, the row is deleted and cancellation is no longer possible. With the Azure Service Bus transport, the broker returns `MessageNotFound` once the scheduled message has been enqueued for delivery, which surfaces here as `false`. If you need a wider cancellation window, schedule messages further in the future or check `SchedulingResult.IsCancellable` to confirm the infrastructure supports cancellation. **`SchedulePublishAsync`/`ScheduleSendAsync` throws `NotSupportedException` instead of returning a result.**No scheduled message store is available for the current transport. `SchedulingResult.IsCancellable` is `true` whenever a call succeeds, there is no store that persists a message without returning a cancellable token. Configure `UsePostgresScheduling()` with an EF Core DbContext as a fallback, or use the PostgreSQL or [Azure Service Bus](https://chillicream.com/docs/mocha/transports/azure-service-bus) transport, which registers its own store automatically. ## Next steps - [**Reliability**](https://chillicream.com/docs/mocha/reliability) \- Configure the transactional outbox and inbox for guaranteed delivery alongside scheduling. - [**Sagas**](https://chillicream.com/docs/mocha/sagas) \- Build multi-step workflows with state machines, timeouts, and scheduled side effects. - [**PostgreSQL Transport**](https://chillicream.com/docs/mocha/transports/postgres) \- Set up the Postgres transport that powers durable scheduling. - [**Messaging Patterns**](https://chillicream.com/docs/mocha/messaging-patterns) \- Understand the difference between publish (fan-out) and send (point-to-point) when choosing which scheduling method to use. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/scheduling.md) Maintained by ChilliCream. Last updated on **August 18, 2026** by **PascalSenn** --- # Transports - Mocha > Understand how transports move messages in Mocha, how the transport abstraction works, and how to choose between InMemory, PostgreSQL, RabbitMQ, and Azure Service Bus. Canonical source: https://chillicream.com/docs/mocha/transports A transport is the infrastructure layer that connects Mocha to a message broker. It manages connections, provisions topology (exchanges, queues, bindings), and handles the low-level details of dispatching and receiving messages. You write handlers and publish messages. The transport handles the rest. The transport abstraction means your handlers, patterns, and pipeline are identical regardless of which broker you use. Only the infrastructure changes. Swap `.AddInMemory()` for `.AddPostgres()`, `.AddRabbitMQ()`, or `.AddAzureServiceBus()` and your application code stays unchanged. This portability is the core value of the [Message Channel](https://www.enterpriseintegrationpatterns.com/patterns/messaging/MessageChannel.html) pattern: the sender and receiver are decoupled from the physical infrastructure that carries the message. Mocha ships with four transports: | Transport | Package | Use case | | --------------------- | ------------------------------- | ------------------------------------------------------------ | | **InMemory** | Mocha.Transport.InMemory | Development, testing, single-process scenarios | | **PostgreSQL** | Mocha.Transport.Postgres | Database-backed messaging when you already operate Postgres | | **RabbitMQ** | Mocha.Transport.RabbitMQ | Production, distributed systems, multi-service architectures | | **Azure Service Bus** | Mocha.Transport.AzureServiceBus | Managed cloud messaging on Azure | ## Add a transport Every transport implements the same `MessagingTransport` base class. The transport is always the last call in the builder chain: C# ``` // InMemory - zero configuration builder.Services .AddMessageBus() .AddEventHandler() .AddInMemory(); ``` C# ``` // RabbitMQ - production-ready builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(); ``` C# ``` // PostgreSQL - database-backed transport builder.Services .AddMessageBus() .AddEventHandler() .AddPostgres("Host=localhost;Database=mocha_messaging;Username=postgres;Password=postgres"); ``` C# ``` // Azure Service Bus - managed cloud messaging builder.Services .AddMessageBus() .AddEventHandler() .AddAzureServiceBus(connectionString); ``` Each `Add{Transport}()` method registers a transport instance, applies default conventions, and wires up the middleware pipelines. ## Choose a transport Use this decision matrix to pick the right transport. Each column includes trade-offs - choose the one whose trade-offs you can accept: | Criterion | InMemory | PostgreSQL | RabbitMQ | Azure Service Bus | | ------------------ | -------------------------------------- | ------------------------------------------- | ------------------------------------------- | ------------------------------------- | | Setup effort | None, zero dependencies | Requires a PostgreSQL database | Requires a running broker | Azure subscription and namespace | | Message durability | **Messages lost on process exit** | Messages are stored in database tables | Messages survive broker restarts | Durable, broker-managed | | Multi-process | **Single process only** | Multiple services sharing the same database | Multiple services, multiple instances | Multiple services, multiple instances | | Request/reply | Supported | Supported | Supported | Supported | | Native scheduling | None, requires UsePostgresScheduling() | Yes, built-in and cancellable | None, requires UsePostgresScheduling() | Yes, durable and cancellable | | Operational cost | None | Database capacity, migrations, monitoring | Broker infrastructure, monitoring, upgrades | Pay-per-use Azure resource | | Network latency | None, in-process | Database round trip | Broker round trip | Cloud network round trip | **InMemory limitations:** Because all messages live in process memory, the InMemory transport cannot model multi-service fan-out, cannot survive process restarts, and does not exercise RabbitMQ-specific behavior like connection recovery, acknowledgement semantics, or topology conflicts. **PostgreSQL trade-offs:** PostgreSQL is a good fit when you already operate PostgreSQL and want database-backed messaging without another broker. It favors operational simplicity and transactional consistency over dedicated broker throughput. **RabbitMQ operational cost:** RabbitMQ requires expertise to operate in production - cluster management, disk and memory alarms, queue type selection, and monitoring. Use a managed broker (CloudAMQP, Amazon MQ) if you want to reduce operational burden. **Azure Service Bus trade-offs:** Azure Service Bus is fully managed and durable, with native scheduling and cancellation. It is a strong fit for Azure-hosted workloads, while cost and available throughput depend on the namespace SKU. ## Scope and middleware Mocha uses a three-level feature scope: **bus**, **transport**, and **endpoint**. Features set at the bus level apply to all transports. Features set at the transport level override bus-level defaults for that transport. Features set at the endpoint level override both. ``` Bus scope (global defaults) └── Transport scope (transport-specific overrides) └── Endpoint scope (endpoint-specific overrides) ``` To add middleware at the transport level: C# ``` builder.Services .AddMessageBus() .AddRabbitMQ(transport => { // Add dispatch middleware scoped to this transport transport.UseDispatch(myDispatchMiddleware); // Add receive middleware scoped to this transport transport.UseReceive(myReceiveMiddleware); // Insert middleware relative to existing ones transport.UseReceive(myMiddleware, after: "ConcurrencyLimiter"); transport.UseDispatch(myMiddleware, before: "Serialization"); }); ``` This scoping model lets you run different middleware configurations per transport without affecting other transports in a multi-transport setup. ## Customize queues and binding Use `transport.Queue("name")` when you need to customize receive topology. The queue builder is the primary surface for custom queue names, multiple handlers on one queue, queue-level settings, and handler assignment. C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(transport => { transport.BindExplicitly(); transport.Queue("order-processing") .BindImplicitly() .Handler(); }); ``` Calling `Queue("name")` without a handler, consumer, or `Receives()` declares only the queue. As soon as you add a handler, consumer, or received message type, Mocha materializes a receive endpoint for that queue. ## Control implicit and explicit binding By default, transports bind handlers implicitly using naming conventions: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(transport => { transport.BindImplicitly(); // This is the default }); ``` With implicit transport binding, registered handlers are auto-discovered, assigned to convention-named queues, and connected to convention-derived exchange, topic, or subscription bindings. Use `BindExplicitly()` at the transport scope when the queues you configure should be the complete receive topology: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(transport => { transport.BindExplicitly(); transport.Queue("order-events") .BindImplicitly() .Handler(); }); ``` Use `BindImplicitly()` on the queue when you want a custom queue name but still want Mocha to generate the source bindings for that queue's handlers. Use `BindExplicitly()` on the queue when you provide those source bindings yourself, for example with `BindFrom(...)` or transport-specific topology declarations. ## Claim handlers for a transport When you need to keep the convention-derived queue name and only tune a single handler endpoint, use `transport.Handler()` at the end of the transport configuration. This claims the handler for the transport and returns a descriptor that lets you configure the endpoint through `ConfigureEndpoint()`: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(transport => { transport.Handler() .ConfigureEndpoint(e => e.MaxPrefetch(50).MaxConcurrency(10)); }); ``` The handler still gets a convention-named endpoint - `Handler()` does not change the name. It gives you a handle to configure that endpoint without needing `BindExplicitly()` or knowing the endpoint name. For raw `IConsumer` types, the equivalent is `transport.Consumer()`: C# ``` transport.Consumer() .ConfigureEndpoint(e => e.MaxConcurrency(3)); ``` `Handler()` and `Consumer()` are the primary tool for multi-transport routing. When a handler is claimed by a transport, it is bound to that transport regardless of which transport is marked as the default. ## Use multiple transports You can register multiple transports and route specific handlers to specific transports. Mark one transport as the default with `.IsDefaultTransport()`. Any handler not explicitly claimed by another transport is bound to the default: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEventHandler() .AddRabbitMQ(r => r.IsDefaultTransport()) // default for unclaimed handlers .AddInMemory(m => m.Handler()); // AuditHandler claimed by InMemory // OrderPlacedEventHandler → RabbitMQ (default, implicit) // AuditHandler → InMemory (claimed) ``` You can also use the queue descriptor with explicit binding: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEventHandler() // Default transport for most messages .AddRabbitMQ() // High-throughput transport for click-stream data .AddInMemory(transport => { transport.BindExplicitly(); transport.Queue("click-stream") .BindImplicitly() .Handler(); }); ``` Each transport manages its own connections, topology, and middleware pipeline independently. A handler bound to one transport does not consume from another transport's endpoints. ## Next steps - [InMemory Transport](https://chillicream.com/docs/mocha/transports/in-memory) \- Set up the InMemory transport for development and testing. - [PostgreSQL Transport](https://chillicream.com/docs/mocha/transports/postgres) \- Configure database-backed messaging with PostgreSQL. - [RabbitMQ Transport](https://chillicream.com/docs/mocha/transports/rabbitmq) \- Configure the RabbitMQ transport for production deployments. - [Azure Service Bus Transport](https://chillicream.com/docs/mocha/transports/azure-service-bus) \- Configure managed cloud messaging on Azure. > **Runnable example:** [MultiTransport](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Transports/MultiTransport) [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/transports/index.md) Maintained by ChilliCream. Last updated on **August 18, 2026** by **PascalSenn** --- # Azure Service Bus Transport - Mocha > Configure the Azure Service Bus transport in Mocha for managed cloud messaging with native scheduling, dead-letter forwarding, and Microsoft Entra ID authentication. Canonical source: https://chillicream.com/docs/mocha/transports/azure-service-bus The Azure Service Bus (ASB) transport connects Mocha to a fully managed Azure messaging namespace. It provisions queues, topics, and subscriptions automatically, dispatches publishes through topics and sends through queues, and exposes ASB-specific primitives - native scheduling with cancellation, broker dead-letter forwarding, and lock-renewal-aware acknowledgement. When you run on Azure and want a managed broker without operating the infrastructure yourself, this is the transport to use. ## Set up the Azure Service Bus transport By the end of this section, you will have a Mocha bus connected to Azure Service Bus with automatic topology provisioning. ### Install the package Bash ``` dotnet add package Mocha.Transport.AzureServiceBus ``` ### Register with a connection string The simplest setup passes a Service Bus connection string directly: C# ``` using Mocha; using Mocha.Transport.AzureServiceBus; var builder = WebApplication.CreateBuilder(args); builder.Services .AddMessageBus() .AddEventHandler() .AddAzureServiceBus("Endpoint=sb://.servicebus.windows.net/;SharedAccessKeyName=...;SharedAccessKey=..."); var app = builder.Build(); app.Run(); ``` ### Register with a fully qualified namespace and a token credential Use [Microsoft Entra ID authentication](https://learn.microsoft.com/azure/service-bus-messaging/service-bus-authentication-and-authorization) with a [managed identity](https://learn.microsoft.com/azure/service-bus-messaging/service-bus-managed-service-identity), [workload identity](https://learn.microsoft.com/azure/aks/workload-identity-overview), or any other [TokenCredential](https://learn.microsoft.com/dotnet/api/azure.core.tokencredential) instead of a [shared access key](https://learn.microsoft.com/azure/service-bus-messaging/service-bus-sas): C# ``` using Azure.Identity; using Mocha; using Mocha.Transport.AzureServiceBus; var builder = WebApplication.CreateBuilder(args); builder.Services .AddMessageBus() .AddEventHandler() .AddAzureServiceBus(transport => { transport.Namespace( "example.servicebus.windows.net", new DefaultAzureCredential()); }); var app = builder.Build(); app.Run(); ``` The example uses [DefaultAzureCredential](https://learn.microsoft.com/dotnet/api/azure.identity.defaultazurecredential), which supports local developer credentials as well as credentials provided by Azure hosting environments. ### Register with .NET Aspire When using [.NET Aspire](https://aspire.dev/integrations/cloud/azure/azure-service-bus/), define a Service Bus resource in your AppHost and reference it from each service. Aspire injects `MESSAGING_CONNECTIONSTRING` for the local emulator and `MESSAGING_FULLYQUALIFIEDNAMESPACE` for an Azure-hosted namespace. The emulator serves messaging and management on different dynamically allocated endpoints. Pass its management endpoint to each service so Mocha can auto-provision topology locally: C# ``` using Aspire.Hosting.ApplicationModel; // AppHost var serviceBus = builder .AddAzureServiceBus("messaging") .RunAsEmulator(); var administrationEndpoint = serviceBus.GetEndpoint("emulatorhealth"); var administrationConnectionString = ReferenceExpression.Create( $"Endpoint=sb://{administrationEndpoint.Property(EndpointProperty.HostAndPort)};SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"); builder .AddProject("order-service") .WithReference(serviceBus) .WithEnvironment( "ConnectionStrings__messaging-administration", administrationConnectionString) .WaitFor(serviceBus); ``` Install Aspire's Azure Service Bus client integration in each service: Bash ``` dotnet add package Aspire.Azure.Messaging.ServiceBus ``` Then register Aspire's messaging client. For the emulator, pass only its administration connection string to Mocha: C# ``` builder.AddAzureServiceBusClient("messaging"); var administrationConnectionString = builder.Configuration.GetConnectionString("messaging-administration"); builder.Services .AddMessageBus() .AddEventHandler() .AddAzureServiceBus(transport => { // Aspire does not register the emulator's separate administration client. if (administrationConnectionString is not null) { transport.AdministrationConnectionString(administrationConnectionString); } }); ``` With this configuration, Mocha resolves Aspire's singleton `ServiceBusClient` from dependency injection. The additional connection string creates only the `ServiceBusAdministrationClient` needed because the emulator exposes management on a separate endpoint. In Azure, the administration connection string is absent, Aspire uses `DefaultAzureCredential`, and runtime provisioning remains disabled. Explicit `ConnectionString(...)` or `Namespace(...)` transport configuration takes precedence over the client registered in dependency injection. The emulator supports runtime entity management through [ServiceBusAdministrationClient](https://learn.microsoft.com/dotnet/api/azure.messaging.servicebus.administration.servicebusadministrationclient), so no queues, topics, or subscriptions need to be pre-declared in its configuration. For production, grant the application an appropriate Azure Service Bus data role and provision topology through Aspire or another infrastructure deployment process. ### Verify it works For a one-off smoke test, replace `app.Run()` with a start-publish-stop sequence. `IMessageBus` is registered as scoped, so resolve it from a service scope: C# ``` await app.StartAsync(); using (var scope = app.Services.CreateScope()) { var bus = scope.ServiceProvider.GetRequiredService(); await bus.PublishAsync( new OrderPlacedEvent { OrderId = Guid.NewGuid(), CustomerId = "customer-1", TotalAmount = 99.99m }, CancellationToken.None); } await app.StopAsync(); ``` Check your application logs. You should see the handler process the event. You can also inspect the auto-provisioned topics, subscriptions, and queues in the Azure portal under your Service Bus namespace. ## How topology works The transport maps Mocha's routing model onto Azure Service Bus [queues, topics, and subscriptions](https://learn.microsoft.com/azure/service-bus-messaging/service-bus-queues-topics-subscriptions): **Events (publish/subscribe):** Each event type gets a topic. Each subscribing receive endpoint gets a queue and a forwarding subscription that delivers messages from the topic into the queue. Publishing sends the message to the topic, which fans it out to all forwarded subscriber queues. **Commands (send):** Each command type gets a queue named after the command. The sender writes directly to that queue - there is no intermediate topic on the send path. The receiving handler binds to the same queue, so a single message instance is delivered to exactly one handler. **Request/reply:** The transport creates a temporary reply queue per service instance (`response-{instanceId}`). The reply address is embedded in the request message so the responder knows where to send the reply. Reply queues are auto-provisioned with a 24-hour idle-deletion policy. While the service is running, the transport periodically peeks at its reply queue to keep it alive even when no replies are in flight. After the service stops, Azure Service Bus removes the idle queue automatically. **Scheduled messages:** Azure Service Bus holds scheduled messages in the broker through its native scheduling API. Mocha returns a transport-scoped cancellation token containing the target entity and broker sequence number, which it uses to cancel the message without a separate scheduler or database. See [Scheduling](https://chillicream.com/docs/mocha/scheduling) for the common API and cancellation behavior. ### Default topology for handlers Each handler-bound receive endpoint provisions three queues by convention - the main queue plus an `_error` queue (handler exceptions) and a `_skipped` queue (no matching consumer): | Queue | Purpose | | ---------------------------- | ---------------------------------------------------------- | | {service}.{handler} | Main inbound queue for the handler | | {service}.{handler}\_error | Destination of ReceiveFaultMiddleware (handler exceptions) | | {service}.{handler}\_skipped | Destination of ReceiveDeadLetterMiddleware (unmatched) | This naming is identical across transports - see [Routing and Endpoints](https://chillicream.com/docs/mocha/routing-and-endpoints) for the full convention. ## Configure transport-level defaults You can set defaults that apply to all auto-provisioned queues, topics, and endpoints: C# ``` builder.Services .AddMessageBus() .AddAzureServiceBus(transport => { transport.ConnectionString(connectionString); transport.ConfigureDefaults(defaults => { defaults.Queue.MaxDeliveryCount = 5; defaults.Queue.LockDuration = TimeSpan.FromMinutes(1); defaults.Queue.DefaultMessageTimeToLive = TimeSpan.FromDays(7); defaults.Queue.DeadLetteringOnMessageExpiration = true; }); }); ``` Available queue defaults: | Property | Type | Description | | -------------------------------- | --------- | ------------------------------------------------------------------------------- | | AutoProvision | bool? | Whether queues are auto-provisioned at startup | | AutoDeleteOnIdle | TimeSpan? | Idle window before the broker may delete the queue | | LockDuration | TimeSpan? | How long the broker holds a peek-lock on a delivered message | | MaxDeliveryCount | int? | Attempts before the broker dead-letters the message (MaxDeliveryCountExceeded) | | DefaultMessageTimeToLive | TimeSpan? | TTL applied to messages that do not specify their own | | MaxSizeInMegabytes | long? | Maximum queue size in megabytes | | RequiresSession | bool? | Whether the queue requires sessions (immutable after creation) | | EnablePartitioning | bool? | Whether the queue is partitioned (immutable after creation) | | ForwardDeadLetteredMessagesTo | string? | Auto-forward target for the entity's $DeadLetterQueue | | DeadLetteringOnMessageExpiration | bool? | Whether expired messages are moved to $DeadLetterQueue instead of being dropped | Available topic defaults: | Property | Type | Description | | ----------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------- | | AutoProvision | bool? | Whether topics are auto-provisioned at startup | | AutoDeleteOnIdle | TimeSpan? | Idle window before the broker may delete the topic | | DefaultMessageTimeToLive | TimeSpan? | TTL applied to messages that do not specify their own | | MaxSizeInMegabytes | long? | Maximum topic size in megabytes | | EnablePartitioning | bool? | Whether the topic is partitioned | | RequiresDuplicateDetection | bool? | Whether the broker rejects [duplicate message identifiers](https://learn.microsoft.com/azure/service-bus-messaging/duplicate-detection) | | DuplicateDetectionHistoryTimeWindow | TimeSpan? | Window during which duplicate message identifiers are tracked | | SupportOrdering | bool? | Whether subscriptions support ordered forwarding | Available receive-endpoint defaults: | Property | Type | Description | | -------------- | ---- | -------------------------------------------------------------- | | PrefetchCount | int? | Number of messages prefetched from the broker | | MaxConcurrency | int? | Maximum number of messages processed concurrently per endpoint | Defaults never override explicitly configured values. If you call `MaxDeliveryCount(...)` on a specific queue, the per-queue value wins. ## Configure message properties per type Azure Service Bus messages carry [native broker properties](https://learn.microsoft.com/azure/service-bus-messaging/service-bus-messages-payloads) such as `SessionId`, `PartitionKey`, and `ReplyToSessionId`. These properties drive session affinity, partition pinning, and request/reply correlation. When a value depends on the payload, register a typed extractor next to the message contract and Mocha writes the result to the outbound `ServiceBusMessage`. ### Configure session affinity with `UseAzureServiceBusSessionId` C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddMessage(m => m // Session per order: every message for the same OrderId // is delivered in order to a single session receiver. .UseAzureServiceBusSessionId(msg => msg.OrderId)) .AddAzureServiceBus(transport => { transport.ConnectionString(connectionString); // The destination queue must be created with RequiresSession = true. transport.DeclareQueue("orders") .RequiresSession(true); }); ``` Use `UseAzureServiceBusSessionId()` when the destination queue or subscription has `RequiresSession = true`, or when you need per-session FIFO processing. The broker routes every message with the same `SessionId` to the same session receiver, which holds an exclusive lock and consumes them in arrival order. See Microsoft Learn on [message sessions](https://learn.microsoft.com/azure/service-bus-messaging/message-sessions) for the complete FIFO and request-response patterns. The extractor runs at dispatch time for each message. It receives the message instance and returns the session identifier string. Return `null` to publish without a `SessionId`. On a queue or topic that is both partitioned and session-aware, the broker uses the `SessionId` as the partition key - Mocha mirrors that by defaulting `PartitionKey = SessionId` when no partition-key extractor is configured, so you do not need to set both. Warning Azure Service Bus rejects a message without a `SessionId` when it is sent to a session-enabled queue or topic subscription. Ensure the extractor always returns a value for messages routed to session-enabled entities. ### Configure partitioning with `UseAzureServiceBusPartitionKey` C# ``` builder.Services .AddMessageBus() .AddMessage(m => m // Pin every tenant's events to the same partition for // in-partition ordering on a non-session-aware entity. .UseAzureServiceBusPartitionKey(msg => msg.TenantId)) .AddAzureServiceBus(transport => { transport.ConnectionString(connectionString); transport.DeclareQueue("tenant-events") .EnablePartitioning(true); }); ``` Use `UseAzureServiceBusPartitionKey()` on partitioned queues and topics when related messages must land on the same broker partition, or when transactional sends must share a partition. A partition key preserves broker submission order within that partition, but it does not by itself guarantee consumer processing order when messages are processed concurrently. Use sessions when strict per-key processing order is required. See Microsoft Learn on [partitioned queues and topics](https://learn.microsoft.com/azure/service-bus-messaging/service-bus-partitioning) and [message sequencing](https://learn.microsoft.com/azure/service-bus-messaging/message-sequencing). When a `SessionId` is also configured on the same message type, the broker requires `PartitionKey == SessionId`. Mocha enforces this at dispatch with a fail-fast check and throws: ``` PartitionKey must equal SessionId when both are set on an Azure Service Bus message. ``` This `InvalidOperationException` surfaces before the message reaches the broker, so a mismatch never costs you a round trip. If you want the automatic `PartitionKey = SessionId` behavior, configure only `UseAzureServiceBusSessionId()` and let the transport default the partition key. Note When the extractor returns `null`, no partition key is set and Service Bus picks a partition with an internal round-robin - use this only when you do not need per-key ordering. ### Configure reply correlation with `UseAzureServiceBusReplyToSessionId` C# ``` public sealed class GetOrderRequest : IEventRequest { public required string OrderId { get; init; } // Unique per requester instance - typically a process GUID. public required string RequesterId { get; init; } } builder.Services .AddMessageBus() .AddMessage(m => m // Tell the responder which session ID to apply when the // configured reply destination is session-enabled. .UseAzureServiceBusReplyToSessionId( req => req.RequesterId)) .AddAzureServiceBus(transport => { transport.ConnectionString(connectionString); }); ``` Use `UseAzureServiceBusReplyToSessionId()` to put the native `ReplyToSessionId` property on an outbound request. When Mocha dispatches the response, it promotes the received value to the reply's `SessionId` and `PartitionKey`. This preserves the native Azure Service Bus correlation contract for applications that provide a session-enabled reply destination. The extractor lives on the request type, not the response - the requester is the one that tells the responder where replies should land. Note Mocha's default temporary reply queue is created per service instance and is not session-enabled. Configuring `ReplyToSessionId` does not turn that queue into a shared, multiplexed session queue. Native session multiplexing requires an explicitly managed session-enabled reply destination and receiver. Tip `ReplyToSessionId` is capped at **128 characters**. Use a stable identifier per requester instance (a GUID created at process start is idiomatic) so replies reach the right receiver even after reconnects. ### Override per dispatch via headers C# ``` public static class TenantAwareBusExtensions { // Override the configured session ID for this publish only. public static ValueTask PublishForTenantAsync( this IMessageBus bus, T message, string tenantId, CancellationToken cancellationToken) where T : class { return bus.PublishAsync( message, new PublishOptions { Headers = new Dictionary { [AzureServiceBusMessageHeaders.SessionId] = tenantId } }, cancellationToken); } } ``` Native property values supplied through `PublishOptions.Headers` take precedence over extractors registered on the message type. This lets send-site code override a per-type default for one dispatch without reconfiguring the bus. The supported headers are defined as string constants on `AzureServiceBusMessageHeaders`: | Constant | Header key | | ---------------------------------------------- | --------------------- | | AzureServiceBusMessageHeaders.SessionId | x-session-id | | AzureServiceBusMessageHeaders.PartitionKey | x-partition-key | | AzureServiceBusMessageHeaders.ReplyToSessionId | x-reply-to-session-id | When a message is sent, these headers are mapped to native Service Bus fields and omitted from `ApplicationProperties`. On receive, Mocha restores the native values into the normalized envelope headers so subsequent dispatches can preserve session and partition affinity. ### Reference `SessionId`, `PartitionKey`, and `ReplyToSessionId` are each capped at **128 characters** by the broker. | Extension method | Sets on ServiceBusMessage | Header key | Gotcha | | ------------------------------------------------ | ------------------------- | --------------------- | --------------------------------------------------------------------------------------- | | UseAzureServiceBusSessionId(extractor) | SessionId | x-session-id | Defaults PartitionKey to the same value when no partition-key extractor is set. | | UseAzureServiceBusPartitionKey(extractor) | PartitionKey | x-partition-key | Must equal SessionId when both are set, else dispatch throws InvalidOperationException. | | UseAzureServiceBusReplyToSessionId(extractor) | ReplyToSessionId | x-reply-to-session-id | Configure on the request type, not the response. | Extractors are the right tool when the ASB property is derived from the payload. When you need to declare the entities they land on - session-aware queues, partitioned topics, or automatic forwarding targets - reach for [the topology builder](#declare-custom-topology) in the next section. ## Declare custom topology Mocha auto-provisions topology by default. To declare additional topics, queues, or subscriptions: C# ``` builder.Services .AddMessageBus() .AddAzureServiceBus(transport => { transport.ConnectionString(connectionString); transport.DeclareTopic("order-events"); transport.DeclareQueue("billing-orders") .MaxDeliveryCount(5) .LockDuration(TimeSpan.FromMinutes(1)); transport.DeclareSubscription("order-events", "billing-orders"); }); ``` To bind handlers explicitly to specific queues: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddAzureServiceBus(transport => { transport.ConnectionString(connectionString); transport.BindExplicitly(); transport.DeclareQueue("process-order"); transport.Endpoint("process-order-ep") .Queue("process-order") .Handler(); transport.DispatchEndpoint("send-demo") .ToQueue("process-order") .Send(); }); ``` ### Choose a queue configuration API Use `Queue(name)` for application queues. It combines the queue declaration with receive endpoint configuration, so handlers, consumers, bindings, middleware, concurrency, and broker settings can be configured in one place: C# ``` transport.Queue("process-order") .Handler() .MaxDeliveryCount(5) .MaxConcurrency(8); ``` Use `DeclareQueue(name)` for low-level broker topology that does not represent an application receive endpoint. It configures the Azure Service Bus queue resource without attaching handlers or receive middleware. ### Configure receive concurrency and sessions Configure broker [prefetch](https://learn.microsoft.com/azure/service-bus-messaging/service-bus-performance-improvements), message concurrency, and [lock renewal](https://learn.microsoft.com/azure/service-bus-messaging/message-transfers-locks-settlement) on an application queue: C# ``` transport.Queue("process-order") .PrefetchCount(32) .MaxConcurrency(8) .MaxAutoLockRenewalDuration(TimeSpan.FromMinutes(10)); ``` For a session-enabled queue, configure the number of simultaneously locked sessions independently from the number of concurrent calls within each session: C# ``` transport.Queue("tenant-orders") .RequiresSession(true) .MaxConcurrentSessions(8) .MaxConcurrentCallsPerSession(1) .SessionIdleTimeout(TimeSpan.FromSeconds(30)); ``` `MaxConcurrentCallsPerSession` defaults to `1` to preserve in-session processing order. When `MaxConcurrentSessions` is not specified, `MaxConcurrency` determines the maximum number of concurrently locked sessions. Session-only settings cause startup to fail when applied to a non-session queue. Lock auto-renewal defaults to five minutes for both regular and session endpoints. ## Temporary receive endpoints Call `Temporary()` on a queue or receive endpoint descriptor to scope its backing queue to the lifetime of the consuming process: C# ``` transport.Queue($"tenant-events-{instanceId}") .Temporary() .Receives(); ``` `Temporary()` sets the queue's `AutoDeleteOnIdle` to a 24-hour default. Use `Temporary(TimeSpan idleTimeout)` for a custom idle window; the broker enforces a five-minute minimum, and Mocha throws `ArgumentOutOfRangeException` for a shorter value. `AutoDeleteOnIdle` measures time since the queue was last accessed by a send or receive, not the age of any individual message on it - it is an entity idle timeout, not a message TTL. While the endpoint is running, a heartbeat periodically peeks the queue to reset that idle timer, so a queue with no message traffic stays alive for as long as its receive endpoint is active. The heartbeat interval is half the configured idle timeout: the 24-hour default produces a 12-hour heartbeat, and a 10-minute `Temporary(TimeSpan)` produces a 5-minute heartbeat. If the backing queue is already declared, for example through `DeclareQueue(...)`, with a conflicting `AutoDeleteOnIdle`, startup fails with an explicit error instead of silently discarding the `Temporary()` configuration. ### Forwarding subscription cleanup A temporary endpoint that subscribes to events uses the same convention-based forwarding subscription as any other subscribe endpoint. When the endpoint stops gracefully, Mocha removes the Mocha-owned, auto-provisioned convention forwarding subscriptions that target its queue. User-declared subscriptions, and subscriptions with auto-provisioning disabled, are left in place. Note This cleanup runs only on graceful shutdown. If the process crashes or is killed, the forwarding subscription is not removed and remains on the topic after its queue is eventually auto-deleted; remove it manually or through your own crash-recovery process. ## Control auto-provisioning When infrastructure is managed externally, for example through [Bicep](https://learn.microsoft.com/azure/templates/microsoft.servicebus/allversions), Terraform, or a CI/CD pipeline, disable auto-provisioning so the transport expects entities to already exist: C# ``` builder.Services .AddMessageBus() .AddAzureServiceBus(transport => { transport.ConnectionString(connectionString); transport.AutoProvision(false); }); ``` With auto-provisioning disabled, the transport will not call the management API to create topics, queues, or subscriptions. All entities must already exist on the namespace before the transport starts. Individual resources can opt back in via `.AutoProvision(true)` when most topology is managed externally but a few entities need to be created dynamically. ## Scheduling Azure Service Bus [schedules messages natively](https://learn.microsoft.com/azure/service-bus-messaging/message-sequencing). The dispatch endpoint calls `ServiceBusSender.ScheduleMessageAsync` and the broker holds the message until the scheduled time: C# ``` var result = await bus.SchedulePublishAsync( new PaymentReminderEvent { OrderId = orderId }, DateTimeOffset.UtcNow.AddHours(24), cancellationToken); if (result.IsCancellable) { // Persist the token alongside the order so we can cancel later await orders.SaveReminderTokenAsync(orderId, result.Token!, cancellationToken); } ``` Cancellation is supported natively: C# ``` await bus.CancelScheduledMessageAsync(reminderToken, cancellationToken); ``` The token identifies the transport owner, target entity, and broker-assigned sequence number. It can only be cancelled through the transport and namespace that created it. `CancelScheduledMessageAsync` revokes the message through the broker; if the message has already been dispatched or no longer exists, Mocha returns `false`. ASB supports both **native scheduling and native cancellation**. See [Scheduling](https://chillicream.com/docs/mocha/scheduling) for the full scheduling API. ## Dead-lettering The transport has two distinct failure destinations. For one-way messages, Mocha forwards exceptions that escape the receive pipeline to the configured fault queue. Azure Service Bus separately owns the entity's `$DeadLetterQueue`, which receives messages explicitly dead-lettered by a handler or moved there by broker rules. `UseNativeDeadLetterForwarding()` can forward messages from the broker dead-letter queue into Mocha's fault queue. ### 1\. One-way handler exception → `_error` queue When a one-way handler throws and the configured retry or redelivery policy does not recover, `ReceiveFaultMiddleware` catches the exception, attaches `fault-*` headers (exception type, message, stack trace, timestamp), and forwards the original envelope to the convention-named `{queue}_error` queue: C# ``` public class ProcessInvoiceHandler : IEventHandler { public ValueTask HandleAsync(ProcessInvoice message, CancellationToken ct) { // Throwing here forwards the message to {queue}_error throw new InvalidOperationException("Downstream service is unavailable."); } } ``` The acknowledgement middleware then completes the lock against the broker so the message does not redeliver. This is the path most applications use - it is consistent across all transports and works without ASB-specific code. For request messages, the fault middleware sends a negative acknowledgement to the response address instead of forwarding the message to `_error`. ### 2\. Broker-managed `$DeadLetterQueue` Messages enter the broker-managed dead-letter queue through broker rules or explicit settlement: | Condition | Reason code | | ----------------------- | ----------------------------- | | Delivery count exceeded | MaxDeliveryCountExceeded | | Message TTL expired | TTLExpiredException | | Explicit dead-letter | Reason supplied by the caller | These messages land in the entity's [$DeadLetterQueue](https://learn.microsoft.com/azure/service-bus-messaging/service-bus-dead-letter-queues) sub-entity (`{queue}/$DeadLetterQueue`), separate from Mocha's `_error` queue. To consolidate operations, opt the endpoint's queue into forwarding broker-dead-lettered messages into the Mocha-managed `_error` queue: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddAzureServiceBus(transport => { transport.ConnectionString(connectionString); transport.Handler() .ConfigureEndpoint(e => e.UseNativeDeadLetterForwarding()); }); ``` `UseNativeDeadLetterForwarding()` sets `ForwardDeadLetteredMessagesTo` to the endpoint's configured Azure Service Bus fault queue. By convention this is `{queueName}_error`, but a custom fault endpoint is respected. Messages in the broker dead-letter queue are forwarded into the same fault queue used by one-way handler exceptions, so operators have one place to look. If you have already configured `ForwardDeadLetteredMessagesTo("custom-target")` on the same queue, the transport surfaces a configuration conflict at provisioning - it will not silently override your choice. ## Next steps - [Transports Overview](https://chillicream.com/docs/mocha/transports) \- Understand the transport abstraction and lifecycle. - [Scheduling](https://chillicream.com/docs/mocha/scheduling) \- Schedule messages for future delivery and cancel them natively through Azure Service Bus. - [Routing and Endpoints](https://chillicream.com/docs/mocha/routing-and-endpoints) \- Understand how `_error` and `_skipped` endpoints fit the receive pipeline. - [Reliability](https://chillicream.com/docs/mocha/reliability) \- Configure fault handling, retries, the transactional outbox, and the idempotent inbox. - [Middleware and Pipelines](https://chillicream.com/docs/mocha/middleware-and-pipelines) \- Customize the receive and dispatch pipelines. > **Runnable example:** [AzureServiceBusTransport](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/AzureServiceBusTransport) > > **Multi-service demo:** The AzureServiceBusTransport example runs OrderService, ShippingService, and NotificationService against the local Azure Service Bus emulator orchestrated through .NET Aspire, demonstrating publish/subscribe, send, request/reply, sagas, and batch processing on a managed broker. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/transports/azure-service-bus.md) Maintained by ChilliCream. Last updated on **August 23, 2026** by **PascalSenn** --- # InMemory Transport - Mocha > Set up the InMemory transport for development, testing, and single-process messaging scenarios in Mocha. Canonical source: https://chillicream.com/docs/mocha/transports/in-memory The InMemory transport routes messages through in-process topics and queues without any external broker. Messages never leave the application process and are never persisted to disk. ## Install and register **1.** Install the package: Bash ``` dotnet add package Mocha.Transport.InMemory ``` **2.** Register the transport: C# ``` using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Mocha; using Mocha.Transport.InMemory; var builder = Host.CreateApplicationBuilder(args); builder.Services .AddMessageBus() .AddEventHandler() .AddInMemory(); // One line - no configuration needed using var host = builder.Build(); await host.StartAsync(); var bus = host.Services.GetRequiredService(); await bus.PublishAsync( new OrderPlacedEvent { OrderId = Guid.NewGuid(), CustomerId = "customer-1", TotalAmount = 99.99m }, CancellationToken.None); ``` `.AddInMemory()` registers the transport with default conventions. Topics, queues, and bindings are created automatically based on your registered handlers and message types. ## Verify it works Run the application and check that your handler receives the event. `StartAsync()` starts the message bus runtime before the event is published. Because the InMemory transport dispatches within the same process, delivery is near-instantaneous and completes before `PublishAsync` returns. ## How the in-process topology works The InMemory transport replicates the same topic/queue/binding model that RabbitMQ uses - except everything lives inside your process memory. There is no broker, no network, and no serialization to disk. When you call `PublishAsync`, Mocha routes the message through an in-process topic to every queue bound to that topic, then invokes the handler bound to each queue. This model is why swapping `.AddInMemory()` for `.AddRabbitMQ()` requires no application code changes. The same topic/queue/binding topology that runs in-process with InMemory is provisioned on the broker when you switch to RabbitMQ. By default, the InMemory transport processes messages sequentially in the order they are published. Each message is delivered to its handler before the next one is dispatched. Note InMemory tests exercise handler logic and message routing, but not RabbitMQ-specific behavior such as topology conflicts, acknowledgement semantics, connection recovery, or quorum queue characteristics. For testing broker-specific behavior, use a real broker in a test container. The following shows the default topology Mocha creates when you register an event handler with the InMemory transport: ## Configure queues Use `transport.Queue("name")` when you want to choose the queue name, bind multiple handlers to one queue, or configure receive settings. The queue builder is the easiest way to customize in-memory topology because it combines queue declaration, handler binding, convention binding, and endpoint settings in one place. C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddInMemory(transport => { transport.BindExplicitly(); transport.Queue("order-processing") .BindImplicitly() .MaxConcurrency(5) .FaultEndpoint("order-errors") .Handler(); }); ``` `BindExplicitly()` at the transport scope means only queues you configure are used for receiving. `BindImplicitly()` on the queue tells Mocha to keep the convention-derived topic binding for the messages handled by that queue. Calling `Queue("name")` without `Handler()`, `Consumer()`, or `Receives()` declares only the in-memory queue. Add a handler, consumer, or received message type when the queue should also consume messages. C# ``` transport.Queue("audit") .Receives(); ``` `Queue(...)` and `Endpoint(...)` descriptors accept `Temporary()`. InMemory has no broker-side entity to auto-delete: a temporary queue lives exactly as long as any other InMemory queue, for the lifetime of the hosting process. Its lifecycle boundary is the runtime's own disposal, not a broker-managed idle timeout or lease. ## Declare topology resources The InMemory transport auto-generates topology from your handler registrations and queue builders. Caution Use `DeclareTopic()`, `DeclareQueue()`, and `DeclareBinding()` only when you need topology resources that are not represented by a receiving queue builder. For handler queues, prefer `transport.Queue("name")`. To declare infrastructure-only topology: C# ``` builder.Services .AddMessageBus() .AddInMemory(transport => { // Declare a topic transport.DeclareTopic("order-events"); // Declare a queue transport.DeclareQueue("billing-orders"); // Bind the topic to the queue transport.DeclareBinding("order-events", "billing-orders"); }); ``` ## Configure convention endpoints Use `transport.Handler()` at the end of the transport configuration when you want to keep the convention-derived queue name and only tune one handler endpoint: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddInMemory(transport => { transport.Handler() .ConfigureEndpoint(e => e.MaxConcurrency(5)); }); ``` The handler keeps its convention-derived endpoint name. `ConfigureEndpoint()` can be called multiple times - actions compose in declaration order: C# ``` transport.Handler() .ConfigureEndpoint(e => e.MaxConcurrency(5)) .ConfigureEndpoint(e => e.FaultEndpoint("order-errors")); ``` For raw `IConsumer` types, use `transport.Consumer()`: C# ``` transport.Consumer() .ConfigureEndpoint(e => e.MaxConcurrency(3)); ``` In a multi-transport setup, `Handler()` also claims the handler for this transport, overriding the default: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddEventHandler() .AddRabbitMQ(r => r.IsDefaultTransport()) .AddInMemory(m => m.Handler()); // OrderPlacedEventHandler → RabbitMQ (default) // AuditHandler → InMemory (claimed) ``` ## Next steps - [RabbitMQ Transport](https://chillicream.com/docs/mocha/transports/rabbitmq) \- Configure the RabbitMQ transport for production deployments. - [Handlers and Consumers](https://chillicream.com/docs/mocha/handlers-and-consumers) \- Learn about every handler type and how to register them. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/transports/in-memory.md) Maintained by ChilliCream. Last updated on **August 23, 2026** by **PascalSenn** --- # PostgreSQL Transport - Mocha > Configure the PostgreSQL transport in Mocha for database-backed messaging with automatic topology provisioning, LISTEN/NOTIFY signaling, and schema multi-tenancy. Canonical source: https://chillicream.com/docs/mocha/transports/postgres Experimental The PostgreSQL transport is currently in preview and its API may change in future releases. The PostgreSQL transport uses your existing database as a message broker. It stores messages, topics, queues, and subscriptions as rows in PostgreSQL tables, delivers messages using `SELECT ... FOR UPDATE SKIP LOCKED`, and signals consumers in real time with `LISTEN/NOTIFY`. When you already run PostgreSQL and want messaging without deploying a separate broker, this is the transport to use. **When to choose PostgreSQL over a dedicated broker:** - You already operate PostgreSQL and want to avoid additional infrastructure. - You need ACID guarantees between your domain writes and message dispatch (outbox pattern). - Your message volume fits within PostgreSQL's throughput (tens of thousands of messages per second). - You value operational simplicity over maximum throughput. **Trade-offs:** A dedicated message broker like RabbitMQ provides higher throughput, built-in clustering, and protocol-level flow control. The PostgreSQL transport trades peak throughput for operational simplicity and transactional consistency with your application data. ## Set up the PostgreSQL transport By the end of this section, you will have a Mocha bus connected to PostgreSQL with automatic topology provisioning and schema migration. ### Install the package Bash ``` dotnet add package Mocha.Transport.Postgres ``` ### Register with a connection string The simplest setup passes a connection string directly: C# ``` using Mocha; using Mocha.Transport.Postgres; var builder = WebApplication.CreateBuilder(args); builder.Services .AddMessageBus() .AddEventHandler() .AddPostgres("Host=localhost;Database=mocha_messaging;Username=postgres;Password=postgres"); var app = builder.Build(); app.Run(); ``` `.AddPostgres(connectionString)` creates an `NpgsqlDataSource` from the connection string, runs schema migrations on first use, and provisions topics, queues, and subscriptions for your registered handlers. ### Register with .NET Aspire When using .NET Aspire, define a PostgreSQL resource in your AppHost and reference it from each service: C# ``` // AppHost var postgres = builder.AddPostgres("postgres").WithPgAdmin(); var messagingDb = postgres.AddDatabase("messaging-db"); builder .AddProject("order-service") .WithReference(messagingDb) .WaitFor(messagingDb); ``` In each service, read the connection string from Aspire-injected configuration: C# ``` using Mocha; using Mocha.Transport.Postgres; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); var connectionString = builder.Configuration.GetConnectionString("messaging-db")!; builder.Services .AddMessageBus() .AddEventHandler() .AddPostgres(t => t.ConnectionString(connectionString)); var app = builder.Build(); app.Run(); ``` The Aspire component handles health checks, dashboard integration, and ensures the database is ready before the service starts. ### Register with advanced configuration For full control over transport settings, use the configuration delegate: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddPostgres(transport => { transport.ConnectionString(connectionString); transport.ConfigureDefaults(defaults => { defaults.Endpoint.MaxBatchSize = 20; defaults.Endpoint.MaxConcurrency = 8; }); }); ``` ### Verify it works Add an endpoint that publishes through the bus and verify the handler executes: C# ``` app.MapPost("/orders", async (IMessageBus bus) => { await bus.PublishAsync(new OrderPlacedEvent { OrderId = Guid.NewGuid(), CustomerId = "customer-1", TotalAmount = 99.99m }, CancellationToken.None); return Results.Ok(); }); ``` Send a POST request to `/orders` and check your application logs. You should see the handler process the event. You can also inspect the `mocha_message`, `mocha_topic`, and `mocha_queue` tables directly to see the auto-provisioned topology. ## How connections work The transport creates a single `NpgsqlDataSource` from your connection string, which manages an internal connection pool. All message operations (publish, send, read, delete) open a connection from this pool and return it when done. Two connection-string settings are applied automatically: | Setting | Value | Reason | | --------- | ----- | ----------------------------------------------------------------------- | | Enlist | false | Prevents messages from enlisting in ambient TransactionScope | | KeepAlive | 30 | Sends TCP keepalive packets every 30 seconds to detect dead connections | A separate long-lived connection is opened for `LISTEN/NOTIFY` signaling. This connection subscribes to the notification channel and dispatches queue-change signals to receive endpoints for low-latency message pickup. The transport checks database connectivity with a lightweight `SELECT 1` query with a 5-second timeout before polling. If the health check fails, the receive endpoint backs off with exponential delay. ## How topology works The PostgreSQL transport stores topology as database rows instead of broker-side resources. Topics, queues, and subscriptions map to rows in the `mocha_topic`, `mocha_queue`, and `mocha_queue_subscription` tables. **Events (publish/subscribe):** Each event type gets a topic row. Each service that subscribes creates a queue row and a subscription row linking the topic to the queue. Publishing inserts one message per subscribed queue using a single `INSERT...SELECT`, then calls `pg_notify` to signal each queue's consumers. **Commands (send):** Each command type gets a queue row. Sending inserts the message directly into the queue and calls `pg_notify`. **Request/reply:** The transport creates a temporary reply queue per service instance. The reply address is embedded in the request message headers so the responder knows where to send the reply. ### Default topology for event handlers When you register an event handler with `AddEventHandler()`, the transport creates: 1. A **topic** named after the message type (e.g., `order-placed-event`) 2. A **queue** named after the service and message type (e.g., `billing-service.order-placed-event`) 3. A **subscription** linking the topic to the queue Publishing fans out to all subscribed queues in a single SQL statement. Each subscriber processes its own copy independently. ### Default topology for send handlers When you register a request handler for send (fire-and-forget), the transport creates a single queue. Only one handler processes each message - this is the point-to-point guarantee. ## Configure transport-level defaults You can set defaults that apply to all auto-provisioned queues, topics, and endpoints. This is useful when you want consistent settings across all resources without configuring each one individually. Use `ConfigureDefaults` to set queue, topic, and endpoint defaults: C# ``` builder.Services .AddMessageBus() .AddPostgres(transport => { transport.ConnectionString(connectionString); transport.ConfigureDefaults(defaults => { // All queues will be auto-provisioned with auto-delete disabled defaults.Queue.AutoProvision = true; defaults.Queue.AutoDelete = false; // All topics will be auto-provisioned defaults.Topic.AutoProvision = true; // All endpoints will fetch 20 messages per batch // and process up to 8 concurrently defaults.Endpoint.MaxBatchSize = 20; defaults.Endpoint.MaxConcurrency = 8; }); }); ``` Available queue defaults: | Property | Type | Description | | ------------- | ----- | -------------------------------------------------------------- | | AutoProvision | bool? | Whether queues are auto-provisioned at startup (default: true) | | AutoDelete | bool? | Whether queues are auto-deleted when unused (default: false) | Available topic defaults: | Property | Type | Description | | ------------- | ----- | -------------------------------------------------------------- | | AutoProvision | bool? | Whether topics are auto-provisioned at startup (default: true) | Available endpoint defaults: | Property | Type | Description | | -------------- | ---- | ---------------------------------------------------------------------------- | | MaxBatchSize | int? | Maximum messages fetched per poll cycle (default: 10) | | MaxConcurrency | int? | Maximum messages processed in parallel (default: Environment.ProcessorCount) | Defaults never override explicitly configured values. If you configure an endpoint with a specific `MaxBatchSize`, that setting takes precedence over the transport default. ## Schema and multi-tenancy The transport stores all data in the `public` schema with a `mocha_` table prefix by default. Table and channel naming is controlled by the `PostgresSchemaOptions` class, which computes fully qualified names from two properties: | Property | Default | Description | | ----------- | --------- | ------------------------------------- | | Schema | "public" | The PostgreSQL schema name | | TablePrefix | "mocha\_" | The prefix applied to all table names | With the defaults, the transport creates the following tables: | Table | Purpose | | --------------------------------- | ----------------------------------- | | public.mocha\_topic | Topic definitions | | public.mocha\_queue | Queue definitions | | public.mocha\_queue\_subscription | Topic-to-queue subscriptions | | public.mocha\_message | Message storage | | public.mocha\_consumers | Consumer registration and heartbeat | | public.mocha\_migrations | Migration tracking | The LISTEN/NOTIFY channel is derived from the table prefix: `mocha_queue_changed`. Changing `Schema` and `TablePrefix` shifts all table names accordingly. For example, with `Schema = "tenant_a"` and `TablePrefix = "bus_"`, the tables become `tenant_a.bus_topic`, `tenant_a.bus_queue`, and so on, and the notification channel becomes `bus_queue_changed`. This enables multi-tenant deployments and coexistence with other applications in the same database. ### Schema migration The transport runs migrations automatically on first use. Migrations are protected by a PostgreSQL advisory lock (`pg_advisory_xact_lock`) to prevent concurrent migration attempts from multiple service instances starting simultaneously. Each migration is tracked in the migrations table and is idempotent - running the same migration twice has no effect. The migration creates the schema if it does not exist, then applies each pending migration in order within a single transaction. Warning The advisory lock ID is fixed. If you run multiple independent Mocha transports in the same PostgreSQL cluster with different table prefixes, they share the same advisory lock. This is safe - it serializes migrations but does not block normal message operations. ## Configure queues Use `transport.Queue("name")` when you need to customize a PostgreSQL queue. The queue builder is the primary API for queue names, auto-provisioning, auto-delete, handler assignment, source bindings, batch size, concurrency, fault queues, and skipped queues. C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddPostgres(transport => { transport.ConnectionString(connectionString); transport.BindExplicitly(); transport.Queue("process-order") .BindImplicitly() .AutoProvision() .AutoDelete(false) .MaxBatchSize(20) .MaxConcurrency(10) .Handler(); }); ``` `BindExplicitly()` at the transport scope means only queues you configure are used for receiving. `BindImplicitly()` on the queue tells Mocha to generate the convention-derived topic subscriptions for the messages handled by that queue. Calling `Queue("name")` without `Handler()`, `Consumer()`, or `Receives()` declares only the PostgreSQL queue row. Add a handler, consumer, or received message type when the queue should also consume messages. C# ``` transport.Queue("audit-log") .AutoProvision(false) .AutoDelete(false); ``` For custom source topology, bind the queue from a specific topic: C# ``` transport.Queue("billing-orders") .BindExplicitly() .BindFrom(new Uri("topic:order-events")) .Handler(); ``` `BindExplicitly()` on the queue suppresses convention-derived topic subscriptions for that queue, so only the `BindFrom(...)` sources are used. ## Declare topology resources Mocha auto-provisions topology from registered handlers and queue builders by default. Caution Use `DeclareTopic()`, `DeclareQueue()`, and `DeclareSubscription()` only when you need infrastructure topology that is not represented by a queue builder, or when you are aligning with topology managed outside of Mocha. For handler queues, prefer `transport.Queue("name")`. To declare infrastructure-only topology: C# ``` builder.Services .AddMessageBus() .AddPostgres(transport => { transport.ConnectionString(connectionString); transport.DeclareTopic("order-events"); transport.DeclareQueue("billing-orders"); transport.DeclareSubscription("order-events", "billing-orders"); }); ``` All declared topology is provisioned when the transport starts, before receive endpoints begin consuming. ## Temporary receive endpoints Call `Temporary()` on a queue or receive endpoint descriptor to scope its backing queue to the lifetime of the consuming process: C# ``` transport.Queue($"tenant-events-{instanceId}") .Temporary() .Receives(); ``` `Temporary()` sets the queue row's `AutoDelete` to `true` and links it to the registering consumer's row in `mocha_consumers`. There is no separate lease mechanism for temporary queues - they reuse the consumer heartbeat and expiry ownership described in [Background maintenance tasks](#background-maintenance-tasks). When that consumer's row is removed, whether through normal shutdown cleanup or through the expired-consumer cleanup task after a missed heartbeat, the `CASCADE` foreign key removes the queue along with any messages still on it. If the queue already declares `AutoDelete(false)` explicitly, `Temporary()` fails startup with an explicit configuration error instead of silently overriding it. ## Control auto-provisioning By default, the transport auto-provisions all topology resources (topics, queues, subscriptions) in the database at startup. In environments where database schema is managed externally - for example by Flyway, Liquibase, or a CI/CD pipeline - you can disable auto-provisioning so the transport expects resources to already exist. ### Disable globally Turn off auto-provisioning for the entire transport: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddPostgres(transport => { transport.ConnectionString(connectionString); transport.ConfigureDefaults(defaults => { defaults.Queue.AutoProvision = false; defaults.Topic.AutoProvision = false; }); }); ``` With auto-provisioning disabled, the transport will not insert any topic, queue, or subscription rows. All rows must already exist in the database before the transport starts. ### Common patterns **Fully managed infrastructure:** Disable auto-provisioning globally and pre-populate the topology tables through your database migration pipeline. **Selective provisioning:** Disable globally but let the transport provision specific resources it owns. **Opt-out individual resources:** Keep auto-provisioning enabled but skip specific resources that are managed elsewhere. The effective auto-provision value for each resource follows a cascading pattern: | Resource setting | Transport default | Result | | ---------------- | ----------------- | --------------- | | true | any | Provisioned | | false | any | Not provisioned | | not set | true (default) | Provisioned | | not set | false | Not provisioned | When a resource does not specify `AutoProvision`, it inherits the transport-level default. When the transport does not specify `AutoProvision`, it defaults to `true`. ## Configure convention endpoints Use `transport.Handler()` at the end of the transport configuration when you want to keep the convention-derived queue name and only tune one handler endpoint: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddPostgres(transport => { transport.ConnectionString(connectionString); transport.Handler() .ConfigureEndpoint(e => e.MaxBatchSize(20).MaxConcurrency(10)); }); ``` For full control over the queue name, source bindings, and handler assignment, use `Queue("name")` instead. **MaxBatchSize** controls how many messages the endpoint reads from the database in a single `SELECT ... FOR UPDATE SKIP LOCKED` query. Default: `10`. Higher values reduce round trips but lock more rows simultaneously. **MaxConcurrency** controls how many messages the endpoint processes in parallel using `Parallel.ForEachAsync`. Default: `Environment.ProcessorCount`. Set this based on your handler's throughput characteristics. A good starting point: set `MaxBatchSize` equal to or slightly higher than `MaxConcurrency`. For I/O-bound handlers (database queries, HTTP calls), increase `MaxConcurrency` beyond the processor count. For CPU-bound handlers, keep `MaxConcurrency` close to `Environment.ProcessorCount`. ## Message delivery The transport uses a hybrid polling and notification model for message delivery. ### Polling with LISTEN/NOTIFY Receive endpoints do not busy-poll the database. Instead, they wait on an `AsyncAutoResetEvent` signal: 1. When a message is published or sent, the transport calls `pg_notify('mocha_queue_changed', queue_name)`. 2. A long-lived LISTEN connection receives the notification and sets the signal for the matching receive endpoint. 3. The endpoint wakes up and reads available messages. If the endpoint drains all messages (empty read), it goes back to waiting on the signal. If it reads a full batch, it immediately reads again until the queue is drained. ### Concurrent consumers with SKIP LOCKED Multiple consumers can process messages from the same queue concurrently. The transport uses `SELECT ... FOR UPDATE SKIP LOCKED` to lock messages for processing without blocking other consumers. Each consumer gets a unique `consumer_id` (a GUID), and locked messages are assigned to that consumer. ### Retry backoff When a message processing attempt fails, the message is released back to the queue with its `delivery_count` incremented. Redelivery is delayed using exponential backoff computed in SQL: ``` delay = 2^min(delivery_count, 10) seconds ``` This means: 2s, 4s, 8s, 16s, ... up to a maximum of 1024 seconds (\~17 minutes). Messages that exceed `max_delivery_count` (default: 10) are moved to a fault queue. ### Scheduled messages Messages with a `scheduled_time` in the future are not eligible for delivery until that time arrives. The transport queries for the next scheduled time after draining all available messages and sets a delayed trigger to wake the polling loop at the right moment. ### Message expiration Messages with an `expiration_time` are automatically skipped during reads when the expiration has passed. A background cleanup task deletes expired messages every 60 seconds. ## Background maintenance tasks The transport runs several background tasks to maintain system health: | Task | Interval | Description | | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------- | | Consumer heartbeat | 10s | Updates the consumer's updated\_at timestamp to indicate liveness | | Expired consumer cleanup | 60s | Removes consumers with no heartbeat for 2 minutes, cascading deletes to their temporary queues and messages | | Message cleanup | 60s | Deletes expired messages that have not been picked up | | Queue monitoring | 5min | Logs queue statistics (message count, scheduled count, age) | | Topic monitoring | 5min | Logs topic statistics (subscription count) | | Queue overflow cleanup | 5min | Enforces per-queue message limits (default: 100,000) by deleting oldest messages | All background tasks use exponential backoff on failure and shut down gracefully when the transport stops. ## Auto-provisioned resource naming | Resource | Naming convention | Created when | | ----------------- | ----------------------------------------------------------- | ---------------------------------- | | Topic | Message type name (e.g., order-placed-event) | First publish or subscribe | | Queue (subscribe) | Service and message type (e.g., billing.order-placed-event) | Handler is bound to the transport | | Queue (send) | Message type name (e.g., process-order-command) | First send or handler registration | | Reply queue | Instance-specific name | Transport starts | | Subscription | Topic-to-queue link | Endpoint discovery phase | All auto-provisioned resources are inserted as rows in the corresponding topology tables at transport startup. ## Next steps - [Transports Overview](https://chillicream.com/docs/mocha/transports) \- Understand the transport abstraction and lifecycle. - [Handlers and Consumers](https://chillicream.com/docs/mocha/handlers-and-consumers) \- Learn about handler types and consumer configuration. - [Reliability](https://chillicream.com/docs/mocha/reliability) \- Configure dead-letter routing, outbox, inbox, and fault handling. - [Middleware and Pipelines](https://chillicream.com/docs/mocha/middleware-and-pipelines) \- Customize the receive and dispatch pipelines. - [Routing and Endpoints](https://chillicream.com/docs/mocha/routing-and-endpoints) \- Understand naming conventions and endpoint model. > **Runnable example:** [PostgresTransport](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/PostgresTransport) > > **Multi-service demo:** The PostgresTransport example includes three services (OrderService, ShippingService, NotificationService) orchestrated with .NET Aspire, demonstrating publish/subscribe, send, request/reply, and batch processing over a shared PostgreSQL database. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/transports/postgres.md) Maintained by ChilliCream. Last updated on **August 23, 2026** by **PascalSenn** --- # RabbitMQ Transport - Mocha > Configure the RabbitMQ transport in Mocha for production messaging with automatic topology provisioning, connection management, and prefetch tuning. Canonical source: https://chillicream.com/docs/mocha/transports/rabbitmq The RabbitMQ transport connects Mocha to a RabbitMQ broker for production messaging. It manages connections, provisions exchanges and queues automatically, handles message acknowledgement, and supports request/reply with dedicated reply endpoints. When you need durable, distributed messaging across multiple services, this is the transport to use. ## Set up the RabbitMQ transport By the end of this section, you will have a Mocha bus connected to RabbitMQ with automatic topology provisioning. ### Install the package Bash ``` dotnet add package Mocha.Transport.RabbitMQ ``` ### Register with .NET Aspire The most common setup uses the Aspire RabbitMQ component for connection management: Bash ``` dotnet add package Aspire.RabbitMQ.Client ``` C# ``` using Mocha; using Mocha.Transport.RabbitMQ; var builder = WebApplication.CreateBuilder(args); // Aspire registers IConnectionFactory from the "rabbitmq" connection resource builder.AddRabbitMQClient("rabbitmq"); // Register the message bus with RabbitMQ transport builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(); var app = builder.Build(); app.Run(); ``` The Aspire component reads the connection string from configuration (typically `ConnectionStrings:rabbitmq`), handles health checks, and integrates with the Aspire dashboard for observability. `.AddRabbitMQ()` picks up the `IConnectionFactory` from DI (registered by Aspire) and uses it to establish connections to the broker. Default conventions automatically create exchanges, queues, and bindings for your registered handlers. ### Register with a manual connection string If you are not using Aspire, register the `IConnectionFactory` directly: C# ``` using Mocha; using Mocha.Transport.RabbitMQ; using RabbitMQ.Client; var builder = WebApplication.CreateBuilder(args); // Register IConnectionFactory manually builder.Services.AddSingleton(_ => new ConnectionFactory { HostName = "localhost", Port = 5672, VirtualHost = "/", UserName = "guest", Password = "guest" }); builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(); var app = builder.Build(); app.Run(); ``` To use a connection string from configuration: C# ``` builder.Services.AddSingleton(_ => { var factory = new ConnectionFactory(); factory.Uri = new Uri(builder.Configuration.GetConnectionString("rabbitmq")!); return factory; }); ``` ### Use a custom connection provider For full control over connection lifecycle, provide a custom `IRabbitMQConnectionProvider`: C# ``` builder.Services .AddMessageBus() .AddRabbitMQ(transport => { transport.ConnectionProvider(sp => { return sp.GetRequiredService(); }); }); ``` The `IRabbitMQConnectionProvider` interface exposes `Host`, `Port`, `VirtualHost`, and a `CreateAsync` method. When no custom provider is registered, the transport falls back to resolving `IConnectionFactory` from DI and wrapping it in a default provider. ### Verify it works Add an endpoint that publishes through the bus and verify the handler executes: C# ``` app.MapPost("/orders", async (IMessageBus bus) => { await bus.PublishAsync(new OrderPlacedEvent { OrderId = Guid.NewGuid(), CustomerId = "customer-1", TotalAmount = 99.99m }, CancellationToken.None); return Results.Ok(); }); ``` Send a POST request to `/orders` and check your application logs. You should see the handler process the event. You can also inspect the RabbitMQ management UI at `http://localhost:15672` to see the auto-provisioned exchanges and queues. ## Two connections per broker transport Mocha opens two connections to the broker: one for consuming and one for dispatching. This design prevents back-pressure from slow consumers from blocking outbound message publishing. When a consumer processes messages slowly, the RabbitMQ client applies back-pressure on that connection. Without separation, a slow consumer could prevent your application from publishing new messages entirely. With separate connections, each direction operates independently. ## How topology works When the transport starts, it provisions topology on the broker automatically. Here is how message types map to RabbitMQ resources: **Events (publish/subscribe):** Each event type gets a fanout exchange. Each service that subscribes creates a queue bound to that exchange. Publishing sends the message to the exchange, which fans it out to all bound queues. **Commands (send):** Each command type gets a direct exchange bound to a single queue. Sending delivers the message to exactly one consumer. **Request/reply:** The transport creates a temporary reply queue per service instance. The reply address is embedded in the request message so the responder knows where to send the reply. Warning Messages published before the transport completes its Start phase may be lost if no queue is bound to the exchange yet. During deployment, ensure consuming services start before publishing services, or use [publisher confirms](https://www.rabbitmq.com/docs/reliability#publisher-confirms) to detect lost messages. If a message is published to an exchange with no bound queue - for example, when no consumer has started - that message is dropped. Mocha auto-provisions topology, but the window between exchange creation and queue binding is a real operational risk. ### Publisher confirms Mocha's RabbitMQ transport uses publisher confirms on dispatch, which means the broker acknowledges each published message before the publish call completes. This provides at-least-once delivery guarantees for outbound messages: if the broker does not confirm, the publish fails with an exception. See the [RabbitMQ Reliability Guide](https://www.rabbitmq.com/docs/reliability) for a full treatment of delivery guarantees. ### Default topology for event handlers When you register an event handler with `AddEventHandler()`, the RabbitMQ transport creates this topology: A fanout exchange named after the message type fans out to per-service exchanges, which bind to per-service queues. This allows multiple services to each receive a copy of every published event. ### Default topology for send handlers When you register a request handler with `AddRequestHandler()` for send (fire-and-forget), the transport creates a single queue: Send messages go to a dedicated queue. Only one handler processes each message - this is the point-to-point guarantee. ## Configure transport-level defaults You can set defaults that apply to all auto-provisioned queues and exchanges. This is useful when you want consistent settings across all resources without configuring each one individually. Use `ConfigureDefaults` to set queue and exchange defaults: C# ``` builder.Services .AddMessageBus() .AddRabbitMQ(transport => { transport.ConfigureDefaults(defaults => { // All queues will be quorum with a delivery limit of 5 defaults.Queue.QueueType = RabbitMQQueueType.Quorum; defaults.Queue.Arguments["x-delivery-limit"] = 5; // All exchanges will use topic routing defaults.Exchange.Type = RabbitMQExchangeType.Topic; }); }); ``` For example, to enable [quorum queues](https://www.rabbitmq.com/docs/quorum-queues) with a specific initial group size: C# ``` builder.Services .AddMessageBus() .AddRabbitMQ(transport => { transport.ConfigureDefaults(defaults => { defaults.Queue.QueueType = RabbitMQQueueType.Quorum; defaults.Queue.Arguments["x-quorum-initial-group-size"] = 3; }); }); ``` Or to use [stream queues](https://www.rabbitmq.com/docs/streams) for append-only log semantics: C# ``` builder.Services .AddMessageBus() .AddRabbitMQ(transport => { transport.ConfigureDefaults(defaults => { defaults.Queue.QueueType = RabbitMQQueueType.Stream; }); }); ``` Available queue defaults: | Property | Type | Description | | ---------- | -------------------------- | ------------------------------------------------------------- | | QueueType | string | Queue type: RabbitMQQueueType.Classic, .Quorum, or .Stream | | Durable | bool? | Whether queues survive broker restarts (default: true) | | AutoDelete | bool? | Whether queues are auto-deleted when unused (default: false) | | Arguments | Dictionary | Additional arguments (e.g., x-delivery-limit, x-max-priority) | Available exchange defaults: | Property | Type | Description | | ---------- | -------------------------- | ------------------------------------------------------------------------ | | Type | string | Exchange type: RabbitMQExchangeType.Fanout, .Direct, .Topic, or .Headers | | Durable | bool? | Whether exchanges survive broker restarts (default: true) | | AutoDelete | bool? | Whether exchanges are auto-deleted when unused (default: false) | | Arguments | Dictionary | Additional arguments (e.g., alternate-exchange) | Defaults never override explicitly configured values. If you declare a queue with a specific queue type, that setting takes precedence over the transport default. You can call `ConfigureDefaults` multiple times - each call accumulates settings on the same defaults object. ## Configure queues Use `transport.Queue("name")` when you need to customize a RabbitMQ queue. The queue builder is the primary API for queue names, queue type, arguments, handler assignment, source bindings, prefetch, concurrency, fault queues, and skipped queues. C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(transport => { transport.BindExplicitly(); transport.Queue("orders.processing") .BindImplicitly() .Quorum() .MaxPrefetch(50) .MaxConcurrency(10) .FaultEndpoint("order-errors") .Handler(); }); ``` `BindExplicitly()` at the transport scope means only queues you configure are used for receiving. `BindImplicitly()` on the queue tells Mocha to generate the convention-derived exchange bindings for the messages handled by that queue. Calling `Queue("name")` without `Handler()`, `Consumer()`, or `Receives()` declares only the RabbitMQ queue. Add a handler, consumer, or received message type when the queue should also consume messages. C# ``` transport.Queue("audit-log") .Quorum() .AutoProvision(false); ``` For custom source topology, bind the queue from a specific exchange: C# ``` transport.Queue("eu-orders") .BindExplicitly() .BindFrom(new Uri("exchange:region-events"), "eu.*") .Consumer(); ``` `BindExplicitly()` on the queue suppresses convention-derived exchange bindings for that queue, so only the `BindFrom(...)` sources are used. ## Declare topology resources Mocha auto-provisions topology from registered handlers and queue builders by default. Caution Use `DeclareExchange()`, `DeclareQueue()`, and `DeclareBinding()` only when you need infrastructure topology that is not represented by a queue builder, or when you are aligning with topology managed outside of Mocha. For handler queues, prefer `transport.Queue("name")`. To declare infrastructure-only topology: C# ``` builder.Services .AddMessageBus() .AddRabbitMQ(transport => { transport.DeclareExchange("order-events") .Type(RabbitMQExchangeType.Fanout) .Durable() .AutoProvision(); transport.DeclareQueue("billing-orders") .Durable() .AutoProvision() .WithArgument("x-queue-type", "quorum"); transport.DeclareBinding("order-events", "billing-orders") .AutoProvision(); }); ``` All declared topology is provisioned when the transport starts, before receive endpoints begin consuming. ## Temporary receive endpoints Call `Temporary()` on a queue or receive endpoint descriptor to scope its backing queue to the lifetime of the consuming process: C# ``` transport.Queue($"tenant-events-{instanceId}") .Temporary() .Receives(); ``` `Temporary()` marks the queue non-durable and auto-delete (`Durable = false`, `AutoDelete = true`). The broker removes the queue once its last consumer disconnects, independent of any idle-time window. If the queue is already explicitly declared as durable or non-auto-delete, for example through `DeclareQueue(...)` without matching settings, startup fails with an explicit configuration error instead of silently ignoring `Temporary()`. ## Control auto-provisioning By default, the transport auto-provisions all topology resources (exchanges, queues, bindings) on the broker at startup. In production environments where infrastructure is managed externally - for example by Terraform, Ansible, or the [RabbitMQ Messaging Topology Operator](https://www.rabbitmq.com/kubernetes/operator/install-topology-operator) on Kubernetes - you can disable auto-provisioning so the transport expects resources to already exist. The examples in this section use `DeclareExchange()`, `DeclareQueue()`, and `DeclareBinding()` because they are about broker topology management. For application handler queues, use `Queue("name")` instead. ### Disable globally Turn off auto-provisioning for the entire transport: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(transport => { transport.AutoProvision(false); }); ``` With auto-provisioning disabled, the transport will not create any exchanges, queues, or bindings. All resources must already exist on the broker before the transport starts. ### Override per resource Individual resources can override the transport-level setting. This is useful when most topology is managed externally but a few resources need to be created dynamically: C# ``` builder.Services .AddMessageBus() .AddRabbitMQ(transport => { // Disable globally transport.AutoProvision(false); // This exchange already exists on the broker - skip provisioning transport.DeclareExchange("order-events"); // This queue should be created by the transport transport.DeclareQueue("billing-orders") .AutoProvision(true); // This binding should also be created transport.DeclareBinding("order-events", "billing-orders") .AutoProvision(true); }); ``` The effective auto-provision value for each resource follows a cascading pattern: | Resource setting | Transport setting | Result | | ---------------- | ----------------- | --------------- | | true | any | Provisioned | | false | any | Not provisioned | | not set | true (default) | Provisioned | | not set | false | Not provisioned | When a resource does not specify `AutoProvision`, it inherits the transport-level default. When the transport does not specify `AutoProvision`, it defaults to `true`. ### Common patterns **Fully managed infrastructure:** Disable auto-provisioning globally and declare all resources without `AutoProvision`. The transport will use existing broker resources without attempting to create them. C# ``` transport.AutoProvision(false); transport.DeclareExchange("order-events"); transport.DeclareQueue("billing-orders"); transport.DeclareBinding("order-events", "billing-orders"); ``` **Selective provisioning:** Disable globally but enable for specific resources that are owned by this service. C# ``` transport.AutoProvision(false); transport.DeclareExchange("shared-events"); // managed externally transport.DeclareQueue("my-service-queue") .AutoProvision(true); // owned by this service transport.DeclareBinding("shared-events", "my-service-queue") .AutoProvision(true); // owned by this service ``` **Kubernetes with the Messaging Topology Operator:** When the [RabbitMQ Messaging Topology Operator](https://www.rabbitmq.com/kubernetes/operator/install-topology-operator) manages your exchanges, queues, and bindings as Kubernetes custom resources, disable auto-provisioning entirely. The operator declares topology through CRDs, and the transport simply uses the existing resources: YAML ``` # Kubernetes CRD - managed by the Messaging Topology Operator apiVersion: rabbitmq.com/v1beta1 kind: Queue metadata: name: billing-orders spec: name: billing-orders durable: true rabbitmqClusterReference: name: my-cluster ``` C# ``` // Application code - topology already exists on the broker transport.AutoProvision(false); transport.DeclareExchange("order-events"); transport.DeclareQueue("billing-orders"); transport.DeclareBinding("order-events", "billing-orders"); ``` **Opt-out individual resources:** Keep auto-provisioning enabled but skip specific resources that are managed elsewhere. C# ``` transport.DeclareExchange("platform-events") .AutoProvision(false); // managed by platform team transport.DeclareQueue("my-queue"); // auto-provisioned (default) transport.DeclareBinding("platform-events", "my-queue"); // auto-provisioned (default) ``` ## Configure convention endpoints Use `transport.Handler()` at the end of the transport configuration when you want to keep the convention-derived queue name and only tune one handler endpoint: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(transport => { transport.Handler() .ConfigureEndpoint(e => e.MaxPrefetch(50).MaxConcurrency(10)); }); ``` This keeps the convention-derived endpoint name while tuning the consumer settings. `ConfigureEndpoint()` can be called multiple times - actions compose in declaration order: C# ``` transport.Handler() .ConfigureEndpoint(e => e.MaxPrefetch(50)) .ConfigureEndpoint(e => e.MaxConcurrency(10)) .ConfigureEndpoint(e => e.FaultEndpoint("order-errors")); ``` For full control over the queue name, queue type, source bindings, and handler assignment, use `Queue("name")` instead. **MaxPrefetch** controls how many unacknowledged messages RabbitMQ delivers to the consumer at once. Default: `100`. Lower values reduce memory pressure under high load. Higher values improve throughput for fast handlers. **MaxConcurrency** controls how many messages the endpoint processes in parallel. Set this based on your handler's throughput characteristics. A good starting point: set `MaxPrefetch` equal to or slightly higher than `MaxConcurrency`. For slow handlers (long database operations, external API calls), lower `MaxPrefetch` to `10` to `20` to prevent messages from piling up in the consumer's unacknowledged buffer. For quorum queues specifically, avoid setting `MaxPrefetch` to `1` \- a prefetch of `1` starves consumers while acknowledgements flow through the consensus mechanism and significantly reduces throughput. For prefetch tuning guidance from first principles, see [CloudAMQP Best Practices](https://www.cloudamqp.com/blog/part1-rabbitmq-best-practice.html). Example with the preferred queue builder: C# ``` builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(transport => { transport.BindExplicitly(); transport.Queue("orders.processing") .BindImplicitly() .MaxPrefetch(50) .MaxConcurrency(10) .Handler(); }); ``` ## Auto-provisioned resource naming | Resource | Naming convention | Created when | | ------------------ | ------------------------------------------------- | ---------------------------------- | | Exchange (event) | Message type name (e.g., OrderPlacedEvent) | First publish or subscribe | | Exchange (command) | Message type name (e.g., ReserveInventoryCommand) | First send or handler registration | | Queue | Endpoint name derived from handler registration | Handler is bound to the transport | | Reply queue | Instance-specific name | Transport starts | | Bindings | Exchange-to-queue | Endpoint discovery phase | All auto-provisioned resources are durable by default and survive broker restarts. ## Routing keys RabbitMQ uses a `routing_key` field on every published message to decide which queues receive it. When you publish to a **topic exchange**, the broker compares the message's routing key against binding patterns on each queue. Queues whose pattern matches get the message. Queues that don't match never see it. **Direct exchanges** work the same way, but require an exact match instead of a pattern. **Fanout exchanges** ignore routing keys entirely - every bound queue gets every message. Routing keys are useful when you need to split a single message stream across different consumers based on a property of the message itself: - **Disconnecting producers from consumers** \- publish messages without knowing which queues or services will consume them. Consumers can bind with patterns to receive only the messages they care about. - **Multi-tenant routing** \- route messages to tenant-specific queues (`tenant-a.orders`, `tenant-b.orders`) - **Region-based routing** \- route to regional processors (`us.east`, `eu.west`) - **Priority routing** \- separate high-priority and low-priority messages (`priority.high`, `priority.low`) For a full treatment of topic exchange routing, see the [RabbitMQ Topics Tutorial](https://www.rabbitmq.com/tutorials/tutorial-five-dotnet). ### Configure routing key extraction To set a routing key on published messages, call `UseRabbitMQRoutingKey()` when registering the message type: C# ``` builder.Services .AddMessageBus() .AddMessage(m => m .UseRabbitMQRoutingKey(msg => msg.Region)) .AddRabbitMQ(); ``` The extractor function runs at dispatch time for each message. It receives the message instance and returns the routing key string. Return `null` to publish without a routing key. `UseRabbitMQRoutingKey()` is configured on `AddMessage()`, not on the transport or endpoint. This keeps routing key logic next to the message definition where it belongs. #### Composite routing keys Combine multiple properties into a single routing key using string interpolation: C# ``` builder.Services .AddMessageBus() .AddMessage(m => m .UseRabbitMQRoutingKey(msg => $"{msg.TenantId}.{msg.Region}")) .AddRabbitMQ(); ``` This produces routing keys like `acme.us.east` or `contoso.eu.west`, which you can match with topic exchange binding patterns like `acme.#` or `*.eu.*`. ### Topic exchange example This example routes region-tagged events to different queues based on their routing key. The US queue receives messages matching `us.*`, and the EU queue receives messages matching `eu.*`. #### Define the message type C# ``` public sealed class RegionEvent { public required string Region { get; init; } public required string Payload { get; init; } } ``` #### Wire up the bus C# ``` builder.Services .AddMessageBus() .AddConsumer() .AddConsumer() .AddMessage(m => m .UseRabbitMQRoutingKey(msg => msg.Region)) .AddRabbitMQ(transport => { transport.BindExplicitly(); // Declare the exchange so the transport provisions it as a topic exchange transport.DeclareExchange("region-events") .Type(RabbitMQExchangeType.Topic); transport.Queue("us-orders") .BindExplicitly() .BindFrom(new Uri("exchange:region-events"), "us.*") .Consumer(); transport.Queue("eu-orders") .BindExplicitly() .BindFrom(new Uri("exchange:region-events"), "eu.*") .Consumer(); // Dispatch to the topic exchange transport.DispatchEndpoint("region-dispatch") .ToExchange("region-events") .Publish(); }); ``` When you publish a `RegionEvent` with `Region = "us.east"`, the routing key middleware extracts `"us.east"` from the message and sets it on the AMQP publish. The topic exchange matches `"us.east"` against `us.*` (match) and `eu.*` (no match). Only the US queue receives the message. #### Topic exchange binding patterns | Pattern | Matches | Does not match | | --------- | ------------------------ | -------------------- | | us.\* | us.east, us.west | us.east.az1, eu.west | | eu.# | eu.west, eu.west.az1 | us.east | | # | Everything | \- | | \*.\*.az1 | us.east.az1, eu.west.az1 | us.east | `*` matches exactly one word. `#` matches zero or more words. Words are separated by dots. ### Direct exchange routing keys Direct exchanges use exact-match routing keys instead of patterns. A message with routing key `"priority-high"` reaches only queues bound with exactly `"priority-high"`. C# ``` builder.Services .AddMessageBus() .AddConsumer() .AddMessage(m => m .UseRabbitMQRoutingKey(msg => $"priority-{msg.Priority}")) .AddRabbitMQ(transport => { transport.BindExplicitly(); transport.DeclareExchange("task-routing") .Type(RabbitMQExchangeType.Direct); transport.Queue("high-priority-tasks") .BindExplicitly() .BindFrom(new Uri("exchange:task-routing"), "priority-high") .Consumer(); transport.DispatchEndpoint("task-dispatch") .ToExchange("task-routing") .Publish(); }); ``` Messages with `Priority = "high"` reach the queue. Messages with any other priority are dropped by the exchange (unless another queue is bound with a matching routing key). ## Next steps - [Transports Overview](https://chillicream.com/docs/mocha/transports) \- Understand the transport abstraction and lifecycle. - [Handlers and Consumers](https://chillicream.com/docs/mocha/handlers-and-consumers) \- Learn about handler types and consumer configuration. - [Reliability](https://chillicream.com/docs/mocha/reliability) \- Configure dead-letter routing, outbox, inbox, and fault handling. > **Runnable example:** [RabbitMQ](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Transports/RabbitMQ) > > **Full demo:** All three Demo services use RabbitMQ in production mode with .NET Aspire. See [Demo.AppHost](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.AppHost) for the Aspire orchestration and [Demo.Catalog](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Catalog) for a complete service using `.AddRabbitMQ()` with outbox, inbox, sagas, and multiple handler types. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/mocha/transports/rabbitmq.md) Maintained by ChilliCream. Last updated on **August 23, 2026** by **PascalSenn** --- # Skills: Agent Skills CLI for .NET > The `skills` .NET CLI installs, updates, and authors Agent Skills: portable SKILL.md files you can share across Claude Code, Cursor, and 55+ agents. Canonical source: https://chillicream.com/docs/skills `skills` is the .NET CLI that installs, updates, and authors [Agent Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills): portable `SKILL.md` files that teach an AI coding agent how you work. Stop re-explaining your conventions every session. Package the knowledge once, then hand the same skill to Claude Code, Cursor, GitHub Copilot, or another of the 55+ supported agents with a single command. Run it with `dnx`, which ships with the .NET 10 SDK, so there is nothing to install first: Bash ``` dnx skills add anthropics/skills --agent claude-code ``` ``` Source: https://github.com/anthropics/skills.git Found 2 skill(s) ┌─Installation Summary─────────────────────────────────────────────────────────┐ │ Canonical: /your-project/.agents/skills │ │ Symlinked: Claude Code │ └──────────────────────────────────────────────────────────────────────────────┘ ┌─Installed 2 skill(s)─────────────────────────────────────────────────────────┐ │ ✓ pdf │ │ → /your-project/.claude/skills/pdf │ │ ✓ docx │ │ → /your-project/.claude/skills/docx │ └──────────────────────────────────────────────────────────────────────────────┘ Done! Review skills before use; they run with full agent permissions. ``` The CLI detects the agents you have installed, then symlinks the skill into each one from a single canonical store. The agent loads the skill on its own when a task matches. These docs show every command as `dnx skills`. Prefer `skills` on your `PATH`? Install the global tool with `dotnet tool install -g skills`, then drop the `dnx` prefix and run `skills` directly. Need the prerequisites or a step-by-step walkthrough? See [Get started](https://chillicream.com/docs/skills/getting-started). The [source repository](https://github.com/ChilliCream/skills) and [skills package on NuGet](https://www.nuget.org/packages/skills) are public. ## What are Agent Skills A skill is a folder with a single `SKILL.md` file. That file has YAML frontmatter (a `name` and a `description`) followed by a Markdown body of instructions. Optional subfolders carry supporting material the agent reads only when it needs it. Think of a skill as an onboarding guide for a new team member. You write down a procedure once (how to run your test suite, how your commit messages are formatted, which API your team prefers), and the agent references the relevant section when a task calls for it. A general-purpose agent becomes a specialist in your codebase without bespoke prompt engineering on every request. Skills differ from prompts. A prompt is conversation-level guidance you paste in for one task. A skill loads on demand across every conversation, so you never repaste the same instructions again. The `SKILL.md` format is an open standard created by Anthropic and adopted across the ecosystem. The same file works in 55+ supported agents, including Claude Code, Cursor, and GitHub Copilot. To go deeper, read Anthropic's [Agent Skills overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) and the [open specification](https://agentskills.io/specification) at agentskills.io. ## How skills load: progressive disclosure Skills stay out of the agent's context window until they are needed. The format loads in three levels, each at a different time. This is the single most important idea to understand, because it is why you can install many skills without paying a context penalty. | Level | What loads | When | Token cost | | --------------------- | ----------------------------------------------- | --------------------------------------- | ---------------------- | | Level 1: Metadata | The name and description from the frontmatter | Always, at startup | \~100 tokens per skill | | Level 2: Instructions | The SKILL.md Markdown body | When the skill is triggered | Keep under \~5k tokens | | Level 3: Resources | Bundled files in references/, scripts/, assets/ | On demand, when the body points to them | Effectively unlimited | At startup the agent loads only each skill's `name` and `description`. The `description` is what the agent matches against your request to decide whether to activate the skill, so write it to describe both what the skill does and when to use it. When a request matches, the agent reads the full body. Bundled resources never enter context until the body references them, which is why supporting material can be effectively unbounded. The open standard frames the same flow as Discovery, Activation, and Execution. For the full model, see Anthropic's [overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) and the [agentskills.io home page](https://agentskills.io/home). ## What a skill looks like A skill is, at minimum, a folder containing one `SKILL.md` file. Everything else is optional and loaded only when referenced: ``` my-skill/ ├── SKILL.md # Required: frontmatter (name + description) + Markdown instructions ├── references/ # Optional: extra docs the agent reads on demand ├── scripts/ # Optional: code the agent runs via the shell └── assets/ # Optional: templates, schemas, images, data files ``` The folder name should match the `name` in the frontmatter. Here is a minimal, valid `SKILL.md` with the two required fields: ``` --- name: roll-dice description: Rolls one or more dice and returns the results. Use when the user asks to roll dice, flip for a decision, or pick a random number in a range. --- # Roll dice When the user asks to roll dice, parse the count and number of sides (default to one six-sided die), then return each roll and the total. ``` The `dnx skills init` command scaffolds this layout for you; [Authoring Skills](https://chillicream.com/docs/skills/authoring-skills) walks through it with the generated `SKILL.md` and folder tree. The full field reference, including optional frontmatter, lives in the [open specification](https://agentskills.io/specification) and in [Authoring Skills](https://chillicream.com/docs/skills/authoring-skills). ## Why Skills The `skills` CLI brings the install-once, run-anywhere skill workflow to the .NET SDK. Three things make it worth adding to your toolbox. Project-scoped installs are recorded in a `skills-lock.json` file in your working directory. Commit it, and your whole team shares one reproducible skill set, the same way a lock file pins your package dependencies. One install reaches every detected agent. The CLI materializes each skill once in a canonical store, then links it into the directory each installed agent expects. You do not manage per-agent copies by hand. Personal skills install globally with `--global`. Those live outside any single project and follow you across every repository you work in. ## The ecosystem The Agent Skills world has three distinct layers. Knowing which is which helps you find what you need. | Layer | What it is | Where | | ------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | Format / spec | The open SKILL.md standard | [agentskills.io](https://agentskills.io/specification), [anthropics/skills](https://github.com/anthropics/skills) | | Discovery | A directory of published skills to browse | [skills.sh](https://www.skills.sh/) | | Installers | CLIs that fetch and install skills | skills (.NET), [npx skills](https://github.com/vercel-labs/skills) (Node) | The `skills` CLI brings the [npx skills](https://github.com/vercel-labs/skills) workflow to the .NET SDK via `dnx`, so you can run it without a global install. The command surface deliberately mirrors what Node developers already know. The CLI has no registry of its own: you point `dnx skills add` at any git repository or local folder, and you discover skills to install from directories like [skills.sh](https://www.skills.sh/). ## Next steps - [Get started](https://chillicream.com/docs/skills/getting-started): run `skills` and add your first skill end to end. - [Author a skill](https://chillicream.com/docs/skills/authoring-skills): scaffold a `SKILL.md`, write a strong description, and publish it. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/skills/index.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Authoring Skills - Skills > Author Agent Skills with the `skills` CLI: scaffold a folder with `dnx skills init`, write the SKILL.md frontmatter and instructions, and publish through git. Canonical source: https://chillicream.com/docs/skills/authoring-skills Package your team's conventions once, and any agent can pick them up. A skill is a folder with a `SKILL.md` file that tells an agent how to do something the way your team does it: your release checklist, your code review rules, your API client patterns. You write it once, push it to a git repo, and your teammates install it with one command. From then on, every agent they run loads that knowledge on demand, no copy-pasting prompts into each session. This page teaches you to scaffold a skill, write a correct `SKILL.md`, add supporting files, and publish it so others can install it with [dnx skills add](https://chillicream.com/docs/skills/installing-skills). The skill format is the open [Agent Skills](https://agentskills.io/home) standard, so what you write here works across [55+ agents](https://chillicream.com/docs/skills), not only the one you author it in. ## Scaffold a skill > Prerequisite: the .NET 10 SDK. See [Getting Started](https://chillicream.com/docs/skills/getting-started) before running the commands below. To create a new skill in its own folder, run `dnx skills init` with a name. Bash ``` dnx skills init my-skill ``` ``` Initialized skill: my-skill Created: my-skill/SKILL.md Next steps: 1. Edit my-skill/SKILL.md to define your skill instructions 2. Update the name and description in the frontmatter Publishing: GitHub: Push to a repo, then skills add / URL: Host the file, then skills add https://example.com/my-skill/SKILL.md ``` This creates `my-skill/SKILL.md`. If everything worked, you have a folder named `my-skill` with one file inside it. To scaffold a skill in the current directory instead, run `dnx skills init` with no name. The CLI derives the skill name from the folder you are in and writes `SKILL.md` next to your other files. Bash ``` dnx skills init ``` ``` Initialized skill: my-skill Created: SKILL.md Next steps: 1. Edit SKILL.md to define your skill instructions 2. Update the name and description in the frontmatter Publishing: GitHub: Push to a repo, then skills add / URL: Host the file, then skills add https://example.com/my-skill/SKILL.md ``` `dnx skills init` never overwrites an existing skill. If `SKILL.md` is already present, it leaves your file untouched and tells you so. ``` Skill already exists at my-skill/SKILL.md ``` ### The generated template `dnx skills init` writes a spec-compliant starting point: the two required frontmatter fields, then a body with a heading and the sections most skills need. ``` --- name: my-skill description: A brief description of what this skill does --- # my-skill Instructions for the agent to follow when this skill is activated. ## When to use Describe when this skill should be used. ## Instructions 1. First step 2. Second step 3. Additional steps as needed ``` Replace the placeholder `description`, then fill in the body. The next sections explain each part. ## Structure a SKILL.md A `SKILL.md` has two parts: YAML frontmatter between `---` fences, and a Markdown body below it. The frontmatter is metadata the agent reads to decide whether your skill is relevant. The body is the instructions the agent follows once it decides to use the skill. The "Required frontmatter" and "Optional frontmatter" subsections below are reference material: field-by-field constraint tables you can scan when you fill in the frontmatter. For the complete, authoritative field list see the [Reference](https://chillicream.com/docs/skills/reference). ### Required frontmatter Every skill needs `name` and `description`. The CLI skips any skill that is missing either field, so getting these right is the difference between a skill that installs and one that silently disappears. | Field | Constraints | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | name | 1 to 64 characters. Lowercase letters, digits, and hyphens only. No leading or trailing hyphen. No consecutive hyphens. Should match the skill's folder name. Must not contain XML tags, and must not contain the reserved words anthropic or claude. | | description | 1 to 1024 characters. Must say BOTH what the skill does AND when to use it. Include specific trigger keywords that match the tasks where the skill applies. Must not contain XML tags. | The `description` is the single most important field you write. At startup the agent loads only your `name` and `description` (not the body), then matches the user's request against that description to decide whether to read the rest. A vague description means the agent never triggers your skill. Front-load what the skill does, then state the conditions that should activate it, and name the tools, file types, or commands involved. ``` --- name: release-checklist description: Runs our release checklist before publishing a NuGet package. Use when cutting a release, bumping a version, or preparing a package for nuget.org. --- ``` These constraints are the union of two authorities: the open spec at [agentskills.io/specification](https://agentskills.io/specification) (which adds the "match the folder name" and "no consecutive hyphens" rules) and the [Anthropic platform rules](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) (which add the reserved-word and XML-tag prohibitions). Honoring both keeps your skill portable across every agent. ### Optional frontmatter Add these fields only when you need them. The open spec at [agentskills.io/specification](https://agentskills.io/specification) is the authoritative field list; the most useful fields are below. | Field | Purpose | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | license | A license name or a reference to a bundled license file. Keep it short. | | compatibility | Up to 500 characters describing environment needs (intended product, system packages, network access). Most skills do not need it. Example: Requires git, docker, and jq. | | metadata | A string-to-string map for client-defined properties the spec does not cover, such as author or version. Use distinctive key names to avoid collisions. | | allowed-tools | A space-separated list of pre-approved tools, for example Bash(git:\*) Read. | For the exhaustive list of frontmatter fields and their constraints, see the [Reference](https://chillicream.com/docs/skills/reference). Warning **`allowed-tools` is experimental.** Support varies between agents, and `dnx skills init` does not emit it. Treat it as a hint that some agents honor and others ignore, not a security boundary. ### The instructions body The body is plain Markdown the agent follows when your skill activates. There are no format restrictions, so write it the way you would brief a new teammate: step-by-step instructions, input and output examples, and the edge cases people get wrong. Keep `SKILL.md` focused. The recommendation from both [Anthropic](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills) and the open spec is to keep the body under roughly 500 lines and move long detail into supporting files (see the next section). A tight body loads fast and keeps the agent on the steps that matter. ## Add supporting files A skill folder can hold more than `SKILL.md`. Three conventional subdirectories cover most needs. ``` my-skill/ ├── SKILL.md # Required: frontmatter + instructions ├── references/ # Docs loaded on demand (REFERENCE.md, API.md, ...) ├── scripts/ # Code the agent runs └── assets/ # Templates, data files, schemas ``` The CLI copies or symlinks the whole folder as a unit, so every supporting file ships with the skill. (A few build artifacts are excluded automatically, including `.git`, `node_modules`, and `__pycache__`.) These directories exist because of [progressive disclosure](https://chillicream.com/docs/skills#how-skills-load-progressive-disclosure): the agent loads your skill in stages so a large skill costs almost nothing until it is needed. - At startup the agent loads only `name` and `description`. - When the skill triggers, the agent reads the `SKILL.md` body. - A file under `references/`, `scripts/`, or `assets/` loads only when the body points the agent at it. This is why moving detail out of `SKILL.md` and into `references/` is free: that material never enters the context window until the agent actually follows a link to it. Reference your supporting files with relative paths one level deep (for example `references/api.md`), and avoid deep nesting chains. For more on the three-stage model, see the [Introduction](https://chillicream.com/docs/skills#how-skills-load-progressive-disclosure) and [Anthropic's overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview). ## How installed skills are named When someone installs your skill, the CLI derives the on-disk directory name by sanitizing the `name` field. It lowercases the name, replaces every run of characters outside `a-z`, `0-9`, `.`, and `_` with a single hyphen, and trims leading and trailing dots and hyphens. For a clean lowercase-hyphen name like `release-checklist`, the on-disk name is identical, with no surprises. A name with spaces or uppercase letters gets rewritten (`My Skill` becomes `my-skill`), which can mismatch what you and your teammates expect on disk and during removal. Pick a clean lowercase-hyphen name that already satisfies the [required frontmatter rules](#required-frontmatter), and the install name matches your folder name exactly. ## Publish and install A skill is a folder in a git repo, so publishing is committing and pushing. Any repository the CLI can reach with git works. To publish, commit your skill folder and push it to GitHub, GitLab, or any git remote. Then anyone installs it with `dnx skills add` pointed at the repo. The forms below show what an installer runs against your published skill; for the full add workflow, including expected output, scoping, and agent targeting, see [Installing Skills](https://chillicream.com/docs/skills/installing-skills). Bash ``` # install every skill in the repo dnx skills add my-org/my-skills # install only one skill from a multi-skill repo dnx skills add my-org/my-skills@release-checklist # install a single SKILL.md hosted at a URL dnx skills add https://example.com/my-skill/SKILL.md ``` Private repositories work with no extra configuration. The CLI shells out to your own `git`, so it uses your existing credentials (SSH agent keys, a git credential helper, or `gh auth`). If you can `git clone` the repo, `skills` can install from it. When authentication fails, see [Troubleshooting](https://chillicream.com/docs/skills/troubleshooting). To keep a work-in-progress skill out of default discovery, set `metadata.internal` to `true` in the frontmatter. The CLI hides internal skills unless the installer explicitly opts in (by filtering for the skill by name, or by setting the `INSTALL_INTERNAL_SKILLS` environment variable). ``` --- name: experimental-skill description: An in-progress skill. Use when testing new conventions before rollout. metadata: internal: "true" --- ``` ## Validate a skill To check a skill against the open spec before you publish, use the reference validator `skills-ref` from the [agentskills/agentskills](https://github.com/agentskills/agentskills) repository. It validates your frontmatter and naming conventions. Bash ``` skills-ref validate ./my-skill ``` It reports a clean pass when the skill is valid, or names the offending field and rule when it is not (for example, an empty `description` or a `name` that breaks the naming rules). Running it after `dnx skills init` and again before you push catches a malformed `name` or an empty `description` while it is still fast to fix, rather than discovering that the CLI skipped your skill on install. ## Next steps - [Installing Skills](https://chillicream.com/docs/skills/installing-skills): install your published skill, target specific agents, and choose project or global scope. - [Reference](https://chillicream.com/docs/skills/reference): the complete command and flag surface, including `dnx skills init`. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/skills/authoring-skills.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Getting Started - Skills > Run the `skills` CLI with `dnx` and add your first Agent Skill to Claude Code or another AI coding agent in five minutes using `dnx skills add`. Canonical source: https://chillicream.com/docs/skills/getting-started By the end of this guide you will have run `skills` and added your first skill to your agent. `skills` is a .NET CLI that installs, updates, and authors [Agent Skills](https://agentskills.io/home), the portable `SKILL.md` files that extend AI coding agents like Claude Code, Cursor, and GitHub Copilot. You point it at a source (a GitHub repo, a git URL, or a local folder), and it places the skill where your agent looks for it. Your agent then loads the skill on demand when a task matches it. This page takes you from zero to one working skill. It takes about five minutes. ## Prerequisites Before you start, make sure you have the following: | Requirement | Why you need it | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [.NET 10 SDK or newer](https://dotnet.microsoft.com/download) | To run skills with dnx, the way these docs show it. The dnx command ships with the .NET 10 SDK. | | An AI coding agent | The target for your skill. This guide uses [Claude Code](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview). The skills CLI supports 55+ agents. | These docs run `skills` with `dnx`, so there is nothing to install first. If you would rather have `skills` on your `PATH`, install the global tool instead (it needs only the .NET SDK 8.0 or newer). See [Install as a global tool](#install-as-a-global-tool). ## Run `skills` These docs run `skills` with `dnx`, which needs no install. You can also install it as a global tool if you prefer `skills` on your `PATH`. ### Run with dnx `dnx` ships with the .NET 10 SDK and runs a tool straight from NuGet, the way `npx` runs a package from npm. Nothing is installed up front. Bash ``` dnx skills add anthropics/skills --agent claude-code ``` On the first run, `dnx` prompts you to confirm the package download before it proceeds: ``` Tool package skills@ will be downloaded from source https://api.nuget.org/v3/index.json. Proceed? [y/n] (y): ``` After you confirm, the install proceeds and prints the summary shown in [Add your first skill](#add-your-first-skill). The first run downloads the package into your NuGet cache; later runs reuse it and start immediately. `dnx skills` is shorthand for `dotnet tool exec skills`. To skip the download prompt (for example in CI), pass `--yes`: Bash ``` dnx --yes skills add anthropics/skills --agent claude-code ``` You can control the `skills` version that `dnx` runs and where it fetches it from: | Command | What it does | | ---------------------------------- | ----------------------------------- | | dnx skills@0.3.0 add ... | Runs an exact version. | | dnx skills@0.3.\* add ... | Runs the latest 0.3.x version. | | dnx --prerelease skills add ... | Allows prerelease versions. | | dnx --source skills add ... | Fetches from a specific NuGet feed. | If your repository has a `.config/dotnet-tools.json` manifest that lists `skills`, `dnx` honors the pinned version from the manifest, which keeps one-shot runs consistent with the version your team committed. ### Install as a global tool If you would rather have `skills` on your `PATH`, install it as a global [.NET tool](https://learn.microsoft.com/dotnet/core/tools/global-tools). This needs the .NET SDK 8.0 or newer. Bash ``` dotnet tool install -g skills ``` ``` You can invoke the tool using the following command: skills Tool 'skills' (version '0.3.0') was successfully installed. ``` With the global tool you run `skills` directly: drop the `dnx` prefix from every example in these docs (`dnx skills add ...` becomes `skills add ...`). Confirm the install with `skills --version`, which prints a value like `0.3.0+` (the trailing suffix records the exact commit the build came from). To pin `skills` to a version your whole team shares, install it as a local tool through a manifest instead. From the repository root: Bash ``` dotnet new tool-manifest dotnet tool install skills ``` Local tools are restored with `dotnet tool restore` and run through `dotnet skills`. Check the manifest (`./.config/dotnet-tools.json`) into source control so every collaborator uses the same version. ## Add your first skill You are ready to install a skill. The example below installs skills from [anthropics/skills](https://github.com/anthropics/skills), Anthropic's reference repository, targeting Claude Code. Bash ``` dnx skills add anthropics/skills --agent claude-code ``` Agent names are case-sensitive. Use `claude-code`, not `claude` or `Claude`. For GitHub Copilot the name is `github-copilot`. The `skills` CLI fetches the source, discovers the skills, installs them, and prints a summary: ``` Source: https://github.com/anthropics/skills.git Found 2 skill(s) ┌─Installation Summary─────────────────────────────────────────────────────────┐ │ Canonical: /your-project/.agents/skills │ │ Symlinked: Claude Code │ └──────────────────────────────────────────────────────────────────────────────┘ ┌─Installed 2 skill(s)─────────────────────────────────────────────────────────┐ │ ✓ pdf │ │ → /your-project/.claude/skills/pdf │ │ ✓ docx │ │ → /your-project/.claude/skills/docx │ └──────────────────────────────────────────────────────────────────────────────┘ Done! Review skills before use; they run with full agent permissions. ``` > The trailing line is a deliberate safety nudge. A skill can include scripts that the agent runs with your agent's full permissions. Install skills only from sources you trust, and read a new skill before you use it. If you omit `--agent`, the CLI runs interactively and lets you pick which agents to target from the ones it detects on your machine. To install only specific skills from a multi-skill source, add `--skill ` (repeatable). For the full set of source forms, scopes, and flags, see [Installing Skills](https://chillicream.com/docs/skills/installing-skills). **Checkpoint.** If everything worked, you should see the `Installed 2 skill(s)` panel and the `Done!` line, with no error. If you saw an `Invalid agents` panel, you misspelled the agent name (it is case-sensitive). If you saw `No valid skills found`, the source had no `SKILL.md` with both a `name` and a `description`. See [Troubleshooting](https://chillicream.com/docs/skills/troubleshooting) for more. ## See what is installed To confirm what landed in your project, list the installed skills: Bash ``` dnx skills list ``` ``` Project Skills Skill Path Agents pdf ./.claude/skills/pdf Claude Code docx ./.claude/skills/docx Claude Code ``` The table shows each skill's name, where it lives on disk, and the agents linked to it (shown by their display name, `Claude Code`). To list global skills instead, add `-g`. For machine-readable output, add `--json`. ## Use the skill The skill now lives where your agent looks for it. You do not need to do anything else. Your agent loads each installed skill's `name` and `description` at startup, then reads the full skill only when a task matches the description. This is [progressive disclosure](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview): the description is the trigger, and the body loads on demand. Start a task that matches the skill, and your agent picks it up automatically. ## Update or remove the global tool If you run `skills` with `dnx`, you never update it yourself: each run fetches the version you ask for. The commands below apply when you installed the global tool. To update the global `skills` tool to the latest version: Bash ``` dotnet tool update -g skills ``` ``` Tool 'skills' was successfully updated from version '0.3.0' to version ''. ``` To uninstall it: Bash ``` dotnet tool uninstall -g skills ``` ``` Tool 'skills' (version '0.3.0') was successfully uninstalled. ``` Note that `dotnet tool update` updates the `skills` CLI, not your installed skills. To check your skills for updates, use the `dnx skills update` command, covered in [Installing Skills](https://chillicream.com/docs/skills/installing-skills). ## Next steps - [Installing Skills](https://chillicream.com/docs/skills/installing-skills): every source form, project versus global scope, picking agents and skills, symlink versus copy, and checking for skill updates. - [Authoring Skills](https://chillicream.com/docs/skills/authoring-skills): scaffold a `SKILL.md` with `dnx skills init`, write a good description, and publish your skill. - [Reference](https://chillicream.com/docs/skills/reference): the complete command and flag surface. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/skills/getting-started.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Installing Skills - Skills > Install Agent Skills from GitHub repos, git URLs, or local folders with `dnx skills add`: the CLI finds every SKILL.md and wires it into your agents. Canonical source: https://chillicream.com/docs/skills/installing-skills `dnx skills add` installs skills from a source into the AI coding agents you already have. Point it at a GitHub repository and the CLI discovers every `SKILL.md`, detects your agents, and wires the skill into each one. Bash ``` dnx skills add anthropics/skills ``` That single command fetches [Anthropic's reference skills](https://github.com/anthropics/skills), then either prompts you to choose which skills and agents you want or, when it is running inside an agent, installs non-interactively. This page walks the full lifecycle: choosing a source, narrowing to specific skills and agents, picking a scope, keeping skills current, and removing them. If you have not installed the CLI yet, start with [Getting Started](https://chillicream.com/docs/skills/getting-started). Warning **Skills run with your agent's full permissions.** A skill is executable instructions plus optional scripts that your agent runs on your machine. Install only from sources you trust, and review a skill before you use it. After every successful install, `skills` reminds you: `Review skills before use; they run with full agent permissions.` ## Choose a source The `source` argument tells the CLI where to fetch skills from. It infers the kind of source from the string, so you pass one positional value and nothing else. The most common form is a GitHub `owner/repo` shorthand: Bash ``` dnx skills add anthropics/skills ``` Every source form below is accepted. The first matching rule wins, and clones are always shallow (the CLI fetches only the latest commit, not the full history). | Source form | Example | What it does | | ---------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------- | | GitHub owner/repo | dnx skills add anthropics/skills | Installs from the repository root. The default shorthand. | | GitHub subpath | dnx skills add anthropics/skills/document-skills/pdf | Installs only the skill at that path in the repo. | | GitHub branch or tag | dnx skills add owner/repo#main | Installs from a specific ref (#branch or #tag). | | GitHub skill filter | dnx skills add owner/repo@my-skill | Installs only the skill named my-skill from the repo. | | GitHub URL | dnx skills add https://github.com/owner/repo | Full URL, with or without a trailing .git. | | GitHub tree URL | dnx skills add https://github.com/owner/repo/tree/main/skills/foo | A /tree// URL pins the ref and subpath. | | GitLab shorthand | dnx skills add gitlab:group/project | Installs from GitLab. Subgroups are supported (group/subgroup/project). | | GitLab URL | dnx skills add https://gitlab.com/group/project/-/tree/main/skills | GitLab uses /-/tree/ (note the \- segment). | | Generic git or SSH | dnx skills add git@github.com:owner/repo.git | Any https, ssh://, git://, or scp-style git transport. | | Local directory | dnx skills add ./my-skills | A local path: ./, ../, an absolute path, or a Windows drive path. | | Well-known HTTP(S) URL | dnx skills add https://example.com/skills | A non-git site that serves skills via .well-known discovery. | A few notes that save time: - A bare name like `my-skills` (no `./` prefix) is treated as `owner/repo` shorthand, not a local path. Use `./my-skills` when you mean a local directory. - You can combine a ref and a skill filter: `owner/repo#main@my-skill`. - Any credentials embedded in a URL are stripped before they appear in output or in the lock file. The `Source:` line that the CLI prints shows `https://@host/...`. > A well-known HTTP source fetches a `.well-known/agent-skills/index.json` (or `.well-known/skills/index.json`) discovery document from the site and downloads the listed skills. Every fetch is pinned to the same origin as the index, and indexes verify a SHA-256 digest per skill. ## Choose which skills to install A source can contain many skills. By default, when you do not pass any filter, the CLI lists what it found and lets you select interactively. To narrow the set up front, use the options below. To preview the available skills without installing anything, pass `--list` (or `-l`): Bash ``` dnx skills add anthropics/skills --list ``` ``` Source: https://github.com/anthropics/skills.git Found 2 skill(s) Available Skills pdf the pdf skill docx the docx skill Use --skill to install specific skills ``` To install one or more specific skills by name, pass `--skill` (or `-s`). The option is repeatable: Bash ``` dnx skills add anthropics/skills --skill pdf --skill docx ``` You can also embed a single skill filter in the source string with `@name`: Bash ``` dnx skills add anthropics/skills@pdf ``` To install every skill from the source into every detected agent, non-interactively, pass `--all`: Bash ``` dnx skills add anthropics/skills --all ``` A successful run prints the `Installation Summary` and `Installed N skill(s)` panels followed by the safety reminder, the same shape shown under [How skills reach each agent](#how-skills-reach-each-agent). `--all` is a shortcut: it selects all skills, targets all agents, and skips every prompt. If a `--skill` filter matches nothing, the CLI lists what was available so you can correct the name, and exits with a non-zero code: ``` Source: /home/you/my-skills Found 2 skill(s) No matching skills found for: nope Available skills: alpha beta ``` A skill is any folder that contains a `SKILL.md` with both a `name` and a `description`. If the source contains no valid skills, the CLI reports `No valid skills found. Skills require a SKILL.md with name and description.` and exits non-zero. See [Authoring Skills](https://chillicream.com/docs/skills/authoring-skills) for the format. ## Choose which agents By default, the CLI detects which agents are installed on your machine and targets them. To choose explicitly, pass `--agent` (or `-a`). The option is repeatable and accepts multiple values per token, and `*` means every supported agent: Bash ``` # repeated flags dnx skills add anthropics/skills --agent claude-code --agent cursor # space-separated values in one token dnx skills add anthropics/skills --agent claude-code cursor # every supported agent dnx skills add anthropics/skills --agent "*" ``` A valid `--agent` run prints the `Installation Summary` and `Installed N skill(s)` panels shown under [How skills reach each agent](#how-skills-reach-each-agent), naming each agent that was linked or copied. The CLI resolves the target agents like this: - **Running inside an agent.** When the CLI runs inside an agent (for example, when your agent invokes it for you), it goes non-interactive and targets that agent plus the shared store. - **Run from your shell, one agent installed.** The CLI auto-selects that agent plus the shared store. - **Run from your shell, several agents installed.** The CLI prompts you to choose, pre-selecting your last-used agents (or the common defaults `claude-code`, `codex`, `opencode` on a first run). - **`--agent` passed.** The CLI uses exactly what you specify, after validation. These are the headline agent identifiers. Names are case-sensitive. | Agent | \--agent identifier | | -------------- | ------------------- | | Claude Code | claude-code | | Cursor | cursor | | GitHub Copilot | github-copilot | | Codex | codex | | Continue | continue | | Gemini CLI | gemini-cli | | Windsurf | windsurf | The `skills` CLI supports 55+ agents. For the complete list, plus the exact directory each agent installs into, see the [Reference](https://chillicream.com/docs/skills/reference). Because identifiers are case-sensitive, `copilot` and `Claude-Code` are not valid. An invalid value fails fast and lists every valid name so you can copy the right one: ``` ┌─Invalid agents───────────────────────────────────────────────────────────────┐ │ Invalid agents: bogus │ │ │ │ Valid agents: adal, aider-desk, amp, antigravity, augment, bob, claude-code, │ │ cline, codearts-agent, codebuddy, codemaker, codestudio, codex, │ │ command-code, continue, cortex, crush, cursor, deepagents, devin, dexto, │ │ droid, firebender, forgecode, gemini-cli, github-copilot, goose, │ │ hermes-agent, iflow-cli, junie, kilo, kimi-cli, kiro-cli, kode, mcpjam, │ │ mistral-vibe, mux, neovate, openclaw, opencode, openhands, pi, pochi, qoder, │ │ qwen-code, replit, roo, rovodev, tabnine-cli, trae, trae-cn, universal, │ │ warp, windsurf, zencoder │ └──────────────────────────────────────────────────────────────────────────────┘ ``` ## Project and global scope The CLI installs into one of two scopes. Understanding the difference tells you where skills live and who shares them. **Project scope is the default.** The CLI records the installed skills in a `skills-lock.json` file in your working directory and materializes the skill folders under `./.agents/skills`. Commit `skills-lock.json` so everyone who clones the repository shares the same skill set. This is the right scope for skills your whole team should use on a given project. **Global scope (`--global` or `-g`) installs skills for your user account**, across every project. Bash ``` dnx skills add anthropics/skills --global ``` In global scope the lock file lives at `~/.local/share/skills/.skill-lock.json` (honoring `XDG_DATA_HOME` if you set it), and the skill folders are materialized under `~/.agents/skills`. In both scopes, the CLI materializes each skill once in a canonical store, then makes each targeted agent point at it. ## How skills reach each agent By default, the CLI writes each skill once into the canonical store and creates a symlink from every targeted agent's directory back to it. Editing the one canonical copy updates the skill for every agent at once, so you maintain a single source of truth. Some agents read directly from the shared `.agents/skills` store, so their skills directory _is_ that store and no symlink is needed. The [Reference](https://chillicream.com/docs/skills/reference) lists which agents read from the shared store and the directory each agent installs into. To copy files instead of symlinking, pass `--copy`. Use it for sandboxed agents that cannot follow symlinks (for example, agents running in containers where symlinks across mounts break): Bash ``` dnx skills add anthropics/skills --copy --agent claude-code --agent windsurf ``` The CLI also copies automatically in two cases, so you rarely need `--copy` by hand: - When a symlink cannot be created (for example, on Windows without the symlink privilege), the CLI falls back to copying for that agent. - When every targeted agent shares one skills directory, the CLI copies, because symlinking only makes sense across distinct directories. A successful install reports which agents were linked or copied and where each skill landed: ``` Source: https://github.com/owner/skills.git Found 1 skill(s) ┌─Installation Summary─────────────────────────────────────────────────────────┐ │ Canonical: /your-project/.agents/skills/alpha │ │ Symlinked: Claude Code, Windsurf │ └──────────────────────────────────────────────────────────────────────────────┘ ┌─Installed 1 skill(s)─────────────────────────────────────────────────────────┐ │ ✓ alpha │ │ → /your-project/.claude/skills/alpha │ └──────────────────────────────────────────────────────────────────────────────┘ Done! Review skills before use; they run with full agent permissions. ``` If everything worked, you should see the `Installation Summary` panel followed by an `Installed N skill(s)` panel and the safety reminder. To confirm later, run `dnx skills list` (see the [Reference](https://chillicream.com/docs/skills/reference)). ## Scan nested skills with --full-depth By default, the CLI installs the skill at the source root if one exists there. If the root is not itself a skill, it discovers every nested skill instead. That fast path covers most repositories. To find skills in nested and curated directories that the default scan does not reach, pass `--full-depth`: Bash ``` dnx skills add owner/curated-skills --full-depth ``` The install reports the same `Installation Summary` and `Installed N skill(s)` panels shown under [How skills reach each agent](#how-skills-reach-each-agent); only the set of discovered skills differs. `--full-depth` widens _discovery_: it scans nested directories and curated locations (such as `skills/.curated`) so the CLI finds skills the default scan would skip. It does not change clone depth. Clones are always shallow regardless of this flag. ## Install from a private repository The CLI never manages or prompts for credentials. It shells out to `git`, which uses your own setup: SSH agent keys, a git credential helper, `gh` authentication, or `~/.netrc`. To avoid hanging on a missing credential, it sets `GIT_TERMINAL_PROMPT=0`, so an unauthenticated clone fails fast with a clear error instead of waiting at a prompt. To install from a private repository, make sure your normal git auth already works, then run `add` as usual: Bash ``` dnx skills add owner/private-skills --agent claude-code ``` This prints the same `Installation Summary` and `Installed N skill(s)` panels shown under [How skills reach each agent](#how-skills-reach-each-agent), once the clone authenticates. Before you install, verify your access with one of: Bash ``` ssh -T git@github.com # confirm your SSH key reaches GitHub gh auth login # or authenticate the GitHub CLI ``` A working SSH key prints the well-known success line (GitHub closes the connection because it does not provide shell access): ``` Hi ! You've successfully authenticated, but GitHub does not provide shell access. ``` If a clone fails with an authentication error, the CLI prints guidance pointing you back to your git credentials. See [Troubleshooting](https://chillicream.com/docs/skills/troubleshooting) for the full list of clone and auth failures. ## Keep skills up to date `dnx skills update` checks your installed skills for newer versions. It has the aliases `upgrade` and `check`. Bash ``` dnx skills update -g ``` ``` Checking for skill updates... Checking global skill 1/1: my-skill Found 1 global update(s) Update available: my-skill Run: skills add owner/repo/skills/my-skill -g -y Updates available for 1 skill(s); no updates were applied. ``` **`update` only reports; it never applies changes.** It prints the exact `skills add ... -y` command to run for each skill that has an update (prefix it with `dnx` when you run through `dnx`). Copy that command and run it yourself to apply the update. The output is explicit: `no updates were applied.` Scope the check the same way you scope an install: - `-g` (or `--global`) checks global skills only. - `-p` (or `--project`) checks project skills only. - Pass both, or name specific skills, to check both scopes. - With no scope flag in an interactive shell, the CLI prompts you to choose. Non-interactively (for example with `-y`), it checks both scopes. When there is nothing to update, you get a clean report: ``` Checking for skill updates... Checking global skill 1/1: my-skill All global skills are up to date ``` The update check calls the GitHub API. If you hit a rate limit, set a `GITHUB_TOKEN` or `GH_TOKEN` environment variable (or sign in with `gh auth login`) and the CLI uses it for the check. See [Troubleshooting](https://chillicream.com/docs/skills/troubleshooting) for rate-limit and timeout details. ## Remove skills `dnx skills remove` uninstalls skills. It unlinks each agent's copy and removes the canonical store entry once no remaining agent references it, then updates the lock file. To remove specific skills by name, list them: Bash ``` dnx skills remove alpha --yes ``` ``` Successfully removed 1 skill(s) ``` To remove everything, pass `--all`: Bash ``` dnx skills remove --all ``` ``` Successfully removed 1 skill(s) ``` To limit removal to specific agents, add `--agent`. To target your user-level skills, add `--global`. Run `dnx skills remove` with no names to remove interactively: the CLI shows a multi-select prompt, then a confirmation that defaults to **no**. Declining cancels and removes nothing: ``` $ dnx skills remove Removal cancelled ``` If a named skill is not installed, the CLI tells you and exits cleanly without changing anything: ``` No matching skills found for: nope ``` ## Next steps - [Authoring Skills](https://chillicream.com/docs/skills/authoring-skills): write and publish your own `SKILL.md` with `dnx skills init`. - [Reference](https://chillicream.com/docs/skills/reference): every command, flag, the full list of 55+ agents, and per-agent install directories. - [Troubleshooting](https://chillicream.com/docs/skills/troubleshooting): clone failures, authentication errors, rate limits, and symlink fallbacks. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/skills/installing-skills.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Reference - Skills > Complete `skills` CLI reference covering every command and flag, JSON output, exit codes, environment variables, file locations, and supported agents. Canonical source: https://chillicream.com/docs/skills/reference This page is the complete, precise reference for the `skills` command surface, JSON output, exit codes, environment variables, file locations, and supported agents. It is meant for lookup, not for learning. To learn by doing, start at [Installing Skills](https://chillicream.com/docs/skills/installing-skills) and [Authoring Skills](https://chillicream.com/docs/skills/authoring-skills). Every command prints the same per-command help at the terminal. Run `dnx skills --help` to see the synopsis, arguments, and options for that command. Bash ``` dnx skills add --help ``` ``` Description: Add a skill from a source Usage: skills add [] [options] Arguments: Source to fetch skills from (e.g., owner/repo, URL, local path) Options: -g, --global Install globally -a, --agent Target agent(s) -s, --skill Skill name filter(s) -y, --yes Skip prompts (non-interactive) --all Install all skills to all agents --copy Copy instead of symlinking --full-depth Scan nested directories for skills -l, --list List available skills without installing -?, -h, --help Show help and usage information ``` > The binary on your PATH is `skills`. Install it with `dotnet tool install -g skills`, or run it without installing via `dnx skills ` (requires the .NET 10 SDK or later). Both forms accept the same commands, arguments, and options documented below. ## Commands `skills` has five commands: `add`, `remove`, `list`, `update`, and `init`. Each command's options are independent. There are no shared global options beyond `--help` and `--version` (see [Global flags](#global-flags)). ### skills add Add skills from a source. Skills are discovered at the source, then materialized into a canonical store and linked into each target agent's skills directory. ``` dnx skills add [options] ``` #### Arguments | Argument | Arity | Required | Description | | -------- | ----- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | source | one | yes | Source to fetch skills from (for example owner/repo, a full git URL, or a local path). If omitted, the command exits 1 with Missing required argument: source. | #### Options | Option | Alias | Type | Default | Description | | ------------- | ------ | ------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \--global | \-g | bool | false | Install to the global scope (your home directory) instead of the current project. | | \--agent | \-a | string (repeatable) | (auto-detected) | Target agent(s). Repeatable and space-separated (\--agent claude-code cursor or \--agent claude-code --agent cursor). \--agent \* targets every agent. See [Supported agents](#supported-agents). | | \--skill | \-s | string (repeatable) | (all) | Install only skills whose name matches the given filter(s). Repeatable. | | \--yes | \-y | bool | false | Skip all prompts and run non-interactively. | | \--all | (none) | bool | false | Install all skills to all agents. Implies \--skill \*, \--agent \*, and \--yes. | | \--copy | (none) | bool | false | Copy skill files into each agent directory instead of symlinking. Use for agents that do not follow symlinks. | | \--full-depth | (none) | bool | false | Widen discovery to scan nested directories in the source, not only the source root. This does not change the git clone (clones are always shallow). | | \--list | \-l | bool | false | List the available skills at the source without installing anything, then exit. | The default install mode is symlink: the CLI materializes each skill once into a canonical store and creates a relative symlink from each agent directory back to it. Pass `--copy` to copy instead. When every selected agent shares one skills directory, copy is used automatically. For sources, scopes, and install mechanics, see [Installing Skills](https://chillicream.com/docs/skills/installing-skills). For agent targeting, see [Supported agents](#supported-agents). ### skills remove \[skills...\] Remove installed skills. With no skill names and an interactive terminal, the CLI prompts you to select which skills to remove. ``` dnx skills remove [skills...] [options] ``` #### Arguments | Argument | Arity | Required | Description | | -------- | ------------ | -------- | --------------------------------------------------------------------------------------------------- | | skills | zero or more | no | Skill names to remove. Matched case-insensitively. With no names and no \--all, runs interactively. | #### Options | Option | Alias | Type | Default | Description | | --------- | ------ | ------------------- | ------- | ------------------------------------------------------------ | | \--global | \-g | bool | false | Remove from the global scope instead of the current project. | | \--agent | \-a | string (repeatable) | (all) | Limit removal to specific agent(s). Repeatable. | | \--yes | \-y | bool | false | Skip prompts and run non-interactively. | | \--all | (none) | bool | false | Remove every installed skill. | In interactive mode, the CLI shows a multiselect prompt followed by a confirmation prompt. The confirmation defaults to no. Declining the confirmation prints `Removal cancelled` and exits with code 130 (see [Exit codes](#exit-codes)). ``` $ dnx skills remove alpha # exit 130 Removal cancelled ``` ### skills list List installed skills. With no options, lists the current project's skills. ``` dnx skills list [options] ``` This command takes no positional arguments. #### Options | Option | Alias | Type | Default | Description | | --------- | ------ | ------------------- | ------- | ----------------------------------------------------------------------------------- | | \--global | \-g | bool | false | List skills in the global scope instead of the current project. | | \--agent | \-a | string (repeatable) | (none) | Filter the listing to specific agent(s). Repeatable. | | \--format | (none) | text \| json | text | Output format. json emits a JSON array to stdout (see [JSON output](#json-output)). | | \--json | (none) | bool | false | Shorthand for \--format json. | JSON output is enabled when either `--json` is present or `--format json` is set (case-insensitive). Enabling JSON suppresses the banner and writes the array to stdout. ### skills update \[skills...\] Check for available updates and print the exact command to apply each one. ``` dnx skills update [skills...] [options] ``` > Aliases: `upgrade` and `check`. `dnx skills upgrade` and `dnx skills check` are identical to `dnx skills update`. Warning **`update` only reports.** It never modifies files or lock files. For each skill that has an update, it prints a `skills add ...` command you can copy and run to apply it (run it as `dnx skills add ...` if you use `dnx`). The command always exits 0, even when updates are available. Its output ends with `no updates were applied.` #### Arguments | Argument | Arity | Required | Description | | -------- | ------------ | -------- | ------------------------------------------------------------------------------------------------------------------ | | skills | zero or more | no | Optional skill names to check. Scope is resolved the same way with or without names (see the scope options below). | #### Options | Option | Alias | Type | Default | Description | | ---------- | ----- | ---- | ------- | -------------------------------------------------------------------------- | | \--global | \-g | bool | false | Check global skills only. | | \--project | \-p | bool | false | Check project skills only. | | \--yes | \-y | bool | false | Skip the interactive scope prompt. Non-interactive runs check both scopes. | With neither `-g` nor `-p`, an interactive terminal prompts you to choose Project, Global, or Both. Passing both flags, or running non-interactively, checks both scopes. ``` $ dnx skills update -g Checking for skill updates... Checking global skill 1/1: my-skill Found 1 global update(s) Update available: my-skill Run: skills add owner/repo/skills/my-skill -g -y Updates available for 1 skill(s); no updates were applied. ``` ### skills init \[name\] Scaffold a new skill directory containing a `SKILL.md` template. This command has no options. ``` dnx skills init [name] ``` #### Arguments | Argument | Arity | Required | Description | | -------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | name | zero or one | no | Skill name. Creates /SKILL.md. The name is sanitized to a slug. With no name, derives the slug from the current directory and writes SKILL.md in place. | If the target `SKILL.md` already exists, the CLI does not overwrite it. It prints `Skill already exists at ` and exits 0. ``` $ dnx skills init my-skill Initialized skill: my-skill Created: my-skill/SKILL.md Next steps: 1. Edit my-skill/SKILL.md to define your skill instructions 2. Update the name and description in the frontmatter Publishing: GitHub: Push to a repo, then skills add / URL: Host the file, then skills add https://example.com/my-skill/SKILL.md ``` See [Authoring Skills](https://chillicream.com/docs/skills/authoring-skills) for the `SKILL.md` format and publishing workflow. ### Global flags These are provided on the root command and on every subcommand. | Option | Aliases | Type | Description | | ---------- | -------- | ---- | ------------------------------------------------------ | | \--help | \-h, \-? | bool | Show help. On a subcommand, shows that command's help. | | \--version | (none) | bool | Print the installed CLI version and exit 0. | Running `skills` with no arguments shows the banner and exits 0\. Running `dnx skills --help` shows curated top-level help. A bare `--` token is removed before parsing, so `dnx skills add --agent codex -- owner/repo` parses the same as without it. ## JSON output `dnx skills list --json` (equivalently `dnx skills list --format json`) prints a JSON array to stdout. Each element describes one installed skill. Bash ``` dnx skills list --json ``` JSON ``` [ { "name": "alpha", "path": "/home/you/project/.agents/skills/alpha", "scope": "project", "agents": ["Claude Code", "Windsurf"] } ] ``` | Field | Type | Description | | ------ | ---------- | ------------------------------------------------------------------------------------------------------------------------ | | name | string | The skill's sanitized name (its on-disk directory name). | | path | string | Absolute path to the skill in the canonical store. | | scope | string | "project" or "global". "global" when listed with \-g, otherwise "project". | | agents | string\[\] | Display names of agents linked to this skill (for example Claude Code). Empty when the skill is not linked to any agent. | The array is written to stdout, so you can pipe it to a tool such as `jq`. Banner and progress output are suppressed in JSON mode. ## Exit codes `skills` uses exactly three exit codes. | Code | Name | Meaning | | ---- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | 0 | Success | The command completed. Note that update exits 0 even when updates are available, and remove exits 0 when there is nothing to remove. | | 1 | Failure | The command failed (for example an invalid agent name, no valid skills found, or a file system error). The error message is written to stderr. | | 130 | Cancelled | The operation was cancelled, either by declining an interactive confirmation or by pressing Ctrl+C. | ## Environment variables | Variable | Default | Effect | | -------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | SKILLS\_CLONE\_TIMEOUT\_MS | 300000 | Git clone timeout in milliseconds (5 minutes). Must parse to a positive integer, otherwise the default is used. Raise it for large repositories or slow networks. | | GITHUB\_TOKEN | (unset) | A GitHub API token. Used only by update to raise GitHub API rate limits when checking for updates. Not used for cloning. | | GH\_TOKEN | (unset) | Fallback GitHub API token. Consulted by update after GITHUB\_TOKEN. Same effect. | | INSTALL\_INTERNAL\_SKILLS | (unset) | Set to 1 or true to include skills their authors marked as internal in discovery. By default such skills are hidden. | | CLAUDE\_CONFIG\_DIR | \~/.claude | Overrides the Claude Code configuration directory used for detection and global installs. | | CODEX\_HOME | \~/.codex | Overrides the Codex configuration directory used for detection and global installs. | | VIBE\_HOME | \~/.vibe | Overrides the Mistral Vibe configuration directory used for detection and global installs. | | XDG\_DATA\_HOME | \~/.local/share | Root for the global lock location. The global lock lives at $XDG\_DATA\_HOME/skills/.skill-lock.json. | `XDG_CONFIG_HOME` (default `~/.config`) and `XDG_STATE_HOME` (default `~/.local/state`) affect where the CLI keeps its own configuration and logs, and where several agents resolve their global directories. For private repositories, the CLI shells out to `git` and uses your existing git credentials (SSH agent, credential helper, `gh` auth). See [Troubleshooting](https://chillicream.com/docs/skills/troubleshooting) for authentication and rate-limit guidance. ## File locations The CLI writes a lock file per scope and materializes skills into a canonical store. Project scope is the current working directory; global scope is your home directory. | Scope | Lock file | Canonical skills store | | ------- | ------------------------------------------------------------------------------------- | ---------------------- | | Project | skills-lock.json (in the current working directory) | ./.agents/skills | | Global | \~/.local/share/skills/.skill-lock.json (or $XDG\_DATA\_HOME/skills/.skill-lock.json) | \~/.agents/skills | Non-universal agents receive a symlink from their own directory (for example `.claude/skills/`) back into the canonical store. Universal agents use the canonical store directly. See [Supported agents](#supported-agents) for per-agent directories. ## Supported agents The `skills` CLI supports 55+ agents. It detects which agents are installed on your machine by probing each agent's configuration directory, and it detects which agent it is running inside by reading environment variables that agent hosts set. When run inside a detected agent, `add` runs non-interactively and targets that agent plus the universal agents. Agent identifiers are case-sensitive (the identifier is `github-copilot`, not `copilot`). The `--agent` option is repeatable and accepts multiple values per token. `--agent *` targets all agents. "Universal" agents share the `.agents/skills` store, so installing a skill for one universal agent makes it visible to all of them. In the table below, project directories are relative to the working directory and global directories are absolute (`~` is your home directory, `` is `$XDG_CONFIG_HOME` or `~/.config`). Agents marked "universal" use the shared `.agents/skills` store at project scope. | Identifier | Display name | Project directory | Global directory | | -------------- | ------------------ | -------------------------- | ------------------------------------------- | | adal | AdaL | .adal/skills | \~/.adal/skills | | aider-desk | AiderDesk | .aider-desk/skills | \~/.aider-desk/skills | | amp | Amp | .agents/skills (universal) | /agents/skills | | antigravity | Antigravity | .agents/skills (universal) | \~/.gemini/antigravity/skills | | augment | Augment | .augment/skills | \~/.augment/skills | | bob | IBM Bob | .bob/skills | \~/.bob/skills | | claude-code | Claude Code | .claude/skills | \~/.claude/skills (via CLAUDE\_CONFIG\_DIR) | | cline | Cline | .agents/skills (universal) | \~/.agents/skills | | codearts-agent | CodeArts Agent | .codeartsdoer/skills | \~/.codeartsdoer/skills | | codebuddy | CodeBuddy | .codebuddy/skills | \~/.codebuddy/skills | | codemaker | Codemaker | .codemaker/skills | \~/.codemaker/skills | | codestudio | Code Studio | .codestudio/skills | \~/.codestudio/skills | | codex | Codex | .agents/skills (universal) | \~/.codex/skills (via CODEX\_HOME) | | command-code | Command Code | .commandcode/skills | \~/.commandcode/skills | | continue | Continue | .continue/skills | \~/.continue/skills | | cortex | Cortex Code | .cortex/skills | \~/.snowflake/cortex/skills | | crush | Crush | .crush/skills | \~/.config/crush/skills | | cursor | Cursor | .agents/skills (universal) | \~/.cursor/skills | | deepagents | Deep Agents | .agents/skills (universal) | \~/.deepagents/agent/skills | | devin | Devin for Terminal | .devin/skills | /devin/skills | | dexto | Dexto | .agents/skills (universal) | \~/.agents/skills | | droid | Droid | .factory/skills | \~/.factory/skills | | firebender | Firebender | .agents/skills (universal) | \~/.firebender/skills | | forgecode | ForgeCode | .forge/skills | \~/.forge/skills | | gemini-cli | Gemini CLI | .agents/skills (universal) | \~/.gemini/skills | | github-copilot | GitHub Copilot | .agents/skills (universal) | \~/.copilot/skills | | goose | Goose | .goose/skills | /goose/skills | | hermes-agent | Hermes Agent | .hermes/skills | \~/.hermes/skills | | iflow-cli | iFlow CLI | .iflow/skills | \~/.iflow/skills | | junie | Junie | .junie/skills | \~/.junie/skills | | kilo | Kilo Code | .kilocode/skills | \~/.kilocode/skills | | kimi-cli | Kimi Code CLI | .agents/skills (universal) | \~/.config/agents/skills | | kiro-cli | Kiro CLI | .kiro/skills | \~/.kiro/skills | | kode | Kode | .kode/skills | \~/.kode/skills | | mcpjam | MCPJam | .mcpjam/skills | \~/.mcpjam/skills | | mistral-vibe | Mistral Vibe | .vibe/skills | \~/.vibe/skills (via VIBE\_HOME) | | mux | Mux | .mux/skills | \~/.mux/skills | | neovate | Neovate | .neovate/skills | \~/.neovate/skills | | openclaw | OpenClaw | skills | \~/.openclaw/skills | | opencode | OpenCode | .agents/skills (universal) | /opencode/skills | | openhands | OpenHands | .openhands/skills | \~/.openhands/skills | | pi | Pi | .pi/skills | \~/.pi/agent/skills | | pochi | Pochi | .pochi/skills | \~/.pochi/skills | | qoder | Qoder | .qoder/skills | \~/.qoder/skills | | qwen-code | Qwen Code | .qwen/skills | \~/.qwen/skills | | replit | Replit | .agents/skills (universal) | /agents/skills | | roo | Roo Code | .roo/skills | \~/.roo/skills | | rovodev | Rovo Dev | .rovodev/skills | \~/.rovodev/skills | | tabnine-cli | Tabnine CLI | .tabnine/agent/skills | \~/.tabnine/agent/skills | | trae | Trae | .trae/skills | \~/.trae/skills | | trae-cn | Trae CN | .trae/skills | \~/.trae-cn/skills | | universal | Universal | .agents/skills (universal) | /agents/skills | | warp | Warp | .agents/skills (universal) | \~/.agents/skills | | windsurf | Windsurf | .windsurf/skills | \~/.codeium/windsurf/skills | | zencoder | Zencoder | .zencoder/skills | \~/.zencoder/skills | Passing an unknown identifier exits 1 and prints the full list of valid identifiers: ``` $ dnx skills add ./local-path --yes --agent bogus # exit 1 Invalid agents: bogus ``` ## See also - [Installing Skills](https://chillicream.com/docs/skills/installing-skills): sources, scopes, and the install workflow in depth. - [Authoring Skills](https://chillicream.com/docs/skills/authoring-skills): the `SKILL.md` format, `dnx skills init`, and publishing. - [Troubleshooting](https://chillicream.com/docs/skills/troubleshooting): authentication, rate limits, timeouts, and common errors. [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/skills/reference.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # Troubleshooting - Skills > Troubleshoot `skills` CLI errors: each section maps the exact message the CLI prints to its cause and a copy-pasteable fix for installing and updating skills. Canonical source: https://chillicream.com/docs/skills/troubleshooting This page maps the errors and surprises you hit when running `skills` to their cause and fix. Each section starts with the exact message the CLI prints (where one exists), explains why it happens, and gives you a copy-pasteable way out. If you are new to `skills`, start with [Getting Started](https://chillicream.com/docs/skills/getting-started). For the full command and flag surface, see the [Reference](https://chillicream.com/docs/skills/reference). For what each agent identifier means and where skills land, see [Installing Skills](https://chillicream.com/docs/skills/installing-skills). ## "No valid skills found. Skills require a SKILL.md with name and description." You ran `dnx skills add ` and the CLI reached the source but found nothing to install: ``` $ dnx skills add ./my-skills --yes --agent claude-code # exit 1 Source: /home/you/my-skills No valid skills found. Skills require a SKILL.md with name and description. ``` Cause: a skill is a directory that contains a `SKILL.md` file with both `name` and `description` in its YAML frontmatter. The CLI silently skips any folder that is missing the file, missing either field, or whose `name`/`description` collapses to empty. By default, it only looks at the source root (or the subpath you pointed it at), so a `SKILL.md` buried in a nested directory is not discovered. To see exactly what the CLI finds without installing anything, run `--list`: Bash ``` dnx skills add owner/repo --list ``` ``` Source: https://github.com/owner/repo.git Found 2 skill(s) Available Skills alpha the alpha skill beta the beta skill Use --skill to install specific skills ``` If `--list` reports `Found 0 skill(s)`, work through these in order: | Check | Fix | | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | Is your path or subpath pointing at the right folder? | Point at the directory that contains SKILL.md, or add the repo subpath: dnx skills add owner/repo/skills/my-skill. | | Does the SKILL.md have both name: and description:? | Add the missing field. Both must be non-empty. See [Authoring Skills](https://chillicream.com/docs/skills/authoring-skills). | | Are the skills in nested directories? | Add \--full-depth to scan nested directories: dnx skills add owner/repo --full-depth --list. | > `--full-depth` widens discovery so the CLI scans nested directories for skills. It does not change how much of the repository is cloned. Clones are always shallow regardless of this flag. ## "Invalid agents: " You passed an `--agent` value that the CLI does not recognize: ``` $ dnx skills add ./my-skills --yes --agent bogus # exit 1 Source: /home/you/my-skills Found 1 skill(s) ┌─Invalid agents───────────────────────────────────────────────────────────────┐ │ Invalid agents: bogus │ │ │ │ Valid agents: adal, aider-desk, amp, antigravity, augment, bob, claude-code, │ │ cline, codearts-agent, codebuddy, codemaker, codestudio, codex, │ │ command-code, continue, cortex, crush, cursor, deepagents, devin, dexto, │ │ droid, firebender, forgecode, gemini-cli, github-copilot, goose, │ │ hermes-agent, iflow-cli, junie, kilo, kimi-cli, kiro-cli, kode, mcpjam, │ │ mistral-vibe, mux, neovate, openclaw, opencode, openhands, pi, pochi, qoder, │ │ qwen-code, replit, roo, rovodev, tabnine-cli, trae, trae-cn, universal, │ │ warp, windsurf, zencoder │ └──────────────────────────────────────────────────────────────────────────────┘ ``` Cause: the value is misspelled or uses the wrong case. Agent identifiers are case-sensitive, and several differ from the product's marketing name. The most common mistakes: | You typed | Use instead | | ------------------------ | -------------- | | copilot, github\_copilot | github-copilot | | Claude-Code, claude | claude-code | | gemini | gemini-cli | | roo-code | roo | The error panel always prints every valid identifier, so copy the correct one from the list. The same `Invalid agents: ` message appears (without the panel) from `dnx skills list` and `dnx skills remove`. For the full table of identifiers and where each one installs skills, see [Installing Skills](https://chillicream.com/docs/skills/installing-skills) and the [Reference](https://chillicream.com/docs/skills/reference). ## Cloning a private repository fails with "Authentication failed" `dnx skills add` reports an authentication error and stops before installing. Cause: the CLI never prompts for credentials. It shells out to your own `git`, which uses your existing setup (SSH agent keys, a git credential helper, `gh` auth, or `~/.netrc`). When git cannot authenticate, the CLI surfaces the failure instead of hanging. Any credentials embedded in a URL are redacted from the output, so secrets never appear in errors or in the lock file. To fix it, confirm your machine can reach the repository, then retry the same `dnx skills add` command. For SSH sources (`git@github.com:owner/repo.git`): Bash ``` ssh -T git@github.com # Hi ! You've successfully authenticated, but GitHub does not provide shell access. ``` For HTTPS sources, authenticate with the GitHub CLI or configure a git credential helper. These are one-time setup commands whose output varies by environment: Bash ``` gh auth login # uses the gh credential helper # or git config --global credential.helper store ``` After `gh auth login` completes, it confirms the authenticated account. Once `git clone ` succeeds on its own, `dnx skills add ` will succeed too. ## The clone times out A large repository can exceed the default clone budget and cause the CLI to abort the fetch. Cause: the CLI caps each clone at 5 minutes (300000 ms). The clone is always shallow (latest commit only), but a very large repository or a slow connection can still run past the limit. To raise the timeout, set `SKILLS_CLONE_TIMEOUT_MS` in milliseconds before running the command: Bash ``` SKILLS_CLONE_TIMEOUT_MS=600000 dnx skills add owner/big-repo --agent claude-code # 10 minutes ``` With the longer budget the clone finishes and the install completes with the usual `Installed 1 skill(s)` panel. Alternatively, clone the repository yourself and point `skills` at the local copy. This skips the network step entirely: Bash ``` git clone --depth 1 https://github.com/owner/big-repo.git dnx skills add ./big-repo --agent claude-code ``` Pointing `skills` at the local clone installs the skill without touching the network and prints the same `Installed 1 skill(s)` panel. ## A skill does not appear for one agent `dnx skills add` reports success, the skill shows up in `dnx skills list`, but one of your agents does not see it. Cause: agents that keep skills in their own directory (for example `.claude/skills` for Claude Code or `.windsurf/skills` for Windsurf) only get a link when that agent's directory already exists in your project. The CLI will not create a config directory for an agent you do not use. When the directory is missing, it skips the link for that agent but still materializes the skill in the shared `.agents/skills` store, so the skill is installed; it is only the per-agent link that is missing. Pick whichever fix matches your intent: | Goal | Fix | | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | You do use that agent in this project | Create its directory, then re-run the install: mkdir -p .claude && dnx skills add owner/repo --agent claude-code. | | You want a self-contained copy regardless of agent directories | Add \--copy to write the skill straight into each agent's directory: dnx skills add owner/repo --agent claude-code --copy. | | You meant a different agent | Re-run with the correct \--agent value. See [Installing Skills](https://chillicream.com/docs/skills/installing-skills). | After the directory exists, re-running the install links the skill into `.claude/skills`. The Installation Summary then lists `Symlinked: Claude Code` instead of skipping that agent. Agents that share the universal `.agents/skills` store (Cursor, Codex, GitHub Copilot, Gemini CLI, and others) always see the skill because their directory is the canonical store. ## Symlinks are not created (Windows) On Windows, `skills` copies skills into agent directories instead of linking them. Cause: creating a symlink on Windows requires either an elevated process or Developer Mode. When the CLI cannot create the link, it automatically falls back to copying the skill so the install still succeeds. The skill works either way; the only difference is that copies do not share a single editable source of truth the way symlinks do. To get symlinks instead of copies, choose one: - Enable Developer Mode (Settings, "For developers", "Developer Mode"), then re-run the command. - Run your terminal elevated ("Run as administrator") and re-run the command. If you prefer copies and want the CLI to copy without attempting a symlink first, pass `--copy` explicitly: Bash ``` dnx skills add owner/repo --agent claude-code --copy ``` The output shows the skill was copied rather than linked: the Installation Summary lists `Copied: Claude Code`, and the skill is written directly into `.claude/skills`. ## "skills update" cannot check a skill or hits a rate limit `dnx skills update` reports that one or more skills could not be checked: ``` $ dnx skills update -g Checking for skill updates... 1 skill(s) cannot be checked automatically: * legacy (Private or deleted repo) To update: skills add https://github.com/owner/repo -g -y ``` Cause: `update` checks for newer versions by calling the GitHub API, and unauthenticated calls are rate-limited. It also cannot diff every kind of source. Skills installed from a local path, a generic git URL, a well-known HTTP source, or a private or deleted repository have no checkable remote tree, so they are reported as "cannot be checked automatically" with a manual refresh hint. To raise the rate limit, give `skills` a GitHub token and re-run. The CLI reads `GITHUB_TOKEN`, then `GH_TOKEN`, then falls back to `gh auth token`: Bash ``` export GITHUB_TOKEN=ghp_... # or: gh auth login dnx skills update ``` With the token in place the rate limit clears and the check completes: ``` Checking for skill updates... All skills are up to date. ``` For sources that can never be checked automatically, re-install with `dnx skills add` to refresh them. The `update` output prints the exact command to run for each one: Bash ``` dnx skills add owner/repo/skills/my-skill -y ``` The refresh re-installs the skill from its source and prints the usual `Installed 1 skill(s)` panel. > `dnx skills update` only checks for and reports available updates. It never applies them. Whatever it finds, it prints the precise `skills add ...` command for you to run (prefix it with `dnx` when you use `dnx`). The aliases `dnx skills upgrade` and `dnx skills check` behave identically. See the [Reference](https://chillicream.com/docs/skills/reference). ## "... is newer than this skills supports" A command refuses to write to a lock file and reports that the on-disk version is newer than this version of `skills` supports. Cause: the lock file (`skills-lock.json` in your project, or the global `.skill-lock.json`) was written by a newer version of `skills` than the one you are running. The CLI reads newer lock files but refuses to modify them, so it does not corrupt state written by a version it does not understand. To fix it, update `skills` to the latest release: Bash ``` dotnet tool update -g skills ``` A successful upgrade reports the version change: ``` Tool 'skills' was successfully updated from version 0.1.0 to version 0.3.0. ``` If you no longer need the existing lock state and want to start fresh, delete the lock file and re-install your skills. The project lock lives at `skills-lock.json` in your working directory; the global lock lives at `~/.local/share/skills/.skill-lock.json` (or under `$XDG_DATA_HOME/skills` if that variable is set). Warning **Deleting a lock file discards update tracking.** The CLI uses the lock file to know what is installed and to check for updates. After deleting it, run `dnx skills add` again for each skill so the CLI can rebuild the lock. The installed skill folders themselves are not removed by deleting the lock. ## Bundled binary assets are missing from a skill A skill installs, but a binary file it ships (an image, a model, a compiled tool) is a tiny placeholder or absent. Cause: the CLI disables Git LFS while cloning, so only LFS pointer files are fetched, not the large binaries they track. This keeps clones fast and shallow, but it means LFS-tracked assets do not arrive with the skill. To fix it, change how the skill stores its assets: - Store skill assets without Git LFS so they travel as ordinary files in the repository. - Vendor the assets directly into the skill folder (commit the real files alongside `SKILL.md`). If you author the skill yourself, see [Authoring Skills](https://chillicream.com/docs/skills/authoring-skills) for how to lay out bundled resources. ## "Failed to remove" a skill `dnx skills remove` reports that it could not remove a skill: ``` $ dnx skills remove "My Skill" --yes # exit 1 Failed to remove 1 skill(s) My Skill: ... ``` Cause: the CLI removes a skill by the sanitized, lowercase, hyphenated name it uses on disk (for example `my-skill`). A folder created outside `skills` whose name does not match that sanitized form is not something the CLI will delete automatically, so it reports the failure rather than falsely claiming success. The CLI also refuses to delete a real non-empty directory or file it did not create, which protects your own data. To fix it, delete the folder yourself from the skills directory. For a project install that is `./.agents/skills/`; for the corresponding agent directory it is, for example, `./.claude/skills/`. For a global install the canonical store is `~/.agents/skills/`. Bash ``` rm -rf "./.agents/skills/My Skill" ``` Run `dnx skills list` afterward to confirm the skill is gone. The listing no longer shows the removed skill: ``` $ dnx skills list No project skills found. Try listing global skills with -g ``` ## Next steps - [Reference](https://chillicream.com/docs/skills/reference): every command, flag, exit code, and environment variable in one place. - [Installing Skills](https://chillicream.com/docs/skills/installing-skills): sources, scopes, agent targeting, and the symlink versus copy model. - Still stuck? Ask in the [ChilliCream Slack](https://slack.chillicream.com/). [Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/skills/troubleshooting.md) Maintained by ChilliCream. Last updated on **August 17, 2026** by **PascalSenn** --- # GraphQL Help for Hot Chocolate and Fusion > Find GraphQL help for Hot Chocolate, Fusion, and Nitro through documentation, the ChilliCream community, an advisory engagement, or a support plan. Canonical source: https://chillicream.com/help GraphQL help Use the documentation and open community for everyday questions, bring a defined technical problem to an advisory engagement, or choose a support plan for ongoing production coverage. [Explore GraphQL advisory](https://chillicream.com/services/advisory) [Join community Slack](https://slack.chillicream.com/) Three paths ## Choose the help that matches your situation. Community for open questions, advisory for getting unstuck on a defined problem, support for teams that depend on GraphQL in production. ### Community Ask public questions and learn from other ChilliCream users. Free - Public Slack channel - Open GitHub discussions - Searchable history - Best-effort responses [Join community Slack](https://slack.chillicream.com/) ### Advisory Bring a GraphQL problem to an expert and get clear direction. 20h increments - Architecture and schema design - Troubleshooting and review - Hot Chocolate and Fusion expertise - Agreed package of hours [Explore advisory](https://chillicream.com/services/advisory) ### Support Ongoing coverage for teams running GraphQL in production. From $450 per month - Private channel on paid plans - Defined incident allowances - Published response times - Coverage options by plan [Compare support plans](https://chillicream.com/services/support) First stop ## Try the self-serve resources before you ask. Most questions have already been answered. Five places to look before you reach out. [DocsGuides, recipes, and the full Hot Chocolate and Fusion reference.](https://chillicream.com/docs) [BlogRelease notes, deep dives, and patterns from the team.](https://chillicream.com/blog) [SlackPublic, best-effort help from maintainers and other users.](https://slack.chillicream.com/) [YouTubeWorkshops, talks, and walkthroughs from the ChilliCream team.](https://www.youtube.com/c/ChilliCream) [GitHubSource code, tracked issues, and technical discussions.](https://github.com/ChilliCream/graphql-platform) FAQ ## Answers to common questions. Where should I start? Start with the docs, GitHub, or community Slack when the question can be handled in public and does not need a response guarantee. Choose advisory for a defined technical problem or review. Choose a support plan when your production team needs ongoing channels, incident allowances, and response terms. What response time can I expect on Slack? Community Slack is best effort and does not include a response guarantee. If response terms matter to your team, compare the paid support plans and choose the incident coverage that matches your production needs. When should we choose advisory instead of a support plan? Advisory fits a scoped question, architecture review, troubleshooting need, or implementation. A support plan fits ongoing production coverage with plan-specific channels, incident allowances, and response times. Teams can also use both. How do I escalate something urgent in production? Use the channels and escalation process defined in your support agreement. Without a paid support plan, community channels remain best effort and should not be treated as an incident-response commitment. Can ChilliCream help us design a schema or migration? Schema design, reviews, Fusion rollout planning, and migration guidance can be scoped as advisory work. For an implementation, discuss a contracting engagement with defined deliverables and milestones. Is the community Slack the right place for bug reports? Slack is good for triage and reproductions. Once a bug is confirmed, please file it on GitHub so it gets a tracking issue, a label, and a place to land the fix. ## Still not sure where to start? Explore advisory for a scoped GraphQL problem or compare support plans when your production team needs ongoing channels and response terms. If we are not the right help, we will say so. [Explore GraphQL advisory](https://chillicream.com/services/advisory) [Explore support plans](https://chillicream.com/services/support) --- # Acceptable Use Policy > Read ChilliCream's Acceptable Use Policy describing the permitted and prohibited uses of our websites, products, and services. Canonical source: https://chillicream.com/legal/acceptable-use-policy This policy is effective as of 26 August 2021. Last updated: 11 September 2023 This acceptable use policy covers the products, services, and technologies (collectively referred to as the “Services” provided by ChilliCream Inc. under any ongoing agreement. It's designed to protect us, our customers, and the general Internet community from unethical, irresponsible, and illegal activity. ChilliCream Inc. customers found engaging in activities prohibited by this acceptable use policy can be liable for service suspension and account termination. In extreme cases, we may be legally obliged to report such customers to the relevant authorities. ## Fair use We provide our facilities with the assumption your use will be “business as usual”, as per our offer schedule. If your use is considered to be excessive, then additional fees may be charged, or capacity may be restricted. We are opposed to all forms of abuse, discrimination, rights infringement, and/or any action that harms or disadvantages any group, individual, or resource. We expect our customers and, where applicable, their users (“end-users”) to likewise engage our Services with similar intent. ## Customer accountability We regard our customers as being responsible for their own actions as well as for the actions of anyone using our Services with the customer's permission. This responsibility also applies to anyone using our Services on an unauthorized basis as a result of the customer's failure to put in place reasonable security measures. By accepting Services from us, our customers agree to ensure adherence to this policy on behalf of anyone using the Services as their end users. Complaints regarding the actions of customers or their end-users will be forwarded to the nominated contact for the account in question. If a customer - or their end-user or anyone using our Services as a result of the customer - violates our acceptable use policy, we reserve the right to terminate any Services associated with the offending account or the account itself or take any remedial or preventative action we deem appropriate, without notice. To the extent permitted by law, no credit will be available for interruptions of service resulting from any violation of our acceptable use policy. ## Prohibited activity ### Copyright infringement and access to unauthorized material Our Services must not be used to transmit, distribute or store any material in violation of any applicable law. This includes but isn't limited to: 1. any material protected by copyright, trademark, trade secret, or other intellectual property right used without proper authorization, and 2. any material that is obscene, defamatory, constitutes an illegal threat or violates export control laws. The customer is solely responsible for all material they input, upload, disseminate, transmit, create or publish through or on our Services, and for obtaining legal permission to use any works included in such material. ### SPAM and unauthorized message activity Our Services must not be used for the purpose of sending unsolicited bulk or commercial messages in violation of the laws and regulations applicable to your jurisdiction (“spam“). This includes but isn't limited to sending spam, soliciting customers from spam sent from other service providers, and collecting replies to spam sent from other service providers. Our Services must not be used for the purpose of running unconfirmed mailing lists or telephone number lists (“messaging lists”). This includes but isn't limited to subscribing email addresses or telephone numbers to any messaging list without the permission of the email address or telephone number owner, and storing any email addresses or telephone numbers subscribed in this way. All messaging lists run on or hosted by our Services must be “confirmed opt-in”. Verification of the address or telephone number owner's express permission must be available for the lifespan of the messaging list. We prohibit the use of email lists, telephone number lists or databases purchased from third parties intended for spam or unconfirmed messaging list purposes on our Services. This spam and unauthorized message activity policy applies to messages sent using our Services, or to messages sent from any network by the customer or any person on the customer's behalf, that directly or indirectly refer the recipient to a site hosted via our Services. ### Unethical, exploitative, and malicious activity Our Services must not be used for the purpose of advertising, transmitting, or otherwise making available any software, program, product, or service designed to violate this acceptable use policy, or the acceptable use policy of other service providers. This includes but isn't limited to facilitating the means to send spam and the initiation of network sniffing, pinging, packet spoofing, flooding, mail-bombing, and denial-of-service attacks. Our Services must not be used to access any account or electronic resource where the group or individual attempting to gain access does not own or is not authorized to access the resource (e.g. “hacking”, “cracking”, “phreaking”, etc.). Our Services must not be used for the purpose of intentionally or recklessly introducing viruses or malicious code into our Services and systems. Our Services must not be used for purposely engaging in activities designed to harass another group or individual. Our definition of harassment includes but is not limited to denial-of-service attacks, hate-speech, advocacy of racial or ethnic intolerance, and any activity intended to threaten, abuse, infringe upon the rights of, or discriminate against any group or individual. Other activities considered unethical, exploitative, and malicious include: 1. Obtaining (or attempting to obtain) services from us with the intent to avoid payment; 2. Using our facilities to obtain (or attempt to obtain) services from another provider with the intent to avoid payment; 3. The unauthorized access, alteration, or destruction (or any attempt thereof) of any information about our customers or end-users, by any means or device; 4. Using our facilities to interfere with the use of our facilities and network by other customers or authorized individuals; 5. Publishing or transmitting any content of links that incite violence, depict a violent act, depict child pornography, or threaten anyone's health and safety; 6. Any act or omission in violation of consumer protection laws and regulations; 7. Any violation of a person's privacy. Our Services may not be used by any person or entity, which is involved with or suspected of involvement in activities or causes relating to illegal gambling; terrorism; narcotics trafficking; arms trafficking or the proliferation, development, design, manufacture, production, stockpiling, or use of nuclear, chemical or biological weapons, weapons of mass destruction, or missiles; in each case including any affiliation with others whatsoever who support the above such activities or causes. ### Unauthorized use of ChilliCream Inc. property We prohibit the impersonation of ChilliCream Inc., the representation of a significant business relationship with ChilliCream Inc., or ownership of any ChilliCream Inc. property (including our Services and brand) for the purpose of fraudulently gaining service, custom, patronage, or user trust. ## About this policy This policy outlines a non-exclusive list of activities and intent we deem unacceptable and incompatible with our brand. We reserve the right to modify this policy at any time by publishing the revised version on our website. The revised version will be effective from the earlier of: - the date the customer uses our Services after we publish the revised version on our website; or - 30 days after we publish the revised version on our website. --- # Cookie Policy > Learn how ChilliCream uses cookies on its websites and applications and how you can manage your cookie preferences. Canonical source: https://chillicream.com/legal/cookie-policy This policy is effective as of 26 August 2021. Last updated: 11 September 2023 We use cookies to help improve your experience of our Websites, which refers to [https://chillicream.com](https://chillicream.com/) as well as other websites operated by ChilliCream Inc. and that link to this policy. This cookie policy is part of ChilliCream Inc.'s privacy policy. It covers the use of cookies between your device and our site. We also provide basic information on third-party services we may use, who may also use cookies as part of their service. This policy does not cover their cookies. If you don't wish to accept cookies from us, you should instruct your browser to refuse cookies from [https://chillicream.com](https://chillicream.com/) or the relevant domain operated by us. In such a case, we may be unable to provide you with some of your desired content and services. ## What is a cookie? A cookie is a small piece of data that a website stores on your device when you visit. It typically contains information about the website itself, a unique identifier that allows the site to recognize your web browser when you return, additional data that serves the cookie's purpose, and the lifespan of the cookie itself. Cookies are used to enable certain features (e.g. logging in), track site usage (e.g. analytics), store your user settings (e.g. time zone, notification preferences), and to personalize your content (e.g. advertising, language). Cookies set by the website you are visiting are usually referred to as first-party cookies. They typically only track your activity on that particular site. Cookies set by other sites and companies (i.e. third parties) are called third-party cookies They can be used to track you on other websites that use the same third-party service. ## Types of cookies and how we use them ### Essential cookies Essential cookies are crucial to your experience of a website, enabling core features like user logins, account management, shopping carts, and payment processing. We use essential cookies to enable certain functions on our website. ### Performance cookies Performance cookies track how you use a website during your visit. Typically, this information is anonymous and aggregated, with information tracked across all site users. They help companies understand visitor usage patterns, identify and diagnose problems or errors their users may encounter, and make better strategic decisions in improving their audience's overall website experience. These cookies may be set by the website you're visiting (first-party) or by third-party services. They do not collect personal information about you. We use performance cookies on our site. ### Functionality cookies Functionality cookies are used to collect information about your device and any settings you may configure on the website you're visiting (like language and time zone settings). With this information, websites can provide you with customized, enhanced, or optimized content and services. These cookies may be set by the website you're visiting (first-party) or by third-party services. We use functionality cookies for selected features on our site. ### Targeting/advertising cookies Targeting/advertising cookies help determine what promotional content is most relevant and appropriate to you and your interests. Websites may use them to deliver targeted advertising or limit the number of times you see an advertisement. This helps companies improve the effectiveness of their campaigns and the quality of content presented to you. These cookies may be set by the website you're visiting (first-party) or by third-party services. Targeting/advertising cookies set by third-parties may be used to track you on other websites that use the same third-party service. We do not use this type of cookie on our site. --- # Privacy Policy > Read ChilliCream's Privacy Policy explaining what personal information we collect, how we use and disclose it, and your rights. Canonical source: https://chillicream.com/legal/privacy-policy This policy is effective as of 26 August 2021. Last updated: 1 October 2024 Your privacy is important to us. It is ChilliCream Inc.'s policy to respect your privacy and comply with any applicable law and regulation regarding any personal information we may collect about you, including across our "Services", which refers collectively to: (1) [https://chillicream.com](https://chillicream.com/) and any other websites we operate and that link to this policy (collectively, "Websites"), and (2) all versions of ChilliCream software, platforms or services (including but not limited to Nitro). Personal information is any information about you which can be used to identify you. This includes information about you as a person (such as name, address, and date of birth), your devices, payment details, and even information about how you use a website or online service. In the event our Services contains links to third-party sites and services, please be aware that those sites and services have their own privacy policies. After following a link to any third-party content, you should read their posted privacy policy information about how they collect and use personal information. This Privacy Policy does not apply to any of your activities after you leave our Services. ## Information We Collect Information we collect falls into one of two categories: “voluntarily provided” information and “automatically collected” information. “Voluntarily provided” information refers to any information you knowingly and actively provide us when using or participating in any of our Services and promotions. “Automatically collected” information refers to any information automatically sent by your devices in the course of accessing our Services. ### Log Data When you visit or use our Services, our servers may automatically log the standard data provided by your web browser or the service used. It may include your device's Internet Protocol (IP) address, your browser type and version, the pages you visit, the time and date of your visit, the time spent on each page, and other details about your visit. Additionally, if you encounter certain errors while using the Services, we may automatically collect data about the error and the circumstances surrounding its occurrence. This data may include technical details about your device, what you were trying to do when the error happened, and other technical information relating to the problem. You may or may not receive notice of such errors, even in the moment they occur, that they have occurred, or what the nature of the error is. Please be aware that while this information may not be personally identifying by itself, it may be possible to combine it with other data to personally identify individual persons. ### Device Data When you visit or interact with our Services, we may automatically collect data about your device, such as: - Device Type - Operating System - Unique device identifiers - Geo-location data Data we collect can depend on the individual settings of your device and software. We recommend checking the policies of your device manufacturer or software provider to learn what information they make available to us. ### Personal Information We may ask for personal information - for example, when you subscribe to our newsletter or when you contact us - which may include one or more of the following: - Name - Email - Social media profiles - Phone/mobile number - Home/mailing address ### Legitimate Reasons for Processing Your Personal Information We only collect and use your personal information when we have a legitimate reason for doing so. In which instance, we only collect personal information that is reasonably necessary to provide our services to you. ### Collection and Use of Information We may collect personal information from you when you do any of the following on our Services: - Register for an account - Sign up to receive updates from us via email or social media channels - Use a mobile device or web browser to access our content - Contact us via email, social media, or on any similar technologies - When you mention us on social media We may collect, hold, use, and disclose information for the following purposes, and personal information will not be further processed in a manner that is incompatible with these purposes: - to provide you with our platform's core features and services - to enable you to customize or personalize your experience of our Services - to deliver products and/or Services to you - to contact and communicate with you - for analytics, market research, and business development, including to operate and improve our Services, and associated social media platforms - for advertising and marketing, including to send you promotional information about our products and Services and information about third parties that we consider may be of interest to you - to enable you to access and use our Services, and associated social media platforms - for internal record keeping and administrative purposes - to comply with our legal obligations and resolve any disputes that we may have - to attribute any content (e.g. posts and comments) you submit that we publish on our Services - for security and fraud prevention, and to ensure that our Services are safe, secure, and used in line with our terms of use - for technical assessment, including to operate and improve our app, associated applications, and associated social media platforms We may combine voluntarily provided and automatically collected personal information with general information or research data we receive from other trusted sources. For example, Our marketing and market research activities may uncover data and insights, which we may combine with information about how visitors use our Services to improve our Services and your experience on it. ### Security of Your Personal Information When we collect and process personal information, and while we retain this information, we will protect it within commercially acceptable means to prevent loss and theft, as well as unauthorized access, disclosure, copying, use, or modification. Although we will do our best to protect the personal information you provide to us, we advise that no method of electronic transmission or storage is 100% secure, and no one can guarantee absolute data security. You are responsible for selecting any password and its overall security strength, ensuring the security of your own information within the bounds of our Services. For example, ensuring any passwords associated with accessing your personal information and accounts are secure and confidential. ### How Long We Keep Your Personal Information We keep your personal information only for as long as we need to. This time period may depend on what we are using your information for, in accordance with this privacy policy. For example, if you have provided us with personal information as part of creating an account with us, we may retain this information for the duration your account exists on our system. If your personal information is no longer required for this purpose, we will delete it or make it anonymous by removing all details that identify you. However, if necessary, we may retain your personal information for our compliance with a legal, accounting, or reporting obligation or for archiving purposes in the public interest, scientific, or historical research purposes or statistical purposes. ## Children's Privacy We do not aim any of our products or Services directly at children under the age of 13, and we do not knowingly collect personal information about children under 13. ## Disclosure of Personal Information to Third Parties We may disclose personal information to: - a parent, subsidiary, or affiliate of our company - third-party service providers for the purpose of enabling them to provide their services, including (without limitation) IT service providers, data storage, hosting and server providers, analytics, error loggers, debt collectors, maintenance or problem-solving providers, marketing providers, professional advisors, and payment systems operators - our employees, contractors, and/or related entities - our existing or potential agents or business partners - credit reporting agencies, courts, tribunals, and regulatory authorities, in the event you fail to pay for goods or services we have provided to you - courts, tribunals, regulatory authorities, and law enforcement officers, as required by law, in connection with any actual or prospective legal proceedings, or in order to establish, exercise, or defend our legal rights - third parties, including agents or sub-contractors, who assist us in providing information, products, services, or direct marketing to you - third parties to collect and process data - an entity that buys, or to which we transfer all or substantially all of our assets and business Third parties we currently use can be found in the section Data Storage Providers ## International Transfers of Personal Information The personal information we collect is stored and/or processed in United States, Netherlands, Ireland, Germany, and Switzerland, or where we or our partners, affiliates, and third-party providers maintain facilities. The countries to which we store, process, or transfer your personal information may not have the same data protection laws as the country in which you initially provided the information. If we transfer your personal information to third parties in other countries: (i) we will perform those transfers in accordance with the requirements of applicable law; and (ii) we will protect the transferred personal information in accordance with this privacy policy. ## Your Rights and Controlling Your Personal Information **Your choice:** By providing personal information to us, you understand we will collect, hold, use, and disclose your personal information in accordance with this privacy policy. You do not have to provide personal information to us, however, if you do not, it may affect your use of our Services or the products and/or services offered on or through it. **Information from third parties:** If we receive personal information about you from a third party, we will protect it as set out in this privacy policy. If you are a third party providing personal information about somebody else, you represent and warrant that you have such person's consent to provide the personal information to us. **Marketing permission:** If you have previously agreed to us using your personal information for direct marketing purposes, you may change your mind at any time by contacting us using the details below. **Access:** You may request details of the personal information that we hold about you. **Correction:** If you believe that any information we hold about you is inaccurate, out of date, incomplete, irrelevant, or misleading, please contact us using the details provided in this privacy policy. We will take reasonable steps to correct any information found to be inaccurate, incomplete, misleading, or out of date. **Non-discrimination:** We will not discriminate against you for exercising any of your rights over your personal information. Unless your personal information is required to provide you with a particular service or offer (for example providing user support), we will not deny you goods or services and/or charge you different prices or rates for goods or services, including through granting discounts or other benefits, or imposing penalties, or provide you with a different level or quality of goods or services. **Notification of data breaches:** We will comply with laws applicable to us in respect of any data breach. **Complaints:** If you believe that we have breached a relevant data protection law and wish to make a complaint, please contact us using the details below and provide us with full details of the alleged breach. We will promptly investigate your complaint and respond to you, in writing, setting out the outcome of our investigation and the steps we will take to deal with your complaint. You also have the right to contact a regulatory body or data protection authority in relation to your complaint. **Unsubscribe:** To unsubscribe from our email database or opt-out of communications (including marketing communications), please contact us using the details provided in this privacy policy, or opt-out using the opt-out facilities provided in the communication. We may need to request specific information from you to help us confirm your identity. ## Use of Cookies We use “cookies” to collect information about you and your activity across our Services. A cookie is a small piece of data that our Websites store on your computer, and access each time you visit, so we can understand how you use our site. This helps us serve you content based on preferences you have specified. Please refer to our Cookie Policy for more information. ## Business Transfers If we or our assets are acquired, or in the unlikely event that we go out of business or enter bankruptcy, we would include data, including your personal information, among the assets transferred to any parties who acquire us. You acknowledge that such transfers may occur, and that any parties who acquire us may, to the extent permitted by applicable law, continue to use your personal information according to this policy, which they will be required to assume as it is the basis for any ownership or use rights we have over such information. ## Limits of Our Policy Our Services may link to external sites that are not operated by us. Please be aware that we have no control over the content and policies of those sites, and cannot accept responsibility or liability for their respective privacy practices. ## Changes to This Policy At our discretion, we may change our privacy policy to reflect updates to our business processes, current acceptable practices, or legislative or regulatory changes. If we decide to change this privacy policy, we will post the changes here at the same link by which you are accessing this privacy policy. If the changes are significant, or if required by applicable law, we will contact you (based on your selected preferences for communications from us) and all our registered users with the new details and links to the updated or changed policy. If required by law, we will get your permission or give you the opportunity to opt in to or opt out of, as applicable, any new uses of your personal information. ## Additional Disclosures for Australian Privacy Act Compliance (AU) ### International Transfers of Personal Information Where the disclosure of your personal information is solely subject to Australian privacy laws, you acknowledge that some third parties may not be regulated by the Privacy Act and the Australian Privacy Principles in the Privacy Act. You acknowledge that if any such third party engages in any act or practice that contravenes the Australian Privacy Principles, it would not be accountable under the Privacy Act, and you will not be able to seek redress under the Privacy Act. ## Additional Disclosures for General Data Protection Regulation (GDPR) Compliance (EU) ### Data Controller / Data Processor The GDPR distinguishes between organizations that process personal information for their own purposes (known as “data controllers”) and organizations that process personal information on behalf of other organizations (known as “data processors”). We, ChilliCream Inc., located at the address provided in our Contact Us section, are a Data Controller with respect to the personal information you provide to us. ### Legal Bases for Processing Your Personal Information We will only collect and use your personal information when we have a legal right to do so. In which case, we will collect and use your personal information lawfully, fairly, and in a transparent manner. If we seek your consent to process your personal information, and you are under 16 years of age, we will seek your parent or legal guardian's consent to process your personal information for that specific purpose. Our lawful bases depend on the services you use and how you use them. This means we only collect and use your information on the following grounds: #### Consent From You Where you give us consent to collect and use your personal information for a specific purpose. You may withdraw your consent at any time using the facilities we provide; however this will not affect any use of your information that has already taken place. You may consent to providing your email address for the purpose of receiving marketing emails from us. While you may unsubscribe at any time, we cannot recall any email we have already sent. If you have any further enquiries about how to withdraw your consent, please feel free to enquire using the details provided in the Contact Us section of this privacy policy. #### Performance of a Contract or Transaction Where you have entered into a contract or transaction with us, or in order to take preparatory steps prior to our entering into a contract or transaction with you. For example, if you contact us with an enquiry, we may require personal information such as your name and contact details in order to respond. #### Our Legitimate Interests Where we assess it is necessary for our legitimate interests, such as for us to provide, operate, improve and communicate our services. We consider our legitimate interests to include research and development, understanding our audience, marketing and promoting our services, measures taken to operate our services efficiently, marketing analysis, and measures taken to protect our legal rights and interests. #### Compliance with Law In some cases, we may have a legal obligation to use or keep your personal information. Such cases may include (but are not limited to) court orders, criminal investigations, government requests, and regulatory obligations. If you have any further enquiries about how we retain personal information in order to comply with the law, please feel free to enquire using the details provided in the Contact Us section of this privacy policy. ### International Transfers Outside of the European Economic Area (EEA) We will ensure that any transfer of personal information from countries in the European Economic Area (EEA) to countries outside the EEA will be protected by appropriate safeguards, for example by using standard data protection clauses approved by the European Commission, or the use of binding corporate rules or other legally accepted means. ### Your Rights and Controlling Your Personal Information **Restrict:** You have the right to request that we restrict the processing of your personal information if (i) you are concerned about the accuracy of your personal information; (ii) you believe your personal information has been unlawfully processed; (iii) you need us to maintain the personal information solely for the purpose of a legal claim; or (iv) we are in the process of considering your objection in relation to processing on the basis of legitimate interests. **Objecting to processing:** You have the right to object to processing of your personal information that is based on our legitimate interests or public interest. If this is done, we must provide compelling legitimate grounds for the processing which overrides your interests, rights, and freedoms, in order to proceed with the processing of your personal information. **Data portability:** You may have the right to request a copy of the personal information we hold about you. Where possible, we will provide this information in CSV format or other easily readable machine format. You may also have the right to request that we transfer this personal information to a third party. **Deletion:** You may have a right to request that we delete the personal information we hold about you at any time, and we will take reasonable steps to delete your personal information from our current records. If you ask us to delete your personal information, we will let you know how the deletion affects your use of our Services. There may be exceptions to this right for specific legal reasons which, if applicable, we will set out for you in response to your request. If you terminate or delete your account, we will delete your personal information within 180 days of the deletion of your account. Please be aware that search engines and similar third parties may still retain copies of your personal information that has been made public at least once, like certain profile information and public comments, even after you have deleted the information from our services or deactivated your account. ## Additional Disclosures for California Compliance (US) Under California Civil Code Section 1798.83, if you live in California and your business relationship with us is mainly for personal, family, or household purposes, you may ask us about the information we release to other organizations for their marketing purposes. To make such a request, please contact us using the details provided in this privacy policy with “Request for California privacy information” in the subject line. You may make this type of request once every calendar year. We will email you a list of categories of personal information we revealed to other organizations for their marketing purposes in the last calendar year, along with their names and addresses. Not all personal information shared in this way is covered by Section 1798.83 of the California Civil Code. ### Do Not Track Some browsers have a “Do Not Track” feature that lets you tell websites that you do not want to have your online activities tracked. At this time, we do not respond to browser “Do Not Track” signals. We adhere to the standards outlined in this privacy policy, ensuring we collect and process personal information lawfully, fairly, transparently, and with legitimate, legal reasons for doing so. ### Cookies and Pixels At all times, you may decline cookies from our Websites if your browser permits. Most browsers allow you to activate settings on your browser to refuse the setting of all or some cookies. Accordingly, your ability to limit cookies is based only on your browser's capabilities. Please refer to the Cookies section of this privacy policy for more information. ### CCPA-permitted financial incentives In accordance with your right to non-discrimination, we may offer you certain financial incentives permitted by the CCPA that can result in different prices, rates, or quality levels for the goods or services we provide. Any CCPA-permitted financial incentive we offer will reasonably relate to the value of your personal information, and we will provide written terms that describe clearly the nature of such an offer. Participation in a financial incentive program requires your prior opt-in consent, which you may revoke at any time. ### California Notice of Collection In the past 12 months, we have collected the following categories of personal information enumerated in the California Consumer Privacy Act: - Identifiers, such as name, email address, phone number account name, IP address, and an ID or number assigned to your account. - Customer records, such as billing and shipping address, and credit or debit card data. - Commercial information, such as products or services history and purchases. For more information on information we collect, including the sources we receive information from, review the “Information We Collect” section. We collect and use these categories of personal information for the business purposes described in the “Collection and Use of Information” section, including to provide and manage our Service. ### Right to Know and Delete If you are a California resident, you have rights to delete your personal information we collected and know certain information about our data practices in the preceding 12 months. In particular, you have the right to request the following from us: - The categories of personal information we have collected about you; - The categories of sources from which the personal information was collected; - The categories of personal information about you we disclosed for a business purpose or sold; - The categories of third parties to whom the personal information was disclosed for a business purpose or sold; - The business or commercial purpose for collecting or selling the personal information; and - The specific pieces of personal information we have collected about you. To exercise any of these rights, please contact us using the details provided in this privacy policy. ### Shine the Light If you are a California resident, in addition to the rights discussed above, you have the right to request information from us regarding the manner in which we share certain personal information as defined by California's “Shine the Light” with third parties and affiliates for their own direct marketing purposes. To receive this information, send us a request using the contact details provided in this privacy policy. Requests must include “California Privacy Rights Request” in the first line of the description and include your name, street address, city, state, and ZIP code. ## Data Storage Providers We utilize a variety of cloud storage providers to manage and process data. Below is a list of these providers along with their respective locations: 1. **Azure**: Some of the data we collect may be stored and processed on Azure data centers located within the United States. Azure is a service provided by Microsoft. For further information about their data handling practices, you can review here: - [Privacy Policy](https://privacy.microsoft.com/en-us/privacystatement) - [Trust Center](https://www.microsoft.com/en-us/trust-center) - [Legal Information](https://azure.microsoft.com/en-us/support/legal/) 2. **AWS** : Some data may be hosted on Amazon Web Services (AWS) data centers, predominantly in the United States. For more about AWS's data practices, check: - [Terms of Service](https://aws.amazon.com/service-terms/) - [Privacy Policy](https://aws.amazon.com/privacy/) 3. **CloudFlare**: CloudFlare acts as our content delivery network, facilitating the speed and reliability of our service. Some static data might pass through or be temporarily cached by CloudFlare. Additional details about their data practices can be found in here: - [Terms of Service](https://www.cloudflare.com/terms/) - [Privacy Policy](https://www.cloudflare.com/privacypolicy/) 4. **ClickHouse on AWS**: We utilize ClickHouse for specific data analytics and storage tasks. While the service is facilitated by ClickHouse, the actual data is primarily hosted on AWS data centers in the United States. Additional insights into ClickHouse and their processes can be found here: - [Terms of Service](https://clickhouse.com/legal/agreements/terms-of-service) - [Privacy Policy](https://clickhouse.com/legal/privacy-policy) - [Trust Center](https://trust.clickhouse.com/) 5. **Elastic Stack on Azure**:Elastic Stack assists us in data processing and analytics, primarily using Azure data centers. More information about Elastic Stack and their data handling practices can be found here: - [Terms of Service](https://www.elastic.co/legal/terms-of-use) - [Privacy Policy](https://www.elastic.co/legal/privacy-statement) 6. **SendGrid**: SendGrid assists us with email communication services. Some user data, such as email addresses used for sending communications, might be processed and stored on SendGrid. More about their data practices can be discovered here: - [Terms of Service](https://sendgrid.com/policies/tos/) - [Privacy Policy](https://sendgrid.com/policies/privacy/) 7. **Stripe**: Stripe is our chosen platform for payment processing. Data related to transactions, including payment details, could be processed and stored on Stripe. For a comprehensive understanding of their data practices, please check the: - [Terms of Service](https://stripe.com/legal) - [Privacy Policy](https://stripe.com/privacy) It's important to note that while we choose our storage providers with careful consideration, all data storage carries inherent risks. We work with our partners to maintain the integrity of our services. Any updates or changes to our storage providers will be reflected in this section, and we'll notify our users as outlined in our 'Changes to This Policy' section. Should you have questions about our data storage practices, please reach out to us at [contact@chillicream.com](mailto:contact@chillicream.com). ## Contact Us For any questions or concerns regarding your privacy, you may contact us using the following details: The ChilliCream Team [contact@chillicream.com](mailto:contact@chillicream.com) --- # Terms of Service > Read ChilliCream's Terms of Service governing the use of our websites, products, and services. Canonical source: https://chillicream.com/legal/terms-of-service This policy is effective as of 26 August 2021. Last updated: 1 October 2024 These Terms of Service govern your use of our "Services", which refers collectively to: (1) [https://chillicream.com](https://chillicream.com/) and any other websites we operate and that link to this policy (collectively, "Websites"), and (2) all versions of ChilliCream software, platforms or services (including but not limited to Nitro). By downloading, installing, using or accessing the Services, you agree that you have read and understood these Terms of Service, that you will abide by them and that you agree to comply with all applicable laws and regulations. If you do not agree with these Terms of Service, you are prohibited from using or accessing the Services provided by ChilliCream Inc.. We, ChilliCream Inc., reserve the right to review and amend any of these Terms of Service at our sole discretion. Upon doing so, we will update this page. Any changes to these Terms of Service will take effect immediately from the date of publication. ## Limitations of Use By using the Services, you warrant on behalf of yourself, your users, and other parties you represent that you will not: 1. modify, copy, prepare derivative works of, decompile, or reverse engineer any materials and software contained in the Services; 2. remove any copyright or other proprietary notations from any materials and software in the Services; 3. transfer the materials to another person or “mirror” the materials on any other server; 4. knowingly or negligently use the Services in a way that abuses or disrupts our networks or any other service ChilliCream Inc. provides; 5. use the Services to transmit or publish any harassing, indecent, obscene, fraudulent, or unlawful material; 6. use the Services in violation of any applicable laws or regulations; 7. use the Services in conjunction with sending unauthorized advertising or spam; 8. harvest, collect, or gather user data without the user's consent; or 9. use the Services in such a way that may infringe the privacy, intellectual property rights, or other rights of third parties. ## Use of the Services ### Right to Use Nitro ChilliCream Inc. grants you a non-exclusive, non-transferable, revocable, non-sublicensable right to use the software application Nitro on your computer. That right will terminate on any violation of these Terms of Service. ## Third party software ChilliCream Inc. Services contain third party code and libraries licensed to us. That includes open source software, of which your right to their use shall be governed by their license agreement instead of this Terms of Service document. ## Previews ChilliCream Inc. may release preview versions of our Services for evaluation and testing to you. These come as-is, with no guarantees, and may be removed or modified without notice. Their use is entirely at your own risk, and you accept that these versions may not have been subject to the same security checks, compatibility, stability and so on as non-preview versions of our Services. You should not use them in production environments or environments with sensitive data. ## Intellectual Property The intellectual property in the materials contained in the Services are owned by or licensed to ChilliCream Inc. and are protected by applicable copyright and trademark law. We grant our users permission to download one copy of the materials for personal, non-commercial transitory use. This constitutes the grant of a license, not a transfer of title. This license shall automatically terminate if you violate any of these restrictions or the Terms of Service, and may be terminated by ChilliCream Inc. at any time. ## Liability Our Services and the materials on them are provided on an 'as is' basis. To the extent permitted by law, ChilliCream Inc. makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties including, without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property, or other violation of rights. In no event shall ChilliCream Inc. or its suppliers be liable for any consequential loss suffered or incurred by you or any third party arising from the use or inability to use the Services or its materials, even if ChilliCream Inc. or an authorized representative has been notified, orally or in writing, of the possibility of such damage. In the context of this agreement, “consequential loss” includes any consequential loss, indirect loss, real or anticipated loss of profit, loss of benefit, loss of revenue, loss of business, loss of goodwill, loss of opportunity, loss of savings, loss of reputation, loss of use and/or loss or corruption of data, whether under statute, contract, equity, tort (including negligence), indemnity, or otherwise. Because some jurisdictions do not allow limitations on implied warranties, or limitations of liability for consequential or incidental damages, these limitations may not apply to you. YOU EXPRESSLY UNDERSTAND AND AGREE THAT WE, CHILLICREAM INC., OR OUR DIRECTORS OR EMPLOYEES, SHALL, IN NO EVENT, BE LIABLE TO YOU OR ANY THIRD PARTY UNDER ANY THEORY OF LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY LOSS OR DAMAGES, INCLUDING BUT NOT LIMITED TO LOSS OF DATA, LOSS OF USE, LOST REVENUE OR INCOME OR PROFIT, FAILURE OF SECURITY PROTOCOLS OR BUSINESS INTERRUPTION, THAT MAY BE INCURRED BY YOU ARISING OUT OF OR RELATED TO THESE TERMS OF SERVICE OR OUR SERVICES OR CONTENT, WHETHER OR NOT WE, CHILLICREAM INC., HAVE BEEN ADVISED OF OR SHOULD HAVE BEEN AWARE OF THE POSSIBILITY OF ANY SUCH LOSSES ARISING. EXCEPT IN CASE OF YOUR VIOLATION OF THE 'LIMITATIONS OF USE' SECTION, NEITHER PARTY'S LIABILITY TO THE OTHER SHALL EXCEED THE FEES PAID BY YOU TO CHILLICREAM INC. IN THE 3 MONTHS IMMEDIATELY PRECEDING THE EVENT GIVING RISE TO THE CLAIM. NOTWITHSTANDING ANYTHING TO THE CONTRARY IN THESE TERMS, CHILLICREAM INC.'S LIABILITY TO YOU FOR SERVICES THAT ARE FREE OF CHARGE SHALL NOT EXCEED USD 50. ## Accuracy of Materials The materials appearing on our Services are not comprehensive and are for general information purposes only. ChilliCream Inc. does not warrant or make any representations concerning the accuracy, validity, applicability, likely results, or reliability of the use of the materials on our Services, or otherwise relating to such materials or on any resources linked to from our Services. UNDER NO CIRCUMSTANCE SHALL CHILLICREAM INC. HAVE ANY LIABILITY TO YOU FOR ANY DAMAGE OR LOSS OF ANY KIND INCURRED AS A RESULT OF THE USE OF OUR SERVICES OR THE USAGE OR RELIANCE OF ANY INFORMATION ON OUR SERVICES. ## Links ChilliCream Inc. has not reviewed all of the sites or resources linked to from our Services and is not responsible for the contents of any such linked site. The inclusion of any link does not imply endorsement, approval, or control by ChilliCream Inc. of the site. Use of any such linked site is at your own risk and we strongly advise you make your own investigations with respect to the suitability of those sites. ## Right to Terminate We may suspend or terminate your right to use our Services and terminate these Terms of Service immediately upon written notice to you for any breach of these Terms of Service. ## Severance Any term of these Terms of Service which is wholly or partially void or unenforceable is severed to the extent that it is void or unenforceable. The validity of the remainder of these Terms of Service is not affected. ## Governing Law These Terms of Service are governed by and construed in accordance with the laws of Delaware. You irrevocably submit to the exclusive jurisdiction of the courts in that State or location. --- # ChilliCream License 1.0 > This is the license we use for our proprietary software like Nitro. Canonical source: https://chillicream.com/licensing/chillicream-license ## Acceptance By using the software, you agree to all of the terms and conditions below. ## Copyright License The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below. ## Limitations You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key. You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. ### Patents The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. ## Notices You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software. ## No Other Rights These terms do not imply any licenses other than those expressly granted in these terms. ## Termination If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently. ## No Liability _As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim._ ## Definitions The **licensor** is the entity offering these terms, and the **software** is the software the licensor makes available under these terms, including any portion of it. **you** refers to the individual or entity agreeing to these terms. **your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. **your licenses** are all the licenses granted to you for the software under these terms. **use** means anything you do with the software requiring one of your licenses. **trademark** means trademarks, service marks, and similar rights. --- # GraphQL API Lifecycle Tools for .NET > Connect GraphQL federation, API analytics, schema checks, and agentic development with ChilliCream's open-source .NET tools and Nitro control plane. Canonical source: https://chillicream.com/platform One platform for every API across your organization, from authoring and composition to operations and telemetry. [Explore Nitro](https://chillicream.com/products/nitro) [Read Fusion Docs](https://chillicream.com/docs/fusion) ## Explore the Platform [Federation with FusionCompose independently owned GraphQL services into one gateway artifact before runtime.Learn more →](https://chillicream.com/docs/fusion) [AnalyticsInstant Insights. Enhanced Performance.Learn more →](https://chillicream.com/platform/analytics) [Release SafetyCatch Breaking Changes Before They Ship.Learn more →](https://chillicream.com/platform/release-safety) [EcosystemAn Ecosystem You Trust and Love.Learn more →](https://chillicream.com/platform/ecosystem) [Agentic DevelopmentConsistently Good Code, from Any Agent.Learn more →](https://chillicream.com/platform/agentic-coding) [Nitro Control PlaneBring operation analytics, traces, schema history, client safety, and delivery checks into one application.Learn more →](https://chillicream.com/products/nitro) --- # Agentic Development for .NET GraphQL > Build with agents on a platform designed for agentic development. Skills, .NET patterns, schema checks, and MCP keep humans and agents aligned. Canonical source: https://chillicream.com/platform/agentic-coding Agentic development Agents are strong at filling a known pattern and weak at inventing architecture. The platform gives your agent the pattern to fill, your conventions as checked-in skills, and feedback it can act on before the merge, so what comes back is best-practice code. `$ dnx skills add chillicream/agent-skills` One command teaches your agent the platform. Agent directory ## Bring the agent you already use. Skills teaches supported agents the same conventions. MCP gives compatible agents access to your API. Switch agents without switching architectures, review standards, or your definition of good code. - ![](https://chillicream.com/agent-logos/claude.svg)Claude - ![](https://chillicream.com/agent-logos/codex.svg)Codex - ![](https://chillicream.com/agent-logos/copilot.svg)Copilot - ![](https://chillicream.com/agent-logos/cursor.svg)Cursor - ![](https://chillicream.com/agent-logos/windsurf.svg)Windsurf - ![](https://chillicream.com/agent-logos/gemini.svg)Gemini - ![](https://chillicream.com/agent-logos/cline.svg)Cline - and many more Review ## Review stays fast because changes stay uniform. When agents fill the same patterns, changes come back in a familiar shape. Review stays a glance instead of an architectural investigation. Writing code is cheap now; reviewing it is where your time actually goes. feat: add product reviews3 files changed+73 \-2In Review Reviews/AddReview.cs+34\-2Viewed +\[Mutation\] +static Task AddReviewAsync(...) Reviews/ReviewAddedHandler.cs+21\-0Viewed +class ReviewAddedHandler : IEventHandler +ValueTask HandleAsync(ReviewAdded e, ...) Reviews/GetSummaryHandler.cs+18\-0Viewed +class GetSummaryHandler : IQueryHandler +ValueTask HandleAsync(...) Checks - BuildIn progress - TestsIn progress - Schema ChecksIn progress Approve review effort uniform changes, faster reviews Skills ## Skills give your agent a head start. Prototype a feature, derive the contract, evolve the schema: on this platform those are workflows an agent can run, not rituals a developer performs. Skills package that working knowledge so agents start productive, and your own conventions ship the same way, reviewed like code. SKILL.md ### graphql-schema-design Schema design and review. Proposes SDL in design mode and audits schema diffs in review mode, following the team's conventions. `$ dnx skills add chillicream/agent-skills --skill graphql-schema-design` SKILL.md ### prototype-feature Frontend prototype with mock data. Builds a clickable, local-only prototype with realistic mock data before any schema or backend work. `$ dnx skills add chillicream/agent-skills --skill prototype-feature` SKILL.md ### prototype-to-contract Prototype to backend contract. Turns an accepted prototype into colocated GraphQL fragments and a contract for the backend. `$ dnx skills add chillicream/agent-skills --skill prototype-to-contract` [Browse chillicream/agent-skills](https://github.com/chillicream/agent-skills) [Read the skills docs](https://chillicream.com/docs/skills) Patterns ## One pattern per problem. Attributes mark queries, mutations, DataLoaders, and pagination; event handlers implement a single interface. The agent fills a known shape instead of inventing structure, so two features written weeks apart come back looking the same. Query`[Query]` Mutation`[Mutation]` DataLoader`[DataLoader]` Pagination`[UseConnection]` Authorization`[Authorize]` Filtering`[UseFiltering]` Event handler`IEventHandler` Request handler`IEventRequestHandler` Feedback ## Feedback before the merge. Strong types turn many bad edits into compile errors. `nitro` checks schema changes in CI against the client registry, so a risky change comes back as feedback while the agent can still fix it. agent patchremove Product.price published opsProductCard { id name price } feedbackbreaking: published clients affected agent fixdeprecate Product.price instead [Explore GraphQL schema checks](https://chillicream.com/platform/release-safety) At runtime ## Your API is a tool, too. Host the MCP transport with `AddMcp()` and `MapGraphQLMcp()`. Supply tool definitions through Nitro or a custom `IMcpStorage`. MCP-compatible agents can then call those operation tools at `/graphql/mcp`. [Read the MCP adapter docs](https://chillicream.com/docs/hotchocolate/adapters/mcp) Tool exposure getProductidempotent searchOrdersidempotent createReviewdestructive ## Point your agent at the platform. The patterns, the feedback, and the checks give every agent the same rails. One command installs ChilliCream’s skills; your own conventions ship the same way. `$ dnx skills add chillicream/agent-skills` --- # API Analytics and OpenTelemetry Observability > Use API analytics and OpenTelemetry traces to investigate latency, errors, throughput, operation impact, and identified client usage in Nitro. Canonical source: https://chillicream.com/platform/analytics Track latency, errors, and throughput for the operations your services report. When something slows down, open the related traces and inspect which calls took the time. [Start Nitro for Free](https://nitro.chillicream.com/) [Read the analytics docs](https://chillicream.com/docs/nitro/open-telemetry/operation-monitoring) healthywarningerror operation · checkoutWarning operation mutation checkout p99 318ms▲ 7.6× latency / 5m p95 42ms throughput 1.2k/m errors 0.3% distributed trace · checkout7f3a·9b2e·c1 · 318ms 0ms100ms200ms318.0 ms 🌐POST /graphql318.0 ms ◈mutation checkout306.0 ms ↪users-svc · GET /me44.0 ms ↪billing · Charge204.0 ms ⚙worker · receipt.enqueue58.0 ms ⚙orders.db · INSERT38.0 ms 204ms of this 318ms request were spent in the billing service. ## OpenTelemetry-native, end to end. Configured services export supported traces, metrics, and logs over plain OTLP. Nitro links reported operation signals to the related distributed traces for investigation. GraphQLRESTgRPCjobDB - Vendor-neutral OTLP in, no proprietary agent - Hot Chocolate is auto-instrumented - Works with any OpenTelemetry backend, not just Nitro C#Program.cs ``` builder.Services .AddNitro() .AddOpenTelemetry(); builder.Services .AddGraphQLServer() .AddInstrumentation(); ``` ## What’s slow. How bad. And for whom. ## Rank operations to investigate with the impact score. The impact score combines traffic, latency, and error rate to help you decide which reported operations to investigate first. ### Operations ranked by impact · last 1h | Operation | Avg latency | Throughput | Errors | Latency | Impact | | ---------------------- | ----------- | ---------- | ------ | ------- | ------ | | Qmutation checkout | 62 ms | 1.2k/m | 0.3% | | | | QgRPC · Billing.Charge | 204 ms | 610/m | 0.4% | | | | QREST · POST /orders | 31 ms | 1.4k/m | 0.0% | | | | Qmutation applyCoupon | 16 ms | 340/m | 1.4% | | | | Qjob · receipt.worker | 58 ms | 240/m | 0.2% | | | ## The whole latency picture, not an average. An average can look healthy while a small number of requests are much slower. Percentiles show you where the tail starts, so you can find and fix those slow paths before they affect more users. ### Latency · checkout p95 / p99 · ms p95p99 p99 318ms p95 42ms throughput 1.2k/m error rate 0.31% ## Know which identified clients are affected. See which client apps and versions are behind an operation. When one starts causing trouble, you can tell whether it affects everyone or only a particular client. ### Clients · checkout share of impact web-storefront mobile-ios android ## Move from a metric spike to the related traces. ## Follow one request through its reported trace. Open the trace waterfall to inspect the services and spans that reported timing for one request, including which calls contributed the most latency. ## Inspect the trace behind a failed operation. When errors spike, open the failing operation and inspect its traces, spans, and captured exception details without correlating logs by hand. ## The whole story, from spike to span. Follow an incident from the latency chart to the affected operation and down to the spans behind it, without leaving Nitro. [Start Nitro for Free](https://nitro.chillicream.com/) [Read Analytics Docs](https://chillicream.com/docs/nitro/open-telemetry/operation-monitoring) --- # Open-Source GraphQL Ecosystem for .NET > Explore the open-source .NET GraphQL ecosystem behind Hot Chocolate, Fusion, Mocha and Strawberry Shake: public code, standards work, docs, and community. Canonical source: https://chillicream.com/platform/ecosystem [Stars](https://github.com/ChilliCream/graphql-platform/stargazers) [MIT LICENSED](https://github.com/ChilliCream/graphql-platform/blob/main/LICENSE) [](https://github.com/ChilliCream/graphql-platform) [](https://slack.chillicream.com/) [](https://chillicream.com/blog) [![](https://avatars.githubusercontent.com/u/9714350?v=4)](https://github.com/michaelstaib) [![](https://avatars.githubusercontent.com/u/261509?v=4)](https://github.com/glen-84) [![](https://avatars.githubusercontent.com/u/14233220?v=4)](https://github.com/PascalSenn) [![](https://avatars.githubusercontent.com/u/45513122?v=4)](https://github.com/tobias-tengler) [![](https://avatars.githubusercontent.com/u/4325318?v=4)](https://github.com/rstaib) [![](https://avatars.githubusercontent.com/u/157336?v=4)](https://github.com/benmccallum) [![](https://avatars.githubusercontent.com/u/45466413?v=4)](https://github.com/N-Olbert) [![](https://avatars.githubusercontent.com/u/681965?v=4)](https://github.com/wonbyte) [![](https://avatars.githubusercontent.com/u/55194784?v=4)](https://github.com/danielreynolds1) [![](https://avatars.githubusercontent.com/u/4927894?v=4)](https://github.com/sunghwan2789) [![](https://avatars.githubusercontent.com/u/13761704?v=4)](https://github.com/glucaci) [![](https://avatars.githubusercontent.com/u/8672758?v=4)](https://github.com/arif-hanif) [![](https://avatars.githubusercontent.com/u/521449?v=4)](https://github.com/jorrit) [![](https://avatars.githubusercontent.com/u/2009544?v=4)](https://github.com/matt-psaltis) [![](https://avatars.githubusercontent.com/u/6015550?v=4)](https://github.com/PHILLIPS71) [![](https://avatars.githubusercontent.com/u/9481836?v=4)](https://github.com/fredericbirke) Explore the code, read the docs, and talk to the maintainers building the platform. [Explore the code](https://github.com/ChilliCream/graphql-platform) [Read the docs](https://chillicream.com/docs) Open source you can inspect. Standards you can follow. People you can reach. ## Built in the open, in one repository. The server, gateway, client, and core libraries are all developed in a single public GitHub repository. [ChilliCream/graphql-platformONE REPOSITORYThe core server, gateway, client, and libraries share one codebase, so the pieces stay in step.MIT LICENSEOpen source under the MIT license. Free to use in commercial products.PUBLIC DEVELOPMENTFollow issues, pull requests, releases, and changelogs as the work lands.](https://github.com/ChilliCream/graphql-platform) ## Helping write the standards we implement. ChilliCream contributors help shape the specifications and conventions the platform implements. See the [official GraphQL team page](https://graphql.org/community/team/) for current roles and participation. [TWO SEATSTechnical Steering CommitteeThe committee steering the GraphQL specification. Michael Staib and Pascal Senn both hold seats.github.com](https://github.com/graphql/graphql-wg/blob/main/GraphQL-TSC.md) [HOSTComposite Schemas Working GroupMichael Staib hosts the Composite Schemas subcommittee, which develops an open standard for composing GraphQL services.github.com](https://github.com/graphql/composite-schemas-wg) [HOSTGraphQL/OpenTelemetry Working GroupPascal Senn hosts GraphQL/OTel, which develops OpenTelemetry conventions for GraphQL APIs.github.com](https://github.com/graphql/otel-wg) [ORGANIZERSGraphQL DayCommunity events around the world, organized with the GraphQL Foundation team.graphql.org](https://graphql.org/day) [MEMBERSGraphQL Working GroupThe main working group evolving the GraphQL specification itself.github.com](https://github.com/graphql/graphql-wg) [MEMBERSGraphQL over HTTPThe specification for transporting GraphQL over HTTP, so servers and clients interoperate.github.com](https://github.com/graphql/graphql-over-http) [MEMBERSAI Working GroupBest practices for GraphQL in AI systems and agent-powered applications.github.com](https://github.com/graphql/ai-wg) ## Find the people behind the code. Follow the work on GitHub, bring questions to Slack, and learn from talks and engineering posts. [GitHubIssues, discussions, pull requests, and the source itself.Stars](https://github.com/ChilliCream/graphql-platform) [SlackAsk questions and talk to the team and other users directly.Join ](https://slack.chillicream.com/) [YouTubeTalks, release walkthroughs, and deep dives.Watch ](https://www.youtube.com/c/ChilliCream) [BlogRelease notes, engineering write-ups, and announcements.Read ](https://chillicream.com/blog) ## From our blog [View all](https://chillicream.com/blog) - [![](https://chillicream.com/images/blog/2026-08-03-directives-all-the-way-down/header.png)Aug 2026Directives All the Way DownGraphQL directives can now be applied to directive definitions themselves in Hot Chocolate 16.4, so you can finally deprecate a directive and attach metadata to your schema's own extension points.Read](https://chillicream.com/blog/2026-08-03-directives-all-the-way-down) - [![](https://chillicream.com/images/blog/2026-07-12-fusion-16-5/header.png)ReleaseJul 2026Fusion 16.5: The Gateway for EveryoneBuilt with C# and .NET, Fusion 16.5 is the only gateway supporting both federation standards, achieves 100% Apollo Federation compliance, and leads in real-world performance.Read](https://chillicream.com/blog/2026-07-12-fusion-16-5) - [![](https://chillicream.com/images/blog/2026-07-06-federated-event-streams/header.png)ReleaseJul 2026Introducing Federated Event Streams for Fusion 16.4Federated Event Streams add broker-backed, resumable GraphQL subscriptions to Fusion 16.4, with stateless gateway scaling and client-owned resume cursors.Read](https://chillicream.com/blog/2026-07-06-federated-event-streams) ## See whether it fits your architecture. Read the docs, run a focused evaluation, and talk to a maintainer about your architecture. Then decide. [Read the docs](https://chillicream.com/docs) [Talk to a maintainer](https://slack.chillicream.com/) --- # GraphQL Schema Checks for Safer Releases > Run GraphQL schema checks against published client operations. See which versions a proposed change could break before you merge or deploy. Canonical source: https://chillicream.com/platform/release-safety Nitro tests a proposed schema against the operations your clients publish to the target environment. See which operations and client versions a change could break, then fix it before users feel it. [Start Nitro for Free](https://nitro.chillicream.com/) [Read client registry docs](https://chillicream.com/docs/nitro/apis/client-registry) schema.graphql @@ type Order @@ 4141 type Order { 4242 id: ID! 43+ totalAmount: Money! 43\- total: Float! RRegistryBREAKINGline 43 Removing `Order.total` breaks queries that still select it. Queries and mutations from 3 client versions published to this stage are affected. Deprecate it, then remove it after those versions are retired or unpublished from the stage. 4444 status: OrderStatus! 45+ placedAt: DateTime @deprecated(reason: "use createdAt") 4546 } registry check failed1 breaking · 1 dangerous · 1 safe ## Block breaking schema changes at the pull request. Nitro returns a failed check when a proposed schema would break an operation published by a client. Configure that check as required in your repository, and the pull request cannot merge until the schema passes. #482 · Add Money type FAILRegistry check1 breaking · 2 safe Schema validation: breaking changeOrder.total removed Schema validation: additiveMoney, totalAmount added Client compatibility: partner appvalidating... Required CI policy blocks merge until checks pass.Re-run check ## See which clients a change would break. Validation runs against the operations your client versions have published to that environment. Each client gets its own result: a change can be safe for web and still break mobile, and you see that before you merge. client registry·impact of #482 | client | operations passing | status | | --------------------- | ------------------ | -------------- | | webproduction | 5/5 | OK | | mobileproduction | 3/5 | at risk | | partnersandbox | none published | outside result | | internal-adminstaging | 6/6 | OK | ## Every environment is its own gate. Development, staging, and production can each hold a different set of published operations. Validate against the environment you plan to update, because a change that passes staging may still affect client versions published to production. Development Passed Staging Failed Production Future ## Run the checks in the CI you already have. The validate, upload, and publish steps ship as ready-made GitHub Actions and Azure Pipelines tasks, both wrapping the Nitro CLI. [GitHub Actions.github/workflows/ci.yml\- uses: ChilliCream/nitro-schema-validate@v16 with: api-id: ${{ vars.NITRO\_API\_ID }} api-key: ${{ secrets.NITRO\_API\_KEY }} schema-file: ./schema.graphql stage: production comment-mode: review✓ check passedGitHub Marketplace](https://github.com/marketplace?query=nitro) [Azure Pipelinesazure-pipelines.yml\- task: NitroSchemaValidate@16 inputs: authenticationType: serviceConnection nitroServiceConnection: nitro-prod apiId: $(NITRO\_API\_ID) schemaFile: ./schema.graphql stage: production✓ task succeededVisual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=ChilliCream.nitro-azure-pipelines-tasks) [Any other CIshell$ nitro schema validate \\ \--api-id $NITRO\_API\_ID \\ \--schema-file schema.graphql \\ \--stage production validating against production… ✓ no breaking changes✓ exit 0CLI reference](https://chillicream.com/docs/nitro/cli/schema) ## Keep the full history of your schema. Every uploaded or published schema leaves a browsable version in the registry, so your team can see what changed, how Nitro classified it, and which published clients the change could affect. See when a field changed without digging through merge commits. schema history 1. v12add Cart.discountSAFE 2. v13deprecate Order.placedAtDANGEROUS 3. v14remove Order.total (blocked)BREAKING 4. v14add Order.totalAmountSAFE 5. v15remove Order.total (no published client selects it)BREAKING ## Know what breaks before your users do. Publish the operations each client uses, validate proposed schemas against the environment you plan to update, and merge with the answer in hand. [Start Nitro for Free](https://nitro.chillicream.com/) [Read Client Registry Docs](https://chillicream.com/docs/nitro/apis/client-registry) --- # Nitro Pricing and Deployment Plans > Start Nitro free on shared cloud, pay as you go from $20 per month, choose Dedicated from $400, BYOC, or self-hosted. Compare usage, retention, and support. Canonical source: https://chillicream.com/pricing Nitro pricing Start on the shared cloud, pay for more usage as your API grows, choose a dedicated or BYOC deployment for greater isolation and control, or run Nitro on your own infrastructure. [Start Nitro for Free](https://nitro.chillicream.com/) [Discuss a private deployment](https://chillicream.com/services/support/contact?subject=Sales&context=Private%20Nitro%20Deployment) ## Nitro pricing plans ### Free Shared cloud, fully managed. $0 - Shared multi-tenant cloud - Schemas & environments included - 1M operations / month - 2 GB ingest / month - 3-day log & trace retention - Community support [Start Nitro for Free](https://nitro.chillicream.com/) ### Pay as you go Shared cloud, usage based. $20 per month - Shared multi-tenant cloud - 5M operations included, then $2 / million - 2 GB ingest per 1M ops, then $1.15 / GB - 60-day log & trace retention - Email support [Start Nitro for Free](https://nitro.chillicream.com/) Dedicated Deployment ### Dedicated Single-tenant, volume based. From $400 per month - Single-tenant cloud or BYOC - Priced by instance size - Configurable retention - Private networking - SSO, audit log, role-based access [Discuss Dedicated](https://chillicream.com/services/support/contact?subject=Sales&context=Dedicated%20Nitro%20Deployment) ### Self-Hosted Your infrastructure. Custom - Run on your own infrastructure - Air-gapped & on-prem supported - Configurable retention - Priority engineering support - Long-term release channel [Discuss Self-Hosted](https://chillicream.com/services/support/contact?subject=Sales&context=Self-Hosted%20Nitro) Compare plans ## Compare Nitro plans, usage, and deployment | Capability | Free | Pay as you go | Dedicated | Self-Hosted | | ------------------------------------------- | ------------------------- | -------------------------------- | --------------------------- | ------------------------- | | Plans & usage | | | | | | Monthly price | $0 | $20 / month | From $400 / month | Custom | | Deployment model | Multi-tenant cloud | Multi-tenant cloud | Single-tenant cloud or BYOC | Your infrastructure | | Included operations / month | 1M | 5M, then $2 / million | Volume based | Unmetered | | Included ingest | 2 GB | 2 GB per 1M ops, then $1.15 / GB | Volume based | Unmetered | | Data retention | 3 days | 60 days | Configurable | Configurable | | Pricing model | Free, capped | Usage based | Volume based | Your infrastructure | | Gateway & server | | | | | | OAuth 2.0 & OpenID Connect | Included | Included | Included | Included | | Authorization policies & roles | Included | Included | Included | Included | | Rate limiting | Included | Included | Included | Included | | Response caching | Included | Included | Included | Included | | Realtime subscriptions | Included | Included | Included | Included | | GraphQL Federation | Included | Included | Included | Included | | Custom middleware & plugins | Included | Included | Included | Included | | Schema lifecycle | | | | | | Schema registry with history & rollback | Included | Included | Included | Included | | Client registry | Included | Included | Included | Included | | Breaking-change classification | Included | Included | Included | Included | | CI schema & client checks | Included | Included | Included | Included | | Stage promotion with approval gates | Included | Included | Included | Included | | Fusion deployment orchestration | Included | Included | Included | Included | | .NET Aspire integration | Included | Included | Included | Included | | Observability | | | | | | OpenTelemetry-native traces, metrics, logs | Included | Included | Included | Included | | Operation insights | Included | Included | Included | Included | | Per-client tracking | Included | Included | Included | Included | | Resolver-level insights | Included | Included | Included | Included | | Distributed tracing across Fusion subgraphs | Included | Included | Included | Included | | Service monitoring for any .NET service | Included | Included | Included | Included | | Operation reporting | Included | Included | Included | Included | | Operations & delivery | | | | | | Persisted / trusted operations enforcement | Included | Included | Included | Included | | Query cost analysis | Included | Included | Included | Included | | Request limits | Included | Included | Included | Included | | Deployment audit log | Included | Included | Included | Included | | Rollback by republishing an earlier tag | Included | Included | Included | Included | | Persisted-op distribution cache | Included | Included | Included | Included | | Security & access | | | | | | Roles & stage-scoped publish permissions | Not included | Not included | Included | Included | | SSO | Not included | Not included | Included | Included | | Audit log | Not included | Not included | Included | Included | | API keys and PATs | Included | Included | Included | Included | | Developer experience | | | | | | Built-in GraphQL IDE | Served from your endpoint | Served from your endpoint | Served from your endpoint | Served from your endpoint | | MCP adapter | Included | Included | Included | Included | | OpenAPI adapter | Included | Included | Included | Included | | Support | | | | | | Support channel | Community | Email | Email + private chat | Priority engineering | | Release channel | Continuous | Continuous | Continuous | Long-term release channel | | Onboarding & training | Docs & community | Docs & community | Guided onboarding | Custom training | FAQ ## Common questions What is included in the Nitro Free plan? The Free plan runs on the shared cloud and includes schemas and environments, 1 million operations, 2 GB of ingest per month, and 3-day log and trace retention for $0 per month. How does Pay as you go pricing work? Pay as you go is $20 per month and includes 5 million operations, 2 GB of ingest per million operations, and 60-day retention. Additional usage is $2 per million operations and $1.15 per GB of ingest. How is a Dedicated Nitro instance priced? Dedicated starts at $400 per month and is priced by instance size and volume. It supports a single-tenant ChilliCream cloud deployment or BYOC, with configurable retention and private networking. When should I choose Self-Hosted Nitro? Choose Self-Hosted when Nitro must run on your infrastructure, including on-premises or air-gapped environments. The plan has custom pricing, configurable retention, a long-term release channel, and priority engineering support. Which plans include SSO and audit logs? Dedicated and Self-Hosted include SSO, an audit log, and roles with stage-scoped publish permissions. Free and Pay as you go do not include those access-control features in the current comparison. How do I choose between Dedicated, BYOC, and Self-Hosted? Choose Dedicated for a single-tenant deployment managed in the ChilliCream cloud, BYOC to run a dedicated instance in your cloud account, or Self-Hosted to run Nitro on your own infrastructure. Contact us to review isolation, networking, retention, and data-location requirements. Private deployment ## Regulated industry or air-gapped? Bring us your infrastructure, network, data-location, and procurement constraints. We will help map them to the right Nitro deployment architecture and commercial plan. [Discuss deployment requirements](https://chillicream.com/services/support/contact?subject=Sales&context=Private%20Nitro%20Deployment) [Explore the platform](https://chillicream.com/platform) - Procurement and security requirements - Dedicated, BYOC, or on-prem deployment - Data location and network constraints ## Start free. Scale when you do. The Free plan includes 1 million operations, 2 GB of ingest per month, schemas and environments, and 3-day log and trace retention. [Start Nitro for Free](https://nitro.chillicream.com/) [Explore Nitro docs](https://chillicream.com/docs/nitro) Need a private deployment? Compare Dedicated and Self-Hosted above. --- # Hot Chocolate: GraphQL Server for .NET > Hot Chocolate is the GraphQL server for .NET: build type-safe APIs with C# schema authoring, DataLoader, subscriptions, security, OpenTelemetry, and Fusion. Canonical source: https://chillicream.com/products/hotchocolate GraphQL Server for .NET The fastest way to build production GraphQL APIs in .NET. Type-safe end to end, federation-ready, and battle-tested at scale. [Build Your First GraphQL API](https://chillicream.com/docs/hotchocolate/get-started-with-graphql-in-net-core) [View on GitHub](https://github.com/ChilliCream/graphql-platform) ## Built for Production ### C# Schema Authoring Define the schema from your C# implementation or use fluent descriptors when you need precise control. Mix both approaches in the same app. ### DataLoader Batching Batch and cache related data fetches to reduce backend requests and address N+1 query patterns. ### Realtime Subscriptions Serve GraphQL over HTTP and deliver real-time results over WebSockets or Server-Sent Events from ASP.NET Core. ### OpenTelemetry Built In Emit GraphQL request, resolver, and DataLoader spans through the built-in OpenTelemetry integration. ### Cost Analysis and Trusted Documents Set operation cost budgets for open APIs or limit first-party apps to pre-registered documents. ### Federation-ready Start with one Hot Chocolate server, then compose services behind Fusion when teams need independent ownership and deployment. ## MIT Licensed, Free to Use Use, modify, and distribute Hot Chocolate in commercial or private projects under the terms of the MIT license. The source is available in the ChilliCream GraphQL Platform repository. [Add a Type-Safe .NET Client](https://chillicream.com/products/strawberryshake) [Scale Out with Fusion](https://chillicream.com/docs/fusion) --- # Mocha: Messaging Framework for .NET > Mocha is a .NET messaging framework with a source-generated mediator for in-process work and a message bus for commands and events between services. Canonical source: https://chillicream.com/products/mocha Mocha is a .NET messaging framework that sends commands and events between your services, for example telling the shipping service that an order was placed. It also runs commands and queries inside a single service, with no broker involved. Define messages and handlers in C#, and Mocha generates the handler registration at build time. [Publish your first message](https://chillicream.com/docs/mocha/quick-start) [Read the docs](https://chillicream.com/docs/mocha) ## Answer the caller now. Finish the rest in the background. When a customer checks out, the order service can confirm the order right away. Billing, inventory, shipping, and search keep working in the background, each at its own pace. Mocha gives you two ways to do this: send a message you don't need to wait for, or send a request and wait for the reply. [See how messaging works](https://chillicream.com/docs/mocha) ## Start with a message and a handler. Messages in Mocha are represented as C# records and processed by handler classes. Define the message, implement its handler interface, and Mocha takes care of registering the handler at build time. Its analyzers also detect invalid or duplicate handlers before the application starts. [Open the quickstart](https://chillicream.com/docs/mocha/quick-start) ## You write the code. Mocha wires up the broker. Mocha can automatically configure the messaging infrastructure your services need. Based on the messages they send and receive, it creates the appropriate routes and transport resources. This configuration is called the topology. Mocha sets up the topology at startup, so naming and configuration conflicts surface early. You can also define the transport configuration yourself when you need more control. ``` builder.Services .AddMessageBus() .AddOrderService() .AddRabbitMQ(); // exchanges, queues, and bindings are derived // from your handlers, validated at startup ``` [See routing and endpoints](https://chillicream.com/docs/mocha/routing-and-endpoints) The default ``` builder.Services .AddMessageBus() .AddOrderService() .AddRabbitMQ(); // exchanges, queues, and bindings are derived // from your handlers, validated at startup ``` Opt out ``` .AddRabbitMQ(transport => { transport .DeclareExchange("region-events") .Type(RabbitMQExchangeType.Topic) .Durable(); transport.Queue("orders") .BindExplicitly() .MaxConcurrency(10); }); ``` ## Mocha is also a mediator. Mocha does more than move messages between services. Its mediator dispatches commands and queries inside your own process, with no broker involved. It builds the dispatch pipeline at build time and runs your middleware around each handler, so the hot path avoids reflection. ``` public record PlaceOrderCommand( Guid ProductId, int Quantity) : ICommand; var result = await sender.SendAsync( new PlaceOrderCommand(productId, 2), ct); ``` [Read about the mediator](https://chillicream.com/docs/mocha/mediator) The command ``` public record PlaceOrderCommand( Guid ProductId, int Quantity) : ICommand; ``` Dispatch ``` var result = await sender.SendAsync( new PlaceOrderCommand(productId, 2), ct); ``` The handler ``` public class PlaceOrderCommandHandler(AppDbContext db) : ICommandHandler { public async ValueTask HandleAsync( PlaceOrderCommand command, CancellationToken ct) { // create the order, return the result } } ``` ## Publish an event. Each subscriber handles it at its own pace. Each subscriber gets its own durable queue, set up before you start publishing. From there, it processes events at its own pace and picks up where it left off after a restart. How reliably messages are delivered, and how long they're kept, depends on the transport (in-memory, broker-backed, or database-backed) and the topology you configure. ``` await bus.PublishAsync(orderPlaced, ct); ``` [See messaging patterns](https://chillicream.com/docs/mocha/messaging-patterns) The event ``` public sealed record OrderPlaced( Guid OrderId, decimal Amount); ``` Publish ``` await bus.PublishAsync(orderPlaced, ct); ``` A subscriber ``` public class OrderPlacedHandler(AppDbContext db) : IEventHandler { public async ValueTask HandleAsync( OrderPlaced message, CancellationToken ct) { // react on this service's schedule } } ``` ## Hand off a command without waiting for the handler. Use a command when one service needs to do work without making the caller wait for it to finish. Mocha routes the command to its handler. You choose the transport and reliability settings that fit the job. ``` await bus.SendAsync( new ReserveInventoryCommand(orderId), ct); ``` [See messaging patterns](https://chillicream.com/docs/mocha/messaging-patterns) The command ``` public sealed record ReserveInventoryCommand( Guid OrderId); ``` Send ``` await bus.SendAsync( new ReserveInventoryCommand(orderId), ct); ``` The handler ``` public class ReserveInventoryHandler(Warehouse wh) : IEventRequestHandler { public async ValueTask HandleAsync( ReserveInventoryCommand command, CancellationToken ct) { // runs later, on the queue's time } } ``` ## Wait for a typed response from another service. Use request/reply when a caller needs an answer from one service and can wait for it. Mocha matches the reply to the original request and returns a typed response, or times out if none arrives. If the handler fails, it surfaces the error through the same reply channel. ``` var product = await bus.RequestAsync( new GetProductRequest(id), ct); ``` [See messaging patterns](https://chillicream.com/docs/mocha/messaging-patterns) The request ``` public sealed record GetProductRequest(Guid Id) : IEventRequest; ``` Ask ``` var product = await bus.RequestAsync( new GetProductRequest(id), ct); ``` The handler ``` public class GetProductHandler(Catalog catalog) : IEventRequestHandler< GetProductRequest, ProductResponse> { public async ValueTask HandleAsync( GetProductRequest request, CancellationToken ct) { // the returned value rides back as the reply } } ``` ## Process messages in batches. When several messages can be processed together, batching can reduce the amount of work your application has to do. Mocha collects messages until the batch reaches a configured size or a timeout expires, then passes them to the handler in a single call. This is especially useful for bulk operations, such as writing many records to a database at once instead of issuing a separate request for each message. ``` .AddBatchHandler( o => o.MaxBatchSize = 100); ``` [Read about batch handlers](https://chillicream.com/docs/mocha/handlers-and-consumers) Registration ``` .AddBatchHandler( o => o.MaxBatchSize = 100); ``` The handler ``` public class OrderPlacedBatchHandler(AppDbContext db) : IBatchEventHandler { public async ValueTask HandleAsync( IMessageBatch batch, CancellationToken ct) { // one call, up to 100 messages } } ``` ## Publish a message at a time you choose. Sometimes you don't want to process a message immediately. Mocha lets you schedule a message for delivery at a specific time or after a delay, making it useful for reminders, delayed retries, and other deferred work. Whether a scheduled message survives restarts or can be cancelled depends on the scheduling store that holds it until it is due. ``` var result = await bus.SchedulePublishAsync( new SendWelcomeEmail(userId), DateTimeOffset.UtcNow.AddMinutes(30), ct); ``` [Read about scheduling](https://chillicream.com/docs/mocha/scheduling) Schedule ``` var result = await bus.SchedulePublishAsync( new SendWelcomeEmail(userId), DateTimeOffset.UtcNow.AddMinutes(30), ct); ``` Cancel ``` // still cancellable until it is dispatched await bus.CancelScheduledMessageAsync( result.Token!, ct); ``` ## Avoid duplicate writes when a broker redelivers a message. Message brokers may deliver the same message more than once, such as after a crash or a lost acknowledgment. Mocha prevents those redeliveries from repeating the same work by recording which messages have already been processed. The inbox and the handler's changes are committed together, while any outgoing messages are held until that transaction succeeds. For database operations, this provides effectively exactly-once processing for as long as the inbox record is retained. [Read about reliability](https://chillicream.com/docs/mocha/reliability) ## Keep long-running workflows in one state machine. Some workflows cannot be completed by a single message. They unfold over several steps and may need to react to events, failures, or timeouts along the way. A saga keeps track of that progress and determines what should happen next. Its state can be stored in memory or persisted when the workflow needs to survive restarts. If part of the workflow fails, the saga can trigger compensating actions to undo earlier steps. Timeout handling requires a scheduling store that supports delayed messages. [Read about sagas](https://chillicream.com/docs/mocha/sagas) ## See where a message went and what handled it. When a message passes through several services, it can be difficult to see where time was spent or where something went wrong. Mocha integrates with OpenTelemetry to connect dispatch, receive, and handler activity into a single trace that follows the message across the system. Those traces can be sent to Nitro, where you can inspect slow handlers, failed messages, and delays between services. [Set up observability](https://chillicream.com/docs/mocha/observability) a ### One message, end to end TRACE · 7f3a·9b2e b ### Correlated across the gap OTEL publish -> consume, end to end delivery latency Correlation propagated ## Separate application logic from infrastructure. Your messaging infrastructure will often change as your application grows. You might start with an in-memory transport during development, move to a database-backed option for simpler deployments, or use a message broker in a distributed system. Mocha keeps those infrastructure choices out of your handlers. The same message-handling code works across transports, while each transport provides its own delivery guarantees, durability, scheduling support, and routing model. ``` builder.Services .AddMessageBus() .AddOrderService() // source-generated .AddRabbitMQ(t => t.IsDefaultTransport()) .AddEventHub(t => t.Handler()); ``` [Compare transports](https://chillicream.com/docs/mocha/transports) Registration ``` builder.Services .AddMessageBus() .AddOrderService() // source-generated .AddRabbitMQ(t => t.IsDefaultTransport()) .AddEventHub(t => t.Handler()); ``` The claimed handler ``` public class DeviceTelemetryHandler(Ingest ingest) : IEventHandler { public async ValueTask HandleAsync( DeviceTelemetry message, CancellationToken ct) { // same shape, different transport } } ``` ## Publish your first message with Mocha. You can start with a single process using the in-memory transport, a message, and a handler. As your application grows, switching to a broker-backed or database-backed transport doesn't require changing your handlers. You simply gain the delivery, durability, and reliability features your application needs. [Publish your first message](https://chillicream.com/docs/mocha/quick-start) [Read the docs](https://chillicream.com/docs/mocha) --- # Nitro: GraphQL Observability Platform > Nitro is the API operations platform for observability, OpenTelemetry tracing, schema governance, client safety, and GraphQL release checks in one control plane. Canonical source: https://chillicream.com/products/nitro API Operations Platform Nitro brings observability, tracing, schema governance, client safety, and rollout checks together, so teams can understand what is running and ship changes with confidence. [Open the Web App](https://nitro.chillicream.com/) [Launch Nitro](https://nitro.chillicream.com/) See everything your gateway is doing Monitoring overview to operation breakdown to distributions to the exact slow trace span. Observe Diagnose Fusion Schema Author 01Observe ## See exactly how your API behaves in production. OpenTelemetry-native monitoring: latency, throughput, and error rate per operation and per client, ranked by the impact score that tells you what to fix first. Nitro / productionlive Requests / sec last 15m p95 0ms Error rate 0.00% Throughput 0rps Trace waterfall / checkoutOrder 212 ms Safe`+ field User.displayName: String` 02Operations Console ## See the health of your API at a glance. Track p95 and p99 latency, throughput, error rate, top clients, and the slowest spans from the same telemetry source. ### Latency p95p99 ### Throughput ops / min ### Top clients by impact web-storefront mobile-ios partner-api admin-console analytics-etl ### Error rate % of requests 0.31%within budget · 1.6% peak ### Impact score what hurts most | Subgraph | Avg latency | Throughput | Errors | Latency | Impact | | ------------------ | ----------- | ---------- | ------ | ------- | ------ | | Qmutation checkout | 168 ms | 1.2k/m | 3.1% | | | | Qquery cart | 44 ms | 4.8k/m | 0.4% | | | | Qquery search | 72 ms | 2.1k/m | 1.2% | | | ### Slow span checkout 0ms50ms100ms150ms168.0 ms 🌐POST /graphql168.0 ms ◈mutation checkout158.0 ms ↪PricingService.quote34.0 ms ⚙InventoryDb.reserve96.0 ms ↪PaymentGateway.charge18.0 ms 03Trace ## Follow one request across your whole backend. Distributed tracing stitches a single operation across GraphQL, REST, gRPC, and background jobs. Walk the span waterfall down to the resolver that ran slow. 04Diagnose ## Move from symptoms to cause. Start with an error spike, open the affected operation, inspect the trace, and find the failing stack frame without digging through disconnected logs. 05Compose ## Understand how your operations are executed. Inspect distributed execution plans, see how work is split across services, and understand how one request becomes one response. 06Schema Governance ## Know the impact before you merge. Nitro compares schema changes with published clients and gives teams a clear signal on what is safe, risky, or breaking. orders-api · v14publish blocked `+ Order.deliveryEstimate: DateTime`SAFE `~ Product.price: Float → Money`DANGEROUS `- Order.total: Float`BREAKING 1 safe · 1 dangerous · 1 breaking 07Delivery ## Ship with confidence. Nitro turns release readiness into visible checks: schema validation, client compatibility, trusted operations, and rollout status. CI schema checkFAILED schema validate127 fields client checks3 published clients breaking changeOrder.total removed trusted operations482 hashes signed merging is blocked until every check passes Persisted operations With strict trusted documents configured, production accepts registered operation IDs instead of arbitrary GraphQL documents. POST /graphql documentId: sha256:7f3a9b2e… 200 · trusted · 12 ms ● safe rolloutstage → canary → prod 08Workspace ## Give teams a shared workspace. Explore schemas, run operations, validate documents, and keep important API work connected to the rest of Nitro. Ready when you are ## Put your API on one control plane. Start with production visibility, then add tracing, schema governance, client safety, and release checks as your team grows. [Start Nitro for Free](https://nitro.chillicream.com/) [Compare Nitro Plans](https://chillicream.com/pricing) --- # Strawberry Shake: GraphQL Client for .NET > Strawberry Shake is a type-safe GraphQL client for .NET that generates C# clients at build time and adds reactive caching and WebSocket subscriptions. Canonical source: https://chillicream.com/products/strawberryshake GraphQL Client for .NET A strongly-typed GraphQL client for .NET with reactive state, caching, and subscriptions baked in. [Build Your First .NET Client](https://chillicream.com/docs/strawberryshake/get-started) [View on GitHub](https://github.com/ChilliCream/graphql-platform) ## Built for .NET Teams ### Strongly-typed Client Write operations in .graphql files and build your project. Strawberry Shake generates typed C# results, inputs, variables, and client APIs. ### Normalized Reactive Store Normalize GraphQL results into an entity store that keeps watched operations and UI components in sync as data changes. ### Flexible Fetch Strategies Choose network-only, cache-first, or cache-and-network behavior for each operation and reuse cached entities across results. ### WebSocket Subscriptions Consume GraphQL subscription streams through the same generated client and update the reactive store as new results arrive. ## MIT Licensed Use, modify, and distribute Strawberry Shake in commercial or private projects under the terms of the MIT license. It works with spec-compliant GraphQL servers, including Hot Chocolate. [Build a GraphQL Server for .NET](https://chillicream.com/products/hotchocolate) [Explore Reactive Caching](https://chillicream.com/docs/strawberryshake/caching) --- # Company Resources > Find ChilliCream contact details, GraphQL services, Nitro pricing, commercial license terms, company policies, and official merchandise in one place. Canonical source: https://chillicream.com/resources Resources Contact the team, compare commercial options, review our policies and license terms, or visit the official ChilliCream shop. ## Company links and policies [Contact ChilliCreamDiscuss Nitro, GraphQL services, training, or support.](https://chillicream.com/services/support/contact) [GraphQL servicesCompare advisory, support, and team training.](https://chillicream.com/services) [Nitro pricingCompare shared-cloud, dedicated, and self-hosted plans.](https://chillicream.com/pricing) [ShopChilliCream merch and goodies.](https://store.chillicream.com/) [Acceptable Use PolicyRules for using ChilliCream services.](https://chillicream.com/legal/acceptable-use-policy) [Cookie PolicyHow we use cookies.](https://chillicream.com/legal/cookie-policy) [Privacy PolicyHow we handle your data.](https://chillicream.com/legal/privacy-policy) [Terms of ServiceThe agreement between you and us.](https://chillicream.com/legal/terms-of-service) [ChilliCream LicenseCommercial license terms.](https://chillicream.com/licensing/chillicream-license) --- # GraphQL Services for .NET Teams > Work with the engineers behind Hot Chocolate, Fusion, and Nitro through GraphQL consulting, production support plans, or training for your .NET team. Canonical source: https://chillicream.com/services ChilliCream services Choose a focused consulting engagement, ongoing production support, or team training from the engineers behind Hot Chocolate, Fusion, and Nitro. [Discuss your project](https://chillicream.com/services/support/contact?subject=Sales&context=GraphQL%20Services) [Find the right help](https://chillicream.com/help) ## Choose the right way to work with us [GraphQL advisoryReview an architecture, resolve a hard technical decision, or bring in the engineers who build the stack for a scoped implementation.Learn more →](https://chillicream.com/services/advisory) [GraphQL supportAdd a private support channel, defined incident allowances, and response times for the systems your team runs in production.Learn more →](https://chillicream.com/services/support) [GraphQL trainingBuild shared GraphQL, Hot Chocolate, and Fusion skills through a curriculum shaped around your team's experience and codebase.Learn more →](https://chillicream.com/services/training) --- # GraphQL Consulting and Advisory > Get GraphQL consulting or a scoped implementation from the engineers behind Hot Chocolate, Fusion, and Nitro. Review your architecture or plan the build. Canonical source: https://chillicream.com/services/advisory ChilliCream Advisory Get focused consulting in packages of hours, or bring in the team behind Hot Chocolate, Fusion, and Nitro for a scoped implementation. Bring a question, a design, or a deadline. We meet you where the work is. [Discuss your GraphQL project](https://chillicream.com/services/support/contact?subject=Sales&context=GraphQL%20Advisory) [Email the advisory team](mailto:contact@chillicream.com?subject=Consulting) ## GraphQL consulting and contracting options Start here Packages of hours ### Consulting Use a package of hours for architecture, troubleshooting, code review, or guidance at a specific point in your project. 20hincrements Best for Teams that already own the build and need a senior GraphQL engineer on call for design, troubleshooting, and review. What is included - Architecture and schema design - Fusion composition and rollout guidance - Performance troubleshooting - Code and design review - Team mentoring [Discuss consulting](https://chillicream.com/services/support/contact?subject=Sales&context=GraphQL%20Advisory) [Email the team](mailto:contact@chillicream.com?subject=Consulting) Scoped engagements ### Contracting Scope an implementation when your team needs delivery capacity or deep product expertise, from proof of concept through production rollout. Customscope & timeline Best for Teams that want our engineers to deliver a working result, from a proof of concept to a production rollout. What is included - Technical discovery and scope - Proof of concept - Production implementation - Milestones and defined deliverables [Scope an engagement](https://chillicream.com/services/support/contact?subject=Sales&context=GraphQL%20Advisory) [Email the team](mailto:contact@chillicream.com?subject=Contracting) How an engagement starts ## From first call to first commit in three steps. Speak directly with an engineer, get a written proposal, and kick off with the scope, deliverables, and working model agreed in advance. Step 01 ### Introductory call Walk us through the system, the decision or outcome you need, and the constraints that shape the work. - Current architecture and stack - Goal, timeline, and constraints - NDA requirements discussed up front Step 02 ### Proposal If there is a fit, the proposal matches the engagement to your need: a package of consulting hours or a scoped statement of work. - Hour package or fixed scope - Clear deliverables and milestones - Commercial terms in writing Step 03 ### Kickoff Once the scope and terms are agreed, we establish the working channels, backlog, and delivery checkpoints for the engagement. - Shared working channel - Visible backlog and checkpoints - Direct access to the engineers doing the work Who you work with ## The team behind Hot Chocolate, Fusion, and Nitro. ChilliCream advisory is not a generalist consultancy that learned GraphQL last quarter. The engineers on the call are the ones who write the framework you depend on. ### Who you work with Senior engineers from the core team. The same people who maintain Hot Chocolate, design Fusion, and ship Nitro. - Direct line to the maintainers - Pull-request authors on the core code - Same team across consulting and contracting ### What we work on The full GraphQL stack we build: schema design, federation with Fusion, ASP.NET Core integration, performance and resolver tuning, MCP, and Nitro observability and CI. - Schema and federation design - Fusion composition and rollout - Nitro observability, CI, and persisted ops ### How we work The proposal defines the codebase, channels, checkpoints, and deliverables for the engagement, so your team knows how the work will run before it starts. - Work tied to your code and architecture - Shared delivery checkpoints - Defined written deliverables Frequently asked ## Honest answers before you reach out. How is consulting priced? Consulting is sold in agreed packages of hours. Contracting engagements are scoped separately with a statement of work that defines the deliverables, milestones, and commercial terms. How small is too small for an engagement? Consulting starts in 20-hour increments, so you can use a focused package to unblock a specific decision, review, or troubleshooting problem. Contracting is the better fit when you want ChilliCream engineers to deliver an agreed result against a defined scope. Do you sign an NDA? Tell us about your NDA and data-handling requirements before sharing code, schemas, or traces. We can discuss a mutual NDA and agree how project material will be handled as part of discovery. How quickly can you start? Availability depends on the scope and current schedule. We will give you a realistic start date during discovery instead of promising a slot before we understand the work. What outcomes can I expect? Concrete, written deliverables tied to your goal: an architecture decision record, a schema review with line-level comments, a working proof of concept, or a production implementation. The proposal defines the outcome before work starts. We do not bill for slideware. Who actually does the work? The engineers who build Hot Chocolate, Fusion, and Nitro. The same people who write the framework code, review the pull requests, and answer the hard issues on GitHub are the people on your call. Ready when you are ## One call is usually enough to know. Tell us what you are building. You walk us through it, we ask the hard questions, and you leave with a clear next step. If we are not the right fit, we will tell you. - Consulting Packages of hours - Contracting Scoped statement of work - NDA Discuss before sharing materials - Timing Confirmed during discovery [Discuss your project](https://chillicream.com/services/support/contact?subject=Sales&context=GraphQL%20Advisory) [Email the advisory team](mailto:contact@chillicream.com?subject=Consulting) --- # GraphQL Support Plans for .NET Teams > Get GraphQL support from the engineers behind Hot Chocolate, Fusion, and Nitro, with private channels, incident allowances, and defined response times. Canonical source: https://chillicream.com/services/support GraphQL support plans Work with the engineers who build Hot Chocolate, Fusion, and Nitro, not a first-line queue. Choose the plan with the incident allowances, response times, and support channels your production team needs. Technical questions ## Skip the first-line queue Bring an exception, a confusing behavior, or a focused implementation question to the people who know the code behind your stack. Production incidents ## Escalate with a defined response time Open a critical incident through your plan's support channel and track it against the incident allowance and response time in your agreement. Ongoing operations ## Keep the product team close Use the private channels, issue tracking, and status reviews included with your plan to keep technical context available as your GraphQL platform evolves. [Compare support plans](#plans) [Discuss support needs](https://chillicream.com/services/support/contact) Plans ## Four plans. Pick the one that fits. ### Community For hackers and side projects Free - Public Slack channel [Join community Slack](https://slack.chillicream.com/) ### Startup Small teams, steady cadence $450 per month - Private Slack channel - 2 critical incidents [Discuss Startup](https://chillicream.com/services/support/contact?subject=Pricing%20%26%20Plans&context=Startup%20Support) Business Coverage ### Business Larger teams, critical work $1,300 per month - Private Slack channel - 5 critical incidents - Non-critical incident coverage - Email support [Discuss Business](https://chillicream.com/services/support/contact?subject=Pricing%20%26%20Plans&context=Business%20Support) ### Enterprise Whole-org coverage, tailored terms Custom - Private Slack channel - Unlimited critical incidents - 10 non-critical incidents - Phone support - Dedicated account manager - Status reviews [Discuss Enterprise](https://chillicream.com/services/support/contact?subject=Pricing%20%26%20Plans&context=Enterprise%20Support) Compare plans ## Compare GraphQL support coverage | Capability | Community | Startup | Business | Enterprise | | ---------------------------- | ------------ | --------------------- | -------------------------- | ---------------------- | | Response & incidents | | | | | | Critical incidents | Not included | 2 (next business day) | 5 (next business day) | Unlimited (24 hours) | | Non-critical incidents | Not included | Not included | Included (3 business days) | 10 (next business day) | | Channels | | | | | | Public Slack channel | Included | Included | Included | Included | | Private Slack channel | Not included | Included | Included | Included | | Private issue tracking board | Not included | Not included | Included | Included | | Email support | Not included | Not included | Included | Included | | Phone support | Not included | Not included | Not included | Included | | Strategic | | | | | | Dedicated account manager | Not included | Not included | Not included | Included | | Status reviews | Not included | Not included | Not included | Included | FAQ ## Common questions What counts as a critical incident? An incident is critical when a production system you run on Hot Chocolate, Fusion, or Nitro is down, returning wrong data, or otherwise hard-blocked. Anything that degrades a live user experience qualifies. Local dev issues and questions are non-critical. How fast do you respond? Startup and Business respond to critical incidents by the next business day. Enterprise responds to critical incidents within 24 hours. Business responds to non-critical incidents within 3 business days, while Enterprise responds by the next business day. Community Slack is best effort. How is an incident opened and tracked? Startup, Business, and Enterprise include a private Slack channel. Business and Enterprise also include a private issue tracking board. Enterprise adds email and phone support to the listed channels. Which paid support plan should we choose? Startup fits a team that needs a private channel and limited critical-incident coverage. Business adds non-critical incidents, email support, and private issue tracking. Enterprise adds tailored terms, more channels, status reviews, and a dedicated account manager. What products can a plan cover? The support page covers Hot Chocolate, Fusion, and Nitro. Tell us which products and deployment models are on your critical path so the commercial agreement can reflect the systems your team operates. How is support different from an advisory engagement? A support plan provides ongoing channels, incident allowances, and response terms. Advisory is scoped around a design decision, review, troubleshooting session, or implementation. Teams can use either service or combine them. Enterprise ## Your coverage, your teams, your platform. Enterprise is for organizations running Hot Chocolate, Fusion, or Nitro across multiple teams. Get a 24-hour critical response time, phone support, status reviews, and a dedicated account manager, with final terms defined in your agreement. - Tailored response times - Dedicated account manager - Phone support - Status reviews - Unlimited critical incidents - Private issue tracking board [Discuss Enterprise support](https://chillicream.com/services/support/contact?subject=Pricing%20%26%20Plans&context=Enterprise%20Support) [See advisory engagements](https://chillicream.com/services/advisory) ## Ready when you are. Join the public Slack for best-effort community help, or tell us which products, teams, and incident response needs a paid plan should cover. [Discuss a support plan](https://chillicream.com/services/support/contact?subject=Pricing%20%26%20Plans&context=GraphQL%20Support) [Join community Slack](https://slack.chillicream.com/) --- # Contact GraphQL Experts > Contact ChilliCream about Nitro pricing, GraphQL consulting, team training, partnerships, or technical support for Hot Chocolate, Fusion, and Nitro. Canonical source: https://chillicream.com/services/support/contact Talk to us Whether you are evaluating Nitro, planning a GraphQL project, training a team, or looking for product support, you will hear from the people behind the stack, not a first-line queue. - Straight to the ChilliCream team - Share your stack, constraints, and timeline - No sales runaround [contact@chillicream.com](mailto:contact@chillicream.com) [Join the community Slack](https://slack.chillicream.com/) --- # GraphQL Training for .NET Teams > Book GraphQL training for your .NET team, with Hot Chocolate, Fusion, schema design, performance, and client topics shaped for beginner or advanced engineers. Canonical source: https://chillicream.com/services/training GraphQL training for teams Our GraphQL, Hot Chocolate, and Fusion curriculum teaches in depth and works really well. It also isn't set in stone, so we shape every engagement to the team in the room. [Talk to a trainer](mailto:contact@chillicream.com?subject=Training) [Where is your team today?](#levels) Where is your team today? ## Pick the row that sounds like your standup. The curriculum is the same set of building blocks. The order, the depth, and the exercises change for the room. Level 1 ### Beginner team Heard of GraphQL. Maybe shipped a toy server. We start from REST instincts and rebuild them. By the end of week one your team can read a schema, write resolvers with confidence, and stop confusing fields with arguments. What we cover - Schema-first thinking and the type system - Queries, mutations, variables, and fragments - Hot Chocolate basics on ASP.NET Core - Connecting a Relay or Apollo client - Pagination, errors, and the everyday traps Level 2 ### Mixed team Half the team has shipped. Half the team is bluffing. The most common shape we see. We split sessions into shared foundations plus parallel tracks, so nobody is bored and nobody is lost. Everyone leaves on the same page. What we cover - Shared foundations to align vocabulary - Parallel tracks for newcomers and veterans - Pair exercises that mix the two groups - A real schema review on your codebase - Working sessions on current design questions Level 3 ### Advanced team Schemas in production. Now the corners get sharp. For teams already shipping GraphQL who want to go deeper. We focus on the parts that hurt at scale: schema design, performance, federation with Fusion, and operating Hot Chocolate in anger. What we cover - Schema design at scale and review patterns - Data loaders, batching, and query plans - Federation with Hot Chocolate Fusion - Observability and Nitro in production - Versioning and breaking-change workflows Two ways to run it ## Training to align, or a workshop to ship. Both engagements use the same curriculum and trainers. They differ in how much hands-on project work sits at the center of the engagement. ### Team training Flexible curriculum, shaped to your team Combine instruction, examples, and exercises across GraphQL, Hot Chocolate, Fusion, Nitro, and client development. We adjust the depth to the team in the room. What is in the box - Shared GraphQL vocabulary - Topics matched to current skill levels - Examples grounded in .NET - Time for team questions [Plan team training](mailto:contact@chillicream.com?subject=Corporate%20Training) Most popular ### Hands-on workshop Hands on, with a real project at the end Work through an agreed project using ASP.NET Core and Hot Chocolate, with optional Fusion or client topics. The emphasis is on applying the patterns, not copying a finished sample. What is in the box - Defined workshop problem - Hands-on schema and resolver work - Review and discussion as a group - Production design considerations - Optional work in your codebase [Plan a workshop](mailto:contact@chillicream.com?subject=Corporate%20Workshop) By the end of the training ## What your team will actually know. No certificate-printer outcomes. These are the things your team walks away able to do, wherever they started. ### Read a schema like a map Navigate a large GraphQL schema, recognize the common shapes, and explain why a type is modeled the way it is. ### Write resolvers without surprises Move from simple fields to data loaders and pagination with patterns that scale instead of snippets that bite later. ### Plan a client they can live with Use fragments, variables, and error handling to structure a Relay or Apollo client the next person on the team can maintain. ### Diagnose the slow query Open a trace, read the plan, find the N+1, and know which Hot Chocolate patterns to reach for before turning to hacks. ### Have an opinion on federation Know when to split a schema, when not to, and how Hot Chocolate Fusion fits with the platform you already run. ### Speak the same language Give backend, frontend, and platform engineers one shared vocabulary, so the next design review is faster and friendlier. Delivery format ## On site, remote, or a sensible hybrid. We have run training in all three formats. Pick the one that fits your calendar and your office, not the other way around. ### On site We come to you A trainer joins your team in a room with a whiteboard and proper coffee. Best when you want the focused energy of being out of inboxes for a week. Best for a single co-located team that can clear the calendar. ### Remote Live, distributed Live sessions over your call tool of choice, with shared notebooks, breakout rooms, and homework between days so timezones do not become a wall. Best for distributed teams or when travel does not make business sense. ### Hybrid Some in the room, some on the call Deliberate breakout design and exercises that work for the people in the room and the people on the call, with a good A/V setup so nobody is half-present. Best when part of the team can fly in and part cannot. ## And yes, have lots of fun. Training that nobody enjoys does not stick. We run sessions like the workshops we wish we had been to: hands on, slightly informal, no slide marathons, room for questions that start with “this is probably stupid but...” (it is not). - Plenty of breaks, by design - Pair and group exercises - Real schemas, not lorem ipsum - Questions welcome, including the basic ones - Optional work in your codebase What we will not do - No slide marathons - No certificate factory - No copy-paste exercises - No graded tests at the end of the week - No vendor pitch dressed up as training Common questions ## Before you book. How long does a typical engagement take? Typically a focused few days up to a full week. Short engagements suit a team that already ships GraphQL and wants depth on one topic. Longer engagements suit foundations plus a small project at the end. The exact shape is set per engagement once we know the team. What team size works best? A single engineering team can usually work as one cohort. For a larger group, we will discuss whether separate cohorts or tracks make the material easier to apply. Share the headcount and experience mix when you contact us. What should the team know before day one? For the beginner track, working knowledge of one server-side language (typically C# or TypeScript) and any web framework is enough. For the advanced track we expect existing GraphQL exposure, ideally a schema in production. There is no certification gate. How much does it cost? Pricing is on request, because the right answer depends on team size, format (on site, remote, or hybrid), duration, and whether we are bundling a workshop project. Send us a short note and we will come back with a concrete proposal. How far ahead do we need to book? Availability depends on the dates, delivery format, and curriculum. Send a few possible dates when you contact us. On-site engagements also need enough time to arrange travel. Can the curriculum cover our actual codebase? It can. We can discuss reviewing a schema before the sessions, shaping exercises around your domain, or using part of a workshop for a current design question. Any access and data-handling requirements are agreed during planning. ## Tell us about your team and we will shape the training around it. Send the team size, current GraphQL level, topics you want to cover, preferred format, and a few dates that work. We will reply with a concrete proposal, not another form to fill in. [Email a trainer](mailto:contact@chillicream.com?subject=Training) [See the two offers again](#offers) --- # Directives All the Way Down > GraphQL directives can now be applied to directive definitions themselves in Hot Chocolate 16.4, so you can finally deprecate a directive and attach metadata to your schema's own extension points. Canonical source: https://chillicream.com/blog/2026-08-03-directives-all-the-way-down ![](https://chillicream.com/images/blog/2026-08-03-directives-all-the-way-down/header.png) [![Glen's avatar](https://chillicream.com/_optimized/images/remote/5a65a00398c8fc0afb627eb1d2557dd0da8c797404be8aa6a5e3c3fc8c636689.png)Glen](https://chillicream.com/authors/glen)2026-08-034 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-08-03-directives-all-the-way-down&text=Directives+All+the+Way+Down) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-08-03-directives-all-the-way-down) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [graphql](https://chillicream.com/blog/tags/graphql) - [directives](https://chillicream.com/blog/tags/directives) - [deprecation](https://chillicream.com/blog/tags/deprecation) Directives are GraphQL's built-in extension mechanism. Every time you write `@deprecated`, `@skip`, `@include`, or a custom directive of your own, you attach a small piece of behavior or metadata to a specific part of your schema or query. Over the years you have been able to apply them almost everywhere: on fields, arguments, object types, enum values, input fields, scalars, and more. Almost everywhere. There was one conspicuous gap. You could never apply a directive to a _directive definition_ itself. That sounds like a technicality until you hit it, and the most common way to hit it is this: you have a custom directive you would like to retire, so you reach for `@deprecated`, only to find it is not allowed there. There was no way, in the schema itself, to signal that a directive was on its way out. That gap is now closed. As of Hot Chocolate 16.4, directives can be applied to directive definitions, and `@deprecated` is one of them. ## A long time coming This is not a Hot Chocolate invention. It is a GraphQL specification feature, and it took a while to get there. The idea goes back years, with earlier attempts like [#567](https://github.com/graphql/graphql-spec/issues/567) and [#907](https://github.com/graphql/graphql-spec/pull/907) exploring how directives on directives should work. It finally came together in [graphql-spec #1206](https://github.com/graphql/graphql-spec/pull/1206), which was accepted into the specification on June 4, 2026, after moving through the RFC process and landing alongside a reference implementation in graphql-js. Hot Chocolate 16.4 ships with full support. ## What it looks like Before a directive can be applied to a directive definition, it has to opt in by declaring the new `DIRECTIVE_DEFINITION` location. In schema-first SDL that is just another entry in the `on` list: GraphQL ``` directive @onDirectiveDefinition on DIRECTIVE_DEFINITION ``` In C#, you can declare that location the implementation-first way, with an attribute: C# ``` [DirectiveType(DirectiveLocation.DirectiveDefinition)] public class OnDirectiveDefinition { } ``` or the code-first way, on the descriptor: C# ``` public class OnDirectiveDefinitionType : DirectiveType { protected override void Configure(IDirectiveTypeDescriptor descriptor) { descriptor.Name("onDirectiveDefinition"); descriptor.Location(DirectiveLocation.DirectiveDefinition); } } ``` Once a directive targets that location, you can apply it to another directive definition. In SDL the applied directives go after the arguments and before the `on` keyword: GraphQL ``` directive @custom(name: String!) @onDirectiveDefinition on OBJECT ``` In code-first, call `Directive()` on the descriptor with the directive you declared, which keeps the reference type-safe: C# ``` public class CustomDirectiveType : DirectiveType { protected override void Configure(IDirectiveTypeDescriptor descriptor) { descriptor.Name("custom"); descriptor.Location(DirectiveLocation.Object); descriptor.Argument("name").Type>(); descriptor.Directive(); } } ``` ## Finally, deprecating a directive This is the one everybody was waiting for. `@deprecated` has always been able to mark fields, arguments, input fields, and enum values as obsolete. Now it can mark a directive definition too. Say you shipped a custom `@legacyAuth` directive and you are moving everyone over to a new `@authorize`. Until now you had no in-schema way to say "stop using this." You would drop a note in a changelog and hope people read it. Now the schema says it for you: GraphQL ``` directive @legacyAuth @deprecated(reason: "Use @authorize instead.") on FIELD_DEFINITION ``` In code-first, call `Deprecated(...)` on the descriptor: C# ``` public class LegacyAuthDirectiveType : DirectiveType { protected override void Configure(IDirectiveTypeDescriptor descriptor) { descriptor.Name("legacyAuth"); descriptor.Location(DirectiveLocation.FieldDefinition); descriptor.Deprecated("Use @authorize instead."); } } ``` Or, on an annotated directive class, reach for the attributes you already know. Both `[Obsolete(...)]` and `[GraphQLDeprecated(...)]` set the deprecation: C# ``` [Obsolete("Use @authorize instead.")] [DirectiveType(DirectiveLocation.FieldDefinition)] public class LegacyAuth { } ``` You can also deprecate a single argument instead of the whole directive, which is handy when you are evolving a directive's signature rather than replacing it outright: GraphQL ``` directive @custom( legacyArg: Int @deprecated(reason: "Use newArg instead.") newArg: String ) on OBJECT ``` Because this is real schema metadata, it shows up in introspection, exactly the way deprecated fields and enum values always have. `__Directive` now carries `isDeprecated` and `deprecationReason`, and `__Schema.directives` gained an `includeDeprecated` argument that hides deprecated directives by default: GraphQL ``` { __schema { directives(includeDeprecated: true) { name isDeprecated deprecationReason } } } ``` That means tooling gets it for free. IDEs, code generators, and Nitro can render a strikethrough or a warning on a deprecated directive the same way they already do for a deprecated field, so the deprecation actually reaches the people using it. ## Beyond deprecation Deprecation is the headline, but it is not the whole story. A directive definition is now just another annotatable schema element, and Hot Chocolate already uses that for more than `@deprecated`. `@requiresOptIn` can be applied to a directive definition to mark it as experimental. Consumers have to opt in to the named feature before they rely on the directive, so you can ship something new without committing to it right away: GraphQL ``` directive @experimentalTrace @requiresOptIn(feature: "experimentalTracing") on FIELD_DEFINITION ``` `@tag` applies to directive definitions too, letting you group and filter directives with the same labels you already apply to types and fields. And because this is an open extension point, your own directives are welcome too. Say you want each directive to point at the design doc that introduced it, the way `@specifiedBy` links a scalar to its specification: GraphQL ``` directive @designDoc(url: String!) on DIRECTIVE_DEFINITION directive @authorize @designDoc(url: "https://example.com/rfcs/authz") on FIELD_DEFINITION ``` You can also fold an annotation into an existing definition with `extend directive`, without touching the original declaration: GraphQL ``` extend directive @custom @deprecated(reason: "Use @modern instead.") ``` One rule is specific to this feature: a directive cannot be applied to its own definition. Hot Chocolate rejects that self-reference with a clear error. ## What will you build? We shipped the mechanism, and Hot Chocolate already puts it to work in more than one way: `@deprecated`, `@requiresOptIn`, and `@tag` all apply to directive definitions today. The interesting part is what you do next. Directives are an open-ended extension point, and now that they reach directives themselves, we are genuinely curious what patterns you will come up with. If you build something interesting on top of this, tell us. And if you just want to deprecate that one directive that has been haunting your schema, that is a perfectly good reason to upgrade too. For the full reference, see the [Directives on Directive Definitions](https://chillicream.com/docs/hotchocolate/defining-a-schema/directives#directives-on-directive-definitions) guide. ## You might also like [View all](https://chillicream.com/blog) --- # Fusion 16.5: The Gateway for Everyone > Built with C# and .NET, Fusion 16.5 is the only gateway supporting both federation standards, achieves 100% Apollo Federation compliance, and leads in real-world performance. Canonical source: https://chillicream.com/blog/2026-07-12-fusion-16-5 ![](https://chillicream.com/images/blog/2026-07-12-fusion-16-5/header.png) [![Michael Staib's avatar](https://chillicream.com/_optimized/images/remote/2bb0dd6e1b9347a1d3732300ea752f39f1ac4f1f76587433ca3a407245b31786.jpg)Michael Staib](https://chillicream.com/authors/michael-staib)2026-07-128 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-07-12-fusion-16-5&text=Fusion+16.5%3A+The+Gateway+for+Everyone) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-07-12-fusion-16-5) - [fusion](https://chillicream.com/blog/tags/fusion) - [graphql](https://chillicream.com/blog/tags/graphql) - [federation](https://chillicream.com/blog/tags/federation) - [apollo-federation](https://chillicream.com/blog/tags/apollo-federation) - [dotnet](https://chillicream.com/blog/tags/dotnet) - [aspnetcore](https://chillicream.com/blog/tags/aspnetcore) Fusion 16.5 is here. It delivers another major leap in performance and introduces full support for Apollo Federation. Fusion is now the only gateway that fully supports both GraphQL Federation (the Composite Schema Specification) and Apollo Federation. We did not bolt on a thin connector layer but integrated Apollo Federation into the core of Fusion. You can freely mix and match GraphQL Federation and Apollo Federation subgraphs in the same graph. Existing services can stay on Apollo Federation, while new subgraphs can adopt the newer open standard and its expanding set of capabilities. The new Apollo Federation connector achieves a perfect score in [The Guild's federation gateway audit](https://the-guild.dev/graphql/hive/blog/federation-gateway-audit), passing all 199 tests across all 46 suites. So far, only Hive Router and our own Fusion Gateway have achieved a perfect score. | Gateway | Compatibility | Test cases | Test suites | | ------------------------ | ------------- | ------------- | ----------- | | Hive Router | 100.00% | 199 / 199 | 46 / 46 | | **Hot Chocolate Fusion** | **100.00%** | **199 / 199** | **46 / 46** | | Hive Gateway | 98.99% | 197 / 199 | 45 / 46 | | Hive Gateway (Rust QP) | 98.49% | 196 / 199 | 44 / 46 | | Apollo Router | 97.49% | 194 / 199 | 43 / 46 | | Apollo Gateway | 96.98% | 193 / 199 | 42 / 46 | | Cosmo Router | 91.96% | 183 / 199 | 37 / 46 | | Grafbase Gateway | 90.45% | 180 / 199 | 39 / 46 | With Fusion, you get one gateway for every federation graph. It combines complete compatibility and market-leading performance with the full power of C# and ASP.NET Core. ## The only gateway for both federation standards The newer GraphQL Federation standard represents where we want federation to go. But we also know that many of you have already invested heavily in Apollo Federation. Fusion lets you protect that investment and adopt new capabilities one subgraph at a time without forcing a migration. There is no schema conversion or Fusion-specific integration required to compose an Apollo Federation subgraph. Your subgraph keeps its existing Federation 2 schema and exposes it through `_service` as it does today. You only need to tell Fusion where it can fetch the schema and where the gateway can reach the subgraph at runtime: JSON ``` { "name": "Products", "transports": { "http": { "url": "http://localhost:4001/graphql" } }, "extensions": { "chillicream": { "apolloFederationSupport": { "version": "2.0" } } } } ``` Compose the subgraph with Nitro: Bash ``` nitro fusion compose \ --source-schema-url http://localhost:4001/graphql \ --source-schema-settings-file ./products/schema-settings.json \ --archive gateway.far ``` The `apolloFederationSupport` setting tells Nitro to fetch the SDL by querying `_service { sdl }`. Fusion then translates directives such as `@key`, `@requires`, `@provides`, and `@interfaceObject` into its internal composition model. At runtime, Fusion continues to speak Apollo Federation to that subgraph through the `_entities` field. GraphQL Federation subgraphs, by contrast, use their native lookup fields. Both models can participate in the same graph and even the same query plan. Fusion's query planner treats them identically. ## The open future of federation runs through Fusion GraphQL Federation is an open specification developed by Apollo, ChilliCream, The Guild, and other contributors from across the GraphQL community. What began as the Composite Schema Specification is now the shared foundation for how distributed GraphQL systems compose, plan, and execute. Fusion implements that model today, and the latest version shows how much further federation can go. The new `@require` model goes far beyond basic field dependencies. A requirement can map nested source data into typed resolver arguments, construct input objects, work across interfaces, and describe precisely what a resolver needs at runtime. Subgraph authors get explicit contracts, while Fusion gets the information it needs to build better query plans. For example, a Shipping subgraph can calculate a delivery estimate using product data owned by other subgraphs. A field selection map declares both the required data and how Fusion should inject it into the resolver argument. GraphQL ``` # Shipping subgraph type Product { id: ID! deliveryEstimate( zip: String! dimensions: ProductDimensionInput! @require( field: """ { weight, length: dimensions.length, width: dimensions.width, height: dimensions.height } """ ) ): Int! } input ProductDimensionInput { weight: Float! length: Float! width: Float! height: Float! } ``` At the code level, this is much cleaner than receiving a representation and manually extracting untyped data. The resolver knows exactly which data it will receive and where it will appear. It also controls how that data maps onto its own types. In this example, the nested dimension fields are lifted into a strongly typed `ProductDimensionInput` record. C# ``` [ObjectType] public static partial class ProductNode { public static int GetDeliveryEstimate( [Parent] Product product, string zip, [Require( """ { weight, length: dimensions.length, width: dimensions.width, height: dimensions.height } """)] ProductDimensionInput dimensions) => ShippingCalculator.Estimate(zip, dimensions); } public sealed record ProductDimensionInput( float Weight, float Length, float Width, float Height); ``` Clients provide only the `zip` argument. Fusion fetches `weight` and the nested dimension fields from whichever subgraphs own them, builds the `ProductDimensionInput`, and passes it to the Shipping resolver. Because the dependency is part of the schema, Fusion can validate that every field exists, is reachable, and has a compatible type during composition. A missing dependency becomes a composition error instead of a runtime failure. The overhauled interface object model addresses long-standing requests from the community. A subgraph can add behavior to an interface without repeating that behavior across every implementing type. Concrete types can replace projected behavior explicitly, and Fusion can carry those relationships across source schemas and federation protocols. Imagine that the Catalog subgraph owns a `Media` interface implemented by books and movies: GraphQL ``` # Catalog subgraph type Query { mediaById(id: ID!): Media @lookup } interface Media { id: ID! title: String! } type Book implements Media @key(fields: "id") { id: ID! title: String! } type Movie implements Media @key(fields: "id") { id: ID! title: String! } ``` The Analytics subgraph can add `views` to every kind of media without knowing that `Book` or `Movie` exists: GraphQL ``` # Analytics subgraph type Query { mediaByKey(id: ID!): Media @lookup @internal } type Media @interfaceObject @key(fields: "id") { id: ID! views: Int! } ``` Composition projects `views` onto the `Media` interface and all of its implementations. When Catalog adds another media type, Analytics does not need to change. If books need their own implementation of `views`, Catalog can replace the projected default explicitly: GraphQL ``` type Book implements Media @key(fields: "id") { id: ID! title: String! views: Int! @implement } ``` `Book` now owns its implementation of `views`, while `Movie` and every other media type continue to use the default from Analytics. The `@implement` marker makes that intent explicit. If the field is redeclared without it, Fusion reports the conflict during composition instead of silently choosing an implementation. This is a leap forward for distributed GraphQL. Apollo Federation support means you can adopt that future at your own pace, while the services you already operate continue to run unchanged. Supporting both federation models was one challenge. Doing it without compromising performance was another. The answer goes back to one of the earliest and most important decisions we made for Fusion. ## We stayed with .NET, and it paid off Staying with C# and .NET was not the obvious bet in a gateway market moving to Rust and Go. It was ours. Fusion 16.5 shows why that decision was the right one for us. Fusion is built on ASP.NET Core, so it stands on the server platform Microsoft has spent years hardening and optimizing. Authentication, dependency injection, configuration, observability, HTTP connection management, and resilience for inter-service communication all use the same platform capabilities as the rest of your ASP.NET Core applications. We also take full advantage of the lower-level APIs the .NET team has introduced over time. Fusion uses request-scoped memory arenas and references data in existing representations instead of copying it. This gives us precise control over the hot path without giving up managed code or the .NET ecosystem. That architecture also works in your favor when you need to extend the gateway. Fusion is a .NET library, so you can extend it using ordinary C#. You can implement a subscription provider, add data masking, influence the query planner, or integrate your own authentication provider without learning a separate extension language or building around a complex hook system. ## The fastest gateway where it matters Choosing .NET did not mean sacrificing performance. On the contrary, Fusion has become the fastest gateway on the market, beating every competitor. Consider the benchmark that simulates real-world load with constant traffic and downstream latency. It runs a complex query across 50 concurrent virtual users (VUs), exercises lookups and requirements, and adds 4 ms of latency to every downstream call. Fusion 16.5 now tops this benchmark. | Gateway | Version | Technology | Median RPS | Best RPS | Worst RPS | CV% | | --------------------------- | ------------ | -------------------- | ---------- | -------- | --------- | ---- | | fusion | 16.5.0 | C# / .NET | 1,856 | 1,908 | 1,853 | 1.0% | | hive-router | v0.0.78 | Rust | 1,846 | 1,907 | 1,837 | 1.2% | | fusion | 16.4.0 | C# / .NET | 1,830 | 1,884 | 1,824 | 1.2% | | fusion | 16.0.0 | C# / .NET | 1,441 | 1,454 | 1,430 | 0.6% | | grafbase | 0.53.5 | Rust | 1,321 | 1,355 | 1,309 | 1.2% | | cosmo | 0.329.0 | Go | 1,160 | 1,207 | 1,152 | 1.7% | | hive-gateway-router-runtime | 2.10.2 | JavaScript + Rust | 544 | 568 | 542 | 1.6% | | apollo-router | v2.16.0 | Rust | 432 | 449 | 431 | 1.4% | | apollo-gateway | 2.14.2 | TypeScript / Node.js | 261 | 264 | 260 | 0.5% | | hive-gateway | 2.10.2 | TypeScript / Node.js | 259 | 267 | 257 | 1.5% | | feddi | 5ff8b6165878 | Java / JVM | 22 | 23 | 21 | 3.2% | The progression across the published Fusion results is clear. Median throughput increased from 1,441 requests per second with Fusion 16.0 to 1,830 with Fusion 16.4 and 1,856 with Fusion 16.5\. That is an improvement of nearly 29% since Fusion 16.0\. Performance remains a constant focus for us, and we continue to invest in it with every release. We measure three benchmark categories: - Constant with latency shows how a gateway performs in real-world scenarios where downstream services make database calls and perform other I/O-bound work. Every downstream call adds 4 ms of latency, and the test runs with 50 concurrent VUs. - Constant without latency is a lab experiment in which the downstream services add no latency. It shows the theoretical throughput of a gateway when downstream latency is not a factor. - Burst shows how gateways handle sudden traffic spikes. The benchmark starts with a constant base load of 50 VUs, spikes to 500 VUs, and then settles back to 50 VUs. The [gateway benchmark suite](https://github.com/ChilliCream/graphql-gateway-benchmarks) runs daily and is regularly updated to test new gateway versions. All benchmarks run on dedicated hardware to produce predictable, comparable results. Each gateway runs on a clean system. After every benchmark run, we reset the system to its original state and restart it before testing the next gateway. ## For every federation graph Fusion 16.5 changes what teams can expect from a federation gateway. It is the only gateway that brings GraphQL Federation and Apollo Federation together. It passes every case in The Guild's federation audit and leads in real-world performance. And it delivers all of that as an extensible ASP.NET Core application built with C# and .NET. You no longer have to choose between compatibility, performance, and extensibility. Keep the graph you have. Build the graph you want. Move one subgraph at a time. If you are moving from Apollo Federation, read [Coming from Apollo Federation](https://chillicream.com/docs/fusion/migration/coming-from-apollo-federation). If you are starting a new graph, follow the [Fusion getting started guide](https://chillicream.com/docs/fusion/getting-started). Then join us on [Slack](https://slack.chillicream.com/) and show us what you are building. ## You might also like [View all](https://chillicream.com/blog) --- # Introducing Federated Event Streams for Fusion 16.4 > Federated Event Streams add broker-backed, resumable GraphQL subscriptions to Fusion 16.4, with stateless gateway scaling and client-owned resume cursors. Canonical source: https://chillicream.com/blog/2026-07-06-federated-event-streams ![](https://chillicream.com/images/blog/2026-07-06-federated-event-streams/header.png) [![Michael Staib's avatar](https://chillicream.com/_optimized/images/remote/2bb0dd6e1b9347a1d3732300ea752f39f1ac4f1f76587433ca3a407245b31786.jpg)Michael Staib](https://chillicream.com/authors/michael-staib)2026-07-067 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-07-06-federated-event-streams&text=Introducing+Federated+Event+Streams+for+Fusion+16.4) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-07-06-federated-event-streams) - [fusion](https://chillicream.com/blog/tags/fusion) - [graphql](https://chillicream.com/blog/tags/graphql) - [federation](https://chillicream.com/blog/tags/federation) - [subscriptions](https://chillicream.com/blog/tags/subscriptions) - [event-streams](https://chillicream.com/blog/tags/event-streams) - [dotnet](https://chillicream.com/blog/tags/dotnet) Queries and mutations are the easy part of GraphQL. A client sends a request, the server returns a response, and the operation is done. You do not have to keep track of connection state, reconnects, transport differences, or delivery guarantees over time. Subscriptions are different. They are long-lived streams, and that means we need to think about what happens while the connection is open, what happens when it drops, and what a client can safely assume when it comes back. From the GraphQL spec's point of view, those delivery guarantees are not defined for you. Take an order management screen. A support agent is watching an order move from `placed`, to `paid`, to `packed`, to `shipped`. Each transition is pushed to the UI as it happens. If the agent's laptop switches networks or the browser reconnects after a short outage, the client needs to know whether it missed an update while it was offline. For some applications, missing a few events is acceptable. A live typing indicator, presence update, or fast-changing dashboard can simply continue with the newest value. For others, every event matters. In an order workflow, audit trail, payment process, or support inbox, the client needs to resume exactly where it left off, without gaps and without replaying events it has already processed. In a federated graph, these concerns become even more important. Events can originate from different subgraphs, and the gateway has to turn each event into the response shape the client asked for. At the same time, we still want the system to scale like the rest of our GraphQL architecture: gateway instances should stay stateless, subgraphs should scale independently, and reconnects should not depend on sticky sessions, in-memory subscription state, or transferring session data from one gateway replica to another. Today, we are introducing **Federated Event Streams** in Fusion 16.4: a new way to build broker-backed, resumable subscriptions across a federated graph without making the gateway stateful. A client subscribes once through the Fusion gateway, events come from your broker, and for each event the gateway resolves exactly the fields the client asked for across the federated graph. Resume state stays with the client, so a reconnect can land on any gateway replica without sticky sessions or gateway-owned subscription state. ### Broker-backed GraphQL subscriptions in Fusion Federated Event Streams starts with the GraphQL subscription the client already knows. The client subscribes through the Fusion gateway and selects the fields it wants back. GraphQL ``` subscription { onReviewCreated { review { id body } } } ``` Instead of forwarding that subscription to a subgraph, the gateway subscribes to a broker topic itself. That topic becomes the event stream for the subscription. Whenever the gateway receives an event, it uses the event as the starting point for the subscription query plan. It performs ordinary stateless GraphQL requests against the relevant subgraphs, builds the response the client asked for, sends it to the client, and waits for the next event. This is the important shift. The gateway does not need to keep a stateful subscription connection open to a subgraph, and subgraphs do not need to keep subscription state for the gateway. From a subgraph's point of view, the gateway only sends normal GraphQL query requests. There is no gateway-to-subgraph subscription lifecycle to recover, no subscription state to move between gateway replicas, and no need for participating subgraphs to hold long-lived connection state. ### Declaring the event stream On the subgraph that exposes the subscription field, you add the `@eventStream` directive. The `message` argument describes the shape of the broker message. It is a selection set over the return type, and it tells the gateway which fields are already present when an event arrives. GraphQL ``` # Reviews subgraph type Subscription { onReviewCreated: ReviewCreated! @eventStream(message: "review { id }") } type ReviewCreated { review: Review! } type Review @key(fields: "id") { id: ID! } ``` If you are using Hot Chocolate, the same stream can be declared with the `[EventStream]` attribute: C# ``` [SubscriptionType] public static partial class ReviewSubscriptions { [EventStream("review { id }")] public static ReviewCreated OnReviewCreated() => EventStream.Create(); } public record ReviewCreated(Review Review); ``` That is the whole contract. `review { id }` means the broker delivers a message shaped like `{ "review": { "id": "1" } }`. The gateway uses that key to resolve the `Review` entity and then continues with whatever the client selected. The message does not have to be only an entity key, though. Because `message` is a selection set over the return type, it can describe any fields that arrive with the event. Some fields can come directly from the broker message, while others can be entity links that the gateway resolves through the graph: GraphQL ``` onProductPriceChanged(productId: ID!): ProductPriceChangedEvent @eventStream(message: "{ oldPrice newPrice product { id } }") type ProductPriceChangedEvent { oldPrice: Float! newPrice: Float! product: Product! } ``` Here `oldPrice` and `newPrice` come straight from the broker, while `product` is resolved across your subgraphs from `{ product { id } }`. You stream exactly what the event is about, and let federation fill in the rest only when the client asks for it. ### Resuming without gateway state A long-lived subscription will be interrupted eventually. A phone goes to sleep, a network blips, or you roll out a new gateway version. The important question is what happens when the client reconnects. Can it continue from the last event it processed without asking the gateway to remember anything? Federated Event Streams supports this with an opaque cursor that lives with the client. On the subscription field, you annotate one argument with the `@eventCursor` directive. On the payload type, you annotate one field with `@eventCursor` as well. That field carries the position of the event within the stream. GraphQL ``` type Subscription { onReviewCreated(after: String @eventCursor): ReviewCreated! @eventStream(message: "review { id }") } type ReviewCreated { review: Review! cursor: String @eventCursor } type Review @key(fields: "id") { id: ID! } ``` The event cursor is inserted by the gateway, so there is no need to add it to the message format. If you are using Hot Chocolate, the same schema looks like this: C# ``` [SubscriptionType] public static partial class ReviewSubscriptions { [EventStream("review { id }")] public static ReviewCreated OnReviewCreated([EventCursor] string? after) => EventStream.Create(after); } public record ReviewCreated( Review Review, [property: EventCursor] string Cursor); ``` The client stores the latest cursor after it has processed the event. If the connection drops, the client opens the same subscription again and passes that cursor back as `after`. GraphQL ``` subscription { onReviewCreated(after: "Mw==") { review { id body } cursor } } ``` The gateway resumes the stream after that cursor. A first-time subscriber simply omits the argument. This is the part that matters for scale. The gateway stores no resume position. There is no per-subscriber cursor table, no position store, and no subscription state to move between gateway replicas. The resume position travels with the client, so a reconnect can land on any gateway instance behind your load balancer. The cursor itself is a black box. It is a base64 token whose meaning belongs to the broker. Clients never parse it. They only store it and send it back when they need to resume. If you have used cursor-based paging in GraphQL, the pattern should feel familiar. Because resume is modeled in the GraphQL schema rather than broker-specific client code, clients can write their resume logic once. You can change the broker behind a stream without changing how clients resume. If you want clients to handle cursors generically, expose a shared interface for resumable payloads: GraphQL ``` type Subscription { onReviewCreated(after: String @eventCursor): ReviewCreated! @eventStream(message: "review { id }") } type ReviewCreated implements Resumable { review: Review! cursor: String @eventCursor } interface Resumable { cursor: String } type Review @key(fields: "id") { id: ID! } ``` ### Pick the broker that fits your world Broker infrastructure stays out of your schema. The schema describes the shape of the event, while the gateway decides which broker implementation to use. Your services can keep publishing with their normal broker clients, and your GraphQL contract stays focused on the API. Fusion ships five broker integrations that you can plug in and configure: - **NATS** (core and JetStream) - **Apache Kafka** - **Azure Event Hubs** - **Amazon SQS** (with optional SNS fan-out) - **Redis** C# ``` builder .AddGraphQLGateway() .AddNatsEventStreamBroker(options => { options.Url = "nats://localhost:4222"; options.JetStream = new NatsJetStreamOptions { Stream = "reviews" }; }); ``` Want Kafka instead? Swap `AddNatsEventStreamBroker` for `AddKafkaEventStreamBroker`. The event shape in your schema stays the same, while the broker configuration lives in host DI. Connection strings, partitions, SASL/SSL, JetStream stream and consumer names all live in your application code. None of that plumbing leaks into the published API contract. If you use a broker we do not ship, or if you want to wrap an existing broker with your own authorization, filtering, or transformation logic, implement `IEventStreamBroker`: C# ``` public interface IEventStreamBroker : IAsyncDisposable { IAsyncEnumerable SubscribeAsync( ISubscriptionFieldContext context, string[] topics, string? cursor, CancellationToken cancellationToken); } ``` Fusion gives your broker the subscription field context, the topics to consume, and the optional resume cursor supplied by the client. Your broker returns `EventMessage` values with the raw JSON body and, when supported by your implementation, the next opaque cursor. ### Publishing stays in your application How does a message get into the stream? This is where your application code connects. Event-driven architecture belongs in your domain, and the gateway is simply one more consumer of those events. C# ``` await nats.PublishAsync( "onReviewCreated", JsonSerializer.SerializeToUtf8Bytes(new { review = new { id } }), cancellationToken: cancellationToken); ``` Whether you publish directly through your NATS client, use [Mocha](https://chillicream.com/docs/mocha/messaging-patterns), or hide the broker behind a small wrapper is up to you. Federated Event Streams takes the complex parts out of the subscription path and lets your application publish events from your domain without bleeding GraphQL execution details into the rest of your system. ### Try Fusion 16.4 Federated Event Streams is the main feature in Fusion 16.4, but it is not the only one. This release also continues our work on the GraphQL-Federation spec (aka Composite Schema spec). `@tag` can now be applied to directive definitions, directive definitions support deprecation and the `DIRECTIVE_DEFINITION` location in introspection, and Fusion adds opt-in feature support with `@requiresOptIn`. The full Federated Event Streams reference, including every broker and the `@eventStream` / `@eventCursor` SDL, lives in the [subscriptions docs](https://chillicream.com/docs/fusion/subscriptions). Give Fusion 16.4 a try, and tell us what works, what is missing, and where you want the feature to go next. Join us on [Slack](https://slack.chillicream.com/). ## You might also like [View all](https://chillicream.com/blog) --- # Agents, Federation, and a Community > Notes from GraphQLConf 2026 and Working Group Day at Meta: AI agents meeting GraphQL schemas, the Composite Schema federation standard, and a community building what comes next. Canonical source: https://chillicream.com/blog/2026-06-24-graphqlconf-2026 ![](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/header.jpg) [![Salome Ruckstuhl's avatar](https://chillicream.com/_optimized/images/remote/3fcbe13df9d90f1d5cb925ee6882b518af2e786a80703bfc0ad4fa00780da77e.jpg)Salome Ruckstuhl](https://chillicream.com/authors/salome-ruckstuhl)2026-06-246 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-06-24-graphqlconf-2026&text=Agents%2C+Federation%2C+and+a+Community) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-06-24-graphqlconf-2026) - [graphql](https://chillicream.com/blog/tags/graphql) - [graphqlconf](https://chillicream.com/blog/tags/graphqlconf) - [community](https://chillicream.com/blog/tags/community) - [ai](https://chillicream.com/blog/tags/ai) - [agents](https://chillicream.com/blog/tags/agents) - [federation](https://chillicream.com/blog/tags/federation) - [fusion](https://chillicream.com/blog/tags/fusion) Every year, GraphQLConf brings the community together and reminds us how much can change in a year. This time we made the trip to the Bay Area for GraphQLConf 2026, stayed for the Working Group Day at Meta, and came home with a lot of notes, a lot of ideas, and a camera roll that was mostly redwoods. A few themes kept coming up in the talks, hallway conversations, and working sessions. AI agents are becoming a real part of the GraphQL world, both as consumers of APIs and as tools that help us build them. Federation now has a shared, vendor-neutral standard. And underneath both of those things, the community is putting more structure in place for what comes next. Here are the parts that stood out to us. ## When agents meet your schema Pascal opened one of our favorite threads of the conference with a simple question: what if an agent could pick up any GraphQL API and use it reliably, without a human wiring everything together first? GraphQL is, on paper, an ideal foundation for agents. The schema describes exactly what exists, the introspection gives an agent a way to discover capabilities, queries let it retrieve information, and mutations let it take action. Yet in the real world, schemas are large, and contexts are tiny. Semantic introspection is a way to work around this problem. Instead of handing an agent an entire schema and hoping it figures things out, semantic introspection gives it a more reliable way to explore a graph, understand the meaning behind types and fields, and pull in only the context it needs. The goal is simple: any agent should be able to work with any GraphQL endpoint without a pile of custom glue code. If you want the deeper version, we wrote about it earlier this year in [Semantic Introspection](https://chillicream.com/blog/2026-04-22-semantic-introspection). ## Closing the loop for coding agents Where Pascal looked at agents using your API, Michael looked at agents changing it. Coding agents now implement features, refactor systems, and ship changes at a pace that would have sounded made up twelve months ago. But an agent is only as good as its feedback loop. If you point an agent at your product API and ask it to add a review system, it might produce something that looks reasonable in isolation. And without knowing what clients actually use, it might happily reshape your types, move fields, and break consumers you had forgotten were out there. GraphQL has a quiet advantage here: every client operation declares exactly which fields and types it needs. That gives you field-level usage data. Give that information to a coding agent, and it is no longer guessing. It can understand what is actually used, make safer schema changes, and avoid breaking existing consumers. That combination of GraphQL's visibility and capable coding agents creates a feedback loop we did not really have before. It means less time reviewing AI-generated code that (just) almost works, and more time working with changes that are much closer to correct the first time. You can try out our prototyping skills with: Bash ``` dnx skills add ChilliCream/agent-skills ``` See the [Skills documentation](https://chillicream.com/docs/skills) for installation, authoring, and command reference. ## The state of GraphQL federation The community has spent a lot of time standardizing how distributed GraphQL systems should work, with GraphQL acting as the gateway. The result is the Composite Schema specification, now being developed under the GraphQL Foundation. The important detail is where the spec stands today: it is still Stage 0, Preliminary. In other words, it is an active proposal, not a finished standard yet, and parts of it can still change before it reaches Draft. Even so, this is already a big step. Instead of every vendor shipping its own version of federation, Composite Schema gives the ecosystem a shared, vendor-neutral direction for composition, validation, and distributed execution. Fusion is our implementation of that direction, and it is already compatible with the Composite Schema spec as it evolves. If you want to try it today, start with the [Fusion getting started guide](https://chillicream.com/docs/fusion/getting-started). ## A community shaping what comes next GraphQL has always been a community-driven project, and the closing keynote was a good reminder of how much of the ecosystem exists because people kept showing up and doing the work. The GraphQL GAP proposal is part of that next phase. It is meant to create new ways for people across the community to collaborate and contribute. Together with updates from the Working Groups, it made one thing very clear: GraphQL's next chapter will not be written by one company or one team. ## Behind the scenes: Working Group Day at Meta One of our highlights was the community day after the conference. This year, we spent a full day together on Meta's campus, split across different Working Group tracks. A big part of the day focused on semantics, taking semantic introspection and turning it into concrete next steps. Salome spent the day working on the community side: shaping a new Community Hub to give the ecosystem a clearer home, and helping improve the Ambassador program to recognize and support the people bringing GraphQL into their companies, communities, and local meetups. The best part of the day was the side discussions. A casual conversation turned into Jordan shipping a new [Relay.js feature](https://github.com/facebook/relay/pull/5295), and Benjie could move several Golden Path initiatives forward, because so many maintainers were in one room. Community Day also turned out to be one of the most productive GraphQL Working Group sessions we've had. It's amazing what happens when you lock the TSC in the same room: suddenly discussions start, opinions align, and quorum is reached! ## When we were not talking GraphQL Outside the conference schedule, we managed to see a bit of the Bay Area too. We hiked through Big Basin Redwoods State Park, saw the Mother of the Forest, and met a few banana slugs moving through the forest at their own pace. We also drove across the Golden Gate Bridge, spent some time wandering through malls, and drank enough coffee to keep the whole trip moving. More than anything, it was a reminder that one of the best parts of any conference is getting to experience a new place with good people. ## Wrap up What stayed with us after GraphQLConf was not one single talk or announcement, but the feeling that GraphQL is entering a new phase. Agents are starting to change how APIs are used and built. Federation now has a shared, vendor-neutral standard. And just as importantly, the community is putting real structure behind the work through Working Groups, GAP, the Community Hub, and the Ambassador program all pointing in the same direction. That is what made the week feel so energizing. GraphQL does not feel like something being handed down by one company or one team. It feels like something a lot of people are building together. If you want to follow along, watch for the GraphQLConf recordings, read our deep dive on [Semantic Introspection](https://chillicream.com/blog/2026-04-22-semantic-introspection) and if you want to try GraphQL Federation yourself, give [Fusion](https://chillicream.com/docs/fusion/getting-started) a go! And if you want to talk GraphQL with people who think about it a little too much, our [Slack](https://slack.chillicream.com/) is always open. See you at the next one. ## A few snapshots from the trip - [![Photo from the GraphQLConf 2026 trip to the Bay Area](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf1.jpg)](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf1.jpg) - [![Photo from the GraphQLConf 2026 trip to the Bay Area](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf2.jpg)](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf2.jpg) - [![Photo from the GraphQLConf 2026 trip to the Bay Area](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf3.jpg)](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf3.jpg) - [![Photo from the GraphQLConf 2026 trip to the Bay Area](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf4.jpg)](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf4.jpg) - [![Photo from the GraphQLConf 2026 trip to the Bay Area](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf5.jpg)](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf5.jpg) - [![Photo from the GraphQLConf 2026 trip to the Bay Area](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf6.jpg)](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf6.jpg) - [![Photo from the GraphQLConf 2026 trip to the Bay Area](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf7.jpg)](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf7.jpg) - [![Photo from the GraphQLConf 2026 trip to the Bay Area](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf8.jpg)](https://chillicream.com/images/blog/2026-06-24-graphqlconf-2026/sf8.jpg) ## You might also like [View all](https://chillicream.com/blog) --- # Open Your GraphQL API for the REST > The new OpenAPI adapter for Hot Chocolate and Fusion turns GraphQL operations into REST endpoints with shared auth, telemetry, and Swagger docs. No second API. Canonical source: https://chillicream.com/blog/2026-06-11-open-your-graphql-api-for-the-rest ![](https://chillicream.com/images/blog/2026-06-11-open-your-graphql-api-for-the-rest/header.png) [![Tobias Tengler's avatar](https://chillicream.com/_optimized/images/remote/301b320e0f0f7a6e613d74ab800c49d42280e6a0d046325f5d65d7112ed71084.jpg)Tobias Tengler](https://chillicream.com/authors/tobias-tengler)2026-06-115 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-06-11-open-your-graphql-api-for-the-rest&text=Open+Your+GraphQL+API+for+the+REST) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-06-11-open-your-graphql-api-for-the-rest) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [fusion](https://chillicream.com/blog/tags/fusion) - [nitro](https://chillicream.com/blog/tags/nitro) - [openapi](https://chillicream.com/blog/tags/openapi) - [rest](https://chillicream.com/blog/tags/rest) - [api](https://chillicream.com/blog/tags/api) Every GraphQL project eventually meets a consumer that doesn't speak GraphQL. A partner platform, a legacy system, or a tool outside of your control asks your team for a plain REST endpoint. Until now, that meant spinning up a one-off HTTP endpoint somewhere in your stack that - falls outside your existing GraphQL telemetry and field usage tracking, - potentially duplicates your authentication and authorization setup, - duplicates type definitions that diverge over time, and - in the case of Fusion, falls outside the composition model: each subgraph is built to contribute its slice of the overall schema, so no single subgraph is positioned to serve an endpoint that aggregates data across the others. Sooner or later, that endpoint comes back to haunt you. Its traffic never shows up in your schema usage insights, so the next breaking change is decided on incomplete data. Its authorization is a copy that silently drifts from the original. Its DTOs mirror your GraphQL types until they don't. And the subgraph hosting the aggregated endpoint ends up hand-rolling the cross-service composition your gateway already does. And for what? You already have a well-defined GraphQL schema. A few one-off endpoints shouldn't require a second API technology. ## Meet the new OpenAPI Adapter The adapter ships with Hot Chocolate 16 and Fusion, and the idea behind it is simple: you define HTTP endpoints by authoring GraphQL documents that invoke your graph and select exactly the fields the REST API needs. In essence, your REST endpoints become just another client of your existing graph. Here is what that looks like in practice: GraphQL ``` query GetProductById($productId: ID!) @http(method: GET, route: "/api/products/{productId}") { productById(id: $productId) { id name price deliveryEstimate } } ``` The `@http` directive assigns the operation an HTTP method and a route. A request to `GET /api/products/42` executes the operation against your schema, passing `42` as the `$productId` variable, and returns the root field's data as plain JSON without the usual GraphQL response envelope: JSON ``` { "id": "42", "name": "Mountain Bike", "price": 899.99, "deliveryEstimate": "2026-06-13" } ``` If your GraphQL server is a Fusion gateway, this gets even better: `name` and `price` might come from a `products` subgraph, while `deliveryEstimate` is resolved through a `shipping` subgraph. The REST caller receives one flat resource, and the gateway does the cross-subgraph composition it was built for. No subgraph has to step outside its role to make this endpoint possible. Notice what is _not_ there: no controller, no DTO, no auth code, no serializer setup. The schema already provides the types, validation, and resolvers this endpoint needs. The request runs through the same execution pipeline as any GraphQL request, so your existing authorization rules are enforced, and the traffic shows up in your telemetry and field usage tracking like that of any other client. Mutations are just as simple. The `@body` directive maps the HTTP request body onto a variable, and route parameters can reach into that variable with the `key:$variable.path` syntax. That comes in handy for sub-resource endpoints, where the URL carries the parent ID and the body carries the rest: GraphQL ``` mutation CreateProductReview($review: CreateProductReviewInput! @body) @http(method: POST, route: "/api/products/{productId:$review.productId}/reviews") { createProductReview(input: $review) { id rating text } } ``` A `POST /api/products/42/reviews` writes `42` into `$review.productId` and fills the remaining input object fields from the JSON body. While fragments are generally [not meant for re-use](https://youtube.com/watch?v=gMCh8jRVMiQ) in client development, a REST API is different: a `Product` should have the same shape no matter which endpoint returns it. So the adapter lets you define a fragment in its own document and spread it across endpoint definitions, where it acts as a shared model: GraphQL ``` fragment Product on Product { id name price deliveryEstimate } ``` ## Wiring it up Install the `HotChocolate.Adapters.OpenApi` NuGet package into your GraphQL server project, or `HotChocolate.Fusion.Adapters.OpenApi` if your server is a Fusion gateway: Bash ``` dotnet add package HotChocolate.Adapters.OpenApi ``` Then extend your GraphQL server and endpoint configuration: Diff ``` var builder = WebApplication.CreateBuilder(args); builder.Services.AddNitro().AddHotChocolate(); builder.Services + .AddOpenApi(options => + { + options.AddGraphQLTransformer(); + }); builder .AddGraphQL() + .AddOpenApi() .AddQueryType(); var app = builder.Build(); app.UseRouting(); + app.MapOpenApi(); + app.MapOpenApiEndpoints(); app.MapGraphQL(); app.Run(); ``` Two of these additions do the heavy lifting: `AddOpenApi()` on the GraphQL server loads the endpoint definitions from a registered `IOpenApiDefinitionStorage`, and `MapOpenApiEndpoints()` exposes the resulting HTTP endpoints. If you're already using Nitro, that's all it takes: your server now loads its OpenAPI GraphQL documents from Nitro and listens for updates. The other two, `AddOpenApi()` on the service collection and `MapOpenApi()`, are optional. They enable the `Microsoft.AspNetCore.OpenApi` integration, which describes your generated endpoints in a standard OpenAPI document served at `/openapi/v1.json`. From there, interactive documentation is one package away. Install `Swashbuckle.AspNetCore.SwaggerUI` and point it at the document: C# ``` app.UseSwaggerUI(options => { options.SwaggerEndpoint("/openapi/v1.json", "v1"); }); ``` This renders the Swagger UI explorer at `/swagger/index.html`, where consumers can discover and try out your REST endpoints. ## Publishing your first endpoint Endpoint definitions are plain GraphQL files, and Nitro treats them like any other deployable artifact. They are organized into OpenAPI collections, each containing its own individually versioned set of endpoints and models. An API can serve any number of collections, so each team or department can contribute endpoints to the GraphQL server independently, on its own release cadence. Create a collection once with `nitro openapi create`, then upload your documents as a tagged version and publish that tag to a stage: Bash ``` nitro openapi upload \ --openapi-collection-id "" \ --tag "v1" \ --pattern "./openapi/**/*.graphql" nitro openapi publish \ --openapi-collection-id "" \ --tag "v1" \ --stage "dev" ``` Your running server picks up the published version and starts serving the new endpoints in place, without a redeploy. The endpoint also stays protected after publishing: if your schema evolves in a way that would make the endpoint document no longer executable, schema validation catches it. From there, the endpoint participates in your graph like any other client: its field selections feed your schema insights and field usage tracking, its traffic shows up in your telemetry, and schema changes that would break it are surfaced before they go live. There is also a dedicated dashboard that shows all of your adapter endpoints and their telemetry at a glance: ![Nitro dashboard listing the REST endpoints generated by the OpenAPI adapter together with their telemetry](https://chillicream.com/images/blog/2026-06-11-open-your-graphql-api-for-the-rest/nitro-endpoints-dashboard.png) ## Wrap up So the next time someone asks you for a REST endpoint, ask yourself: can my graph already provide this? If it can, don't go off and build a separate API. Plug in the OpenAPI adapter, author an endpoint document, and let your graph do the rest. If you want to go deeper, check out the [OpenAPI Adapter guide](https://chillicream.com/docs/hotchocolate/adapters/openapi). ## You might also like [View all](https://chillicream.com/blog) --- # Newsletter May 2026 > Hot Chocolate 16, Fusion 16, MCP, OpenAPI, Semantic Introspection, skillz, and more. Read the newsletter to learn about all the things we shipped in May and what comes next. Canonical source: https://chillicream.com/blog/2026-06-06-newsletter-may-2026 ![](https://chillicream.com/images/blog/2026-06-06-newsletter-may-2026/header.png) [![Pascal Senn's avatar](https://chillicream.com/_optimized/images/remote/3ebc6212aa4091005799d46f902a92e2eb7e77cd966797ecb5f4eaeab3bb26fe.png)Pascal Senn](https://chillicream.com/authors/pascal-senn)2026-06-065 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-06-06-newsletter-may-2026&text=Newsletter+May+2026) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-06-06-newsletter-may-2026) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [fusion](https://chillicream.com/blog/tags/fusion) - [graphql](https://chillicream.com/blog/tags/graphql) - [dotnet](https://chillicream.com/blog/tags/dotnet) - [ai](https://chillicream.com/blog/tags/ai) - [mcp](https://chillicream.com/blog/tags/mcp) - [openapi](https://chillicream.com/blog/tags/openapi) - [semantic-introspection](https://chillicream.com/blog/tags/semantic-introspection) - [release](https://chillicream.com/blog/tags/release) Dear ChilliCream Community, It has been more than a year since our last major platform cycle, and May was worth the wait. Hot Chocolate 16 and Fusion 16 are both out, we added new adapters for MCP and OpenAPI, we pushed Semantic Introspection forward for AI-driven schema discovery, we released `skillz` on NuGet to help you bring your conventions to your AI agents, and many of us got to see each other in person again at GraphQLConf 2026\. It was one of our biggest months yet, and it was shaped by the people reading this. Here is what we shipped and what comes next. ## Fusion 16 GraphQL gateway performance is often framed as a Rust or Go story. With Fusion 16, it is also a .NET story. While other vendors rewrote their gateways in Rust and Go to chase throughput, we stayed on .NET. Fusion 16 now ranks #2 in our federation benchmarks, second only to the Hive Router. It outperforms two Rust-based routers and a Go router, and when subgraphs carry realistic IO latency, the remaining gap nearly disappears. That performance comes from a brand-new execution engine. Fusion 16 is no longer built as an extension on top of Hot Chocolate. It now has its own architecture with a memory model inspired by Rust-style arena allocation: everything is managed as bytes, results are referenced instead of copied, and each request rents and returns fixed-size memory chunks. The result is a fast gateway without giving up the .NET platform. Your gateway remains an ASP.NET Core application running on .NET 8, 9, and 10\. Authentication, configuration, resilience, and observability stay in your hands, and you automatically inherit every Kestrel and runtime improvement Microsoft ships. Read the full post: [What's new in Fusion 16](https://chillicream.com/blog/2026-05-15-fusion-16). ## Hot Chocolate 16 Hot Chocolate 16 is our first major GraphQL server release in over a year, and it touches some of the deepest parts of the server. We reworked the type system, tightened scalar contracts, improved batching, adopted new GraphQL spec proposals, and made the defaults safer. Read the full post: [What's new for Hot Chocolate 16](https://chillicream.com/blog/2026-05-11-hot-chocolate-16). ## OpenAPI adapter Every GraphQL project eventually meets a consumer that needs REST: a partner integration, a legacy system, or a tool that cannot speak GraphQL. The new OpenAPI adapter lets you expose selected parts of your graph as REST endpoints without building and maintaining a second API. Read the full post: [Open Your GraphQL API for the REST](https://chillicream.com/blog/2026-06-11-open-your-graphql-api-for-the-rest). ## MCP adapter Agents are becoming API consumers, and with v16 you can give them an MCP server built directly on top of your data graph. Tools are authored as GraphQL operations, so they reuse the schema, validation, authorization, and execution pipeline you already have. But that is not all. The new MCP adapter also supports the MCP Apps standard, so you can colocate UI with your tools and return richer, agentic experiences directly from your GraphQL server or gateway. Read the full post: [From GraphQL to MCP in Two Lines](https://chillicream.com/blog/2026-05-28-mcp-hotchocolate-fusion). ## skillz `skillz` is a .NET CLI for installing, updating, and authoring Agent Skills. You package your team's conventions once as a skill, and any compatible agent loads it when a matching task comes up, so you stop re-explaining the same context every session. It runs one-shot with `dnx`, the way `npx` runs a package from npm. Alongside the CLI, we are publishing our first skill for the platform: `graphql-schema-design` for schema design and review. More are on the way, including `graphql-backend` for Hot Chocolate v16 backend patterns and `dataloader` for Green Donut DataLoaders. Bash ``` dnx skillz add ChilliCream/agent-skills --skill graphql-schema-design ``` Read the announcement: [Introducing skillz](https://chillicream.com/blog/2026-06-05-introducing-skillz). The CLI is now named `skills`; use the current [Skills documentation](https://chillicream.com/docs/skills) for installation and commands. ## Semantic Introspection Classic GraphQL introspection tells a client everything about a schema. That works well for developer tools, but it is too much context for agents working against large APIs. Semantic Introspection adds a search layer to introspection, so an agent can find the schema members relevant to a task and then fetch the exact definitions it needs. Hot Chocolate 16 and Fusion 16 include this through `__search` and `__definitions`. By default, the schema is indexed with BM25, so discovery can scale from small schemas to large enterprise graphs without sending the whole schema to the model on every turn. GraphQL keeps its precision for data fetching, while discovery becomes practical for AI workflows. With Nitro, Semantic Introspection can go beyond BM25 by adding embeddings to your schema, giving agents true semantic search across your API. That can make agent interaction dramatically cheaper than loading full API descriptions into context, while keeping GraphQL's precision intact. For the first time, you can really talk to your data. Read the full post: [Semantic Introspection](https://chillicream.com/blog/2026-04-22-semantic-introspection). ## From the community None of this lands without you. Two major releases in a single cycle meant a long preview period, and the people who ran those previews against real workloads, filed the issues that caught the rough edges, sent pull requests, and argued schema design with us in the open are the reason v16 feels solid on day one. Your feedback shaped real decisions in the type system, the scalar contracts, and the new Fusion engine. Thank you for that. It was also great to see so many of you in person at GraphQLConf 2026\. Putting faces to GitHub handles and Slack avatars was the best part. Talking through federation, schema evolution, and GraphQL for agents over nitro cold brew is the part no release notes can capture. We would love to hear what you are building. If you have shipped something with Hot Chocolate, Fusion, or Nitro that you are proud of, tell us about it. Come share it, ask questions, and join the conversation with the rest of the community on [Slack](https://slack.chillicream.com/). ## Thank you May was a release month, but the work continues. We will keep publishing more Agent Skills, more v16 documentation, and more guidance for building production GraphQL systems on .NET. To everyone who helped get this over the line: thank you. We are glad to build this alongside you. Warm regards, The ChilliCream Team ## You might also like [View all](https://chillicream.com/blog) --- # Introducing skillz: the .NET CLI for Agent Skills > skillz is a .NET CLI for installing, updating, and authoring Agent Skills, with dnx support for a one-shot workflow the way npx skills works in JavaScript. Canonical source: https://chillicream.com/blog/2026-06-05-introducing-skillz ![](https://chillicream.com/images/blog/2026-06-05-introducing-skillz/header.png) [![Pascal Senn's avatar](https://chillicream.com/_optimized/images/remote/3ebc6212aa4091005799d46f902a92e2eb7e77cd966797ecb5f4eaeab3bb26fe.png)Pascal Senn](https://chillicream.com/authors/pascal-senn)2026-06-055 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-06-05-introducing-skillz&text=Introducing+skillz%3A+the+.NET+CLI+for+Agent+Skills) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-06-05-introducing-skillz) - [dotnet](https://chillicream.com/blog/tags/dotnet) - [release](https://chillicream.com/blog/tags/release) - [products](https://chillicream.com/blog/tags/products) - [ai](https://chillicream.com/blog/tags/ai) Note `skillz` was the product's launch name. The current package, command, and documentation use `skills`; see the [Skills documentation](https://chillicream.com/docs/skills). Over the past year, everyone who has worked with coding agents has probably had their _wow_ moment. Mine came when I pasted an error message (`unterminated string`) from an HTTP response into Codex. We knew the issue had something to do with the parser, but had no idea how it was even possible. Yet five minutes later, Codex pointed me to this code in the HTTP middleware: C# ``` const int bufferSize = 4096; var buffer = new byte[bufferSize]; while (await stream.ReadAsync(buffer) == bufferSize) { // process buffer } // handle remaining bytes in buffer ``` Do you see it? The code reads from the stream in a loop until it gets back fewer bytes than the buffer size - that's the signal that the stream has ended. But, as so often in software engineering, the devil is in the details. The documentation for `ReadAsync` says: ``` //Returns: // A task that represents the asynchronous read operation. The value of its ValueTask property // contains the total number of bytes read into the destination. The result value can be less than // the number of bytes allocated in destination if that many bytes are not currently available, or // it can be 0 (zero) if the end of the memory stream has been reached. ``` Which makes sense! When a client delivers bytes too slowly to fill a whole buffer, `ReadAsync` can return less than the buffer size even if there are more bytes to come. And of course this _never_ happens in a test case - but out in the wild, with real clients and real network conditions, Murphy's law is lurking at every turn. That was my _wow_ moment. It probably saved us a day of debugging and a lot of head scratching. Since then, it's been a bumpy ride. Working with agents can be amazing, but it can also be incredibly frustrating - especially when you spend time explaining exactly what you want, the agent finally gets it, the task wraps up, and then it forgets everything. Next session, you start from scratch. Anthropic's answer to this is [Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview): a way to package your workflows, conventions, and best practices into a format agents can understand and load on demand. Write a skill once, and any compatible agent can pick it up when a matching task comes up. No more re-explaining the same context every session. The concept proved so useful that other coding agents quickly followed suit. The catch: every agent looks for skills in its own place. Claude uses `.claude`, Codex uses `.agents`, Windsurf uses `.windsurf`, and so on. You end up with the same skill files scattered across multiple directories, kept in sync by hand. And since there's a new _"best model yet"_ every other week - promptly followed by everyone switching tools out of FOMO - skills go stale fast and become a genuine pain to maintain. That's where `dnx skillz` comes in. It's a CLI for installing, updating, and authoring Agent Skills. You manage skills like any other project dependency, and `skillz` handles putting them where each agent expects to find them. It runs one-shot via `dnx` \- the same way `npx` runs packages from npm - so there's nothing to install globally. It also uses symlinks, so you maintain one canonical version of each skill and every agent picks it up from there. [Install .NET 10 to use dnx.](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) `dnx skillz` is heavily inspired by Vercel's `npx skills`, which does the same for JavaScript. They even have a [registry of skills](https://www.skills.sh/) that anyone can publish to. We wanted to bring that same experience to .NET (just without the phoning-home telemetry 🤫), so we built `skillz` as a NuGet package. You can check out [the source code here](https://github.com/ChilliCream/skillz). There's now [an official standard for skills](https://agentskills.io/) and more agents are adopting it. But `dnx skillz` goes beyond just copying files into the right directory - it also supports installing skills from GitHub, GitLab, local directories, and more. Private repositories work too, as long as your git credentials can reach them. Bash ``` dnx skillz add ChilliCream/agent-skills --skill graphql-schema-design ``` That command pulls the `graphql-schema-design` skill from the `ChilliCream/agent-skills` repository on GitHub, while `skillz` itself is a NuGet package. If you are working with Aspire you can try out their skills too: Bash ``` dnx skillz add microsoft/aspire-skills ``` or add the .NET-specific ones: Bash ``` dnx skillz add dotnet/skills ``` ## Installing There are two ways to run `skillz`: Bash ``` # one-shot, no install (needs the .NET 10 SDK) dnx skillz add # persistent tool on your PATH (.NET SDK 8.0+) dotnet tool install -g skillz skillz add ``` `dnx` ships with the .NET 10 SDK and runs a tool straight from NuGet, the way `npx` runs a package from npm. The first run downloads `skillz` into the NuGet cache, and later runs reuse it. No global install and no PATH entry to manage. ## What skillz does `skillz add` installs skills from a source for the agents on your machine. A source can be a GitHub `owner/repo`, a full git URL, a GitLab project, or a local directory. Bash ``` dnx skillz add ChilliCream/agent-skills --skill graphql-schema-design dnx skillz list dnx skillz update dnx skillz remove graphql-schema-design ``` By default, installs are project-scoped, recorded in `skills-lock.json` in your working directory. The whole repository shares one set of skills, so nobody has to install and configure the same skills by hand. For personal skills you want everywhere, add `--global`: Bash ``` dnx skillz add ChilliCream/agent-skills --skill graphql-schema-design --global ``` Skills are symlinked from one canonical location, so a single update reaches every agent. If an agent or sandbox can't follow symlinks, use `--copy`. Common flags: Bash ``` dnx skillz add --agent claude-code # target one agent (repeatable) dnx skillz add --skill # pick a single skill from the source dnx skillz add --all # install everything, no prompts dnx skillz add --copy # copy files instead of symlinking dnx skillz list --json # machine-readable output ``` ## Authoring your own skills To package your team's conventions as a skill, start with: Bash ``` dnx skillz init my-skill ``` That scaffolds a valid `SKILL.md` with the required frontmatter. From there, write the instructions, add references if you need them, and commit the folder. In ChilliCream we have an internal repository on GitHub with skills for our teams to use, and we also publish some of those skills publicly in the `ChilliCream/agent-skills` repository. ## graphql-schema-design The first ChilliCream skill we're shipping is `graphql-schema-design`. Schema mistakes are cheap to make and expensive to fix once clients depend on them. It's easy to write a GraphQL schema that mirrors your database tables, treats every mutation as a generic update, and returns unbounded arrays instead of connections. That schema looks fine in a diff, but it's harder to evolve and harder for clients to use. `graphql-schema-design` turns the agent into a schema reviewer rather than a code generator. It helps you design new schemas, evolve existing ones, and review schema diffs. It brings the best practices from the GraphQL ecosystem - the connection specification, mutation payload pattern, error conventions, naming rules, and more - directly to your fingertips via `/graphql-schema-design`. It focuses on the decisions that are easy to get wrong and costly to walk back: - naming that holds up as the schema grows - connections and pagination for lists that can get large - mutation payloads and domain errors - schema evolution, deprecations, and avoiding breaking changes - client-first query and mutation shape - Relay and platform conventions That skill is available now, and more are on the way. We are building skills for backend patterns in Hot Chocolate v16, DataLoader best practices, and more. If you have a skill you want to see built, let us know on [Slack](https://slack.chillicream.com/). `skillz` is on [NuGet](https://www.nuget.org/packages/skillz), and the source is on [GitHub](https://github.com/ChilliCream/skillz). If you're building skills for your own stack, we'd love to see what you build. Come find us on [Slack](https://slack.chillicream.com/). ## You might also like [View all](https://chillicream.com/blog) --- # From GraphQL to MCP in Two Lines > Hot Chocolate and Fusion now ship an MCP adapter. Add two lines, author tools and prompts on disk, publish them with Nitro, and connect any MCP host to your GraphQL API. Canonical source: https://chillicream.com/blog/2026-05-28-mcp-hotchocolate-fusion ![](https://chillicream.com/images/blog/2026-05-28-mcp-hotchocolate-fusion/header.png) [![Glen's avatar](https://chillicream.com/_optimized/images/remote/5a65a00398c8fc0afb627eb1d2557dd0da8c797404be8aa6a5e3c3fc8c636689.png)Glen](https://chillicream.com/authors/glen)2026-05-288 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-05-28-mcp-hotchocolate-fusion&text=From+GraphQL+to+MCP+in+Two+Lines) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-05-28-mcp-hotchocolate-fusion) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [fusion](https://chillicream.com/blog/tags/fusion) - [nitro](https://chillicream.com/blog/tags/nitro) - [mcp](https://chillicream.com/blog/tags/mcp) - [ai](https://chillicream.com/blog/tags/ai) - [llm](https://chillicream.com/blog/tags/llm) Agents are becoming first-class consumers of our APIs. Alongside the web and mobile clients we have always served, our servers now talk to models that reason over typed tool results and decide what to call next. The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is the interface they use. If you already run a GraphQL server with Hot Chocolate or a Fusion gateway, you have most of what you need to be an MCP server. You have a typed schema, you have operations with arguments and results, you have validation and error handling. What is missing is the wiring. Hot Chocolate 16 ships that wiring. With the new MCP adapter and Nitro as the control plane, two calls on your server expose every published tool and prompt at `/graphql/mcp`. Authoring is plain files on disk, deployment is a CLI command, and rolling out a new version of your tool catalog does not require a redeploy. ## What MCP is, and why you might want it MCP is an open standard for connecting AI applications to external systems. The host (Claude, ChatGPT, a VS Code agent, an internal agent runtime) speaks the protocol once. Any MCP-compatible server plugs in without custom integration code per product. An MCP server exposes two main things: - **Tools** are callable operations. The model picks which one to call, fills in the arguments, and reads the result. - **Prompts** are templated workflows. The user picks one from the host's prompt menu, fills in a few inputs, and the host hands a pre-shaped conversation to the model. For a GraphQL backend the fit is good. A tool is a GraphQL operation, its arguments are GraphQL variables, and its result is the JSON your server already returns. Your schema stays the source of truth, and your tool catalog is just a set of operations against it. ## The pieces There are three moving parts: 1. **The MCP adapter** on your Hot Chocolate server or Fusion gateway. It speaks MCP over Streamable HTTP and answers tool and prompt requests by running operations against the schema. 2. **A feature collection** on disk: GraphQL files for tools, JSON files for prompts, optional HTML files for inline views. 3. **Nitro** as the control plane. It stores versioned, immutable snapshots of the collection, validates them on upload, distributes them to your runtime, and surfaces per-tool telemetry. The adapter does not know how to fetch tools on its own. It asks an `IMcpStorage` for them. With Nitro referenced, that storage is wired up automatically. With Nitro absent, you can implement `IMcpStorage` yourself for self-hosted scenarios, but most teams should let Nitro do the heavy lifting. ## Enable MCP on a Hot Chocolate server Start with an existing GraphQL server. Reference the adapter, the core Nitro package, and the Hot Chocolate integration: Bash ``` dotnet add package HotChocolate.Adapters.Mcp dotnet add package ChilliCream.Nitro dotnet add package ChilliCream.Nitro.HotChocolate ``` `ChilliCream.Nitro` ships a source generator that emits an `AddDefaults()` extension method from the integration packages you reference. With `ChilliCream.Nitro.HotChocolate` in the project, `AddDefaults()` calls `AddHotChocolate()` for you. Wire it up: C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddNitro(o => { o.ApiId = builder.Configuration["Nitro:ApiId"]!; o.ApiKey = builder.Configuration["Nitro:ApiKey"]!; o.Stage = builder.Configuration["Nitro:Stage"]!; }) .AddDefaults(); builder .AddGraphQL() .AddMcp(); var app = builder.Build(); app.MapGraphQL(); app.MapGraphQLMcp(); app.Run(); ``` `AddMcp()` registers the MCP server and a warmup that pulls tool and prompt definitions from storage at startup. `MapGraphQLMcp()` exposes the transport at `/graphql/mcp`. That is the URL any MCP client will connect to. If you prefer environment variables, set `NITRO_API_ID`, `NITRO_API_KEY`, and `NITRO_STAGE` and drop the `AddNitro` delegate entirely. Nitro service options bind to them. ## Enable MCP on a Fusion gateway The Fusion story is the same shape. Different packages, same two calls: Bash ``` dotnet add package HotChocolate.Fusion.Adapters.Mcp dotnet add package ChilliCream.Nitro dotnet add package ChilliCream.Nitro.Fusion ``` C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddNitro(o => { o.ApiId = builder.Configuration["Nitro:ApiId"]!; o.ApiKey = builder.Configuration["Nitro:ApiKey"]!; o.Stage = builder.Configuration["Nitro:Stage"]!; }) .AddDefaults(); builder .AddGraphQLGateway() .AddMcp(); var app = builder.Build(); app.MapGraphQL(); app.MapGraphQLMcp(); app.Run(); ``` The gateway resolves your tools' GraphQL operations across all source schemas it composes, so a single tool can fetch data from multiple subgraphs in one call. From the model's perspective there is just one MCP server and one URL. ## Author tools and prompts on disk A feature collection is a folder tree. Each tool and each prompt lives in its own folder, and files inside the folder share the folder name as the basename: ``` mcp/ ├── prompts/ │ └── SearchProducts/ │ └── SearchProducts.json └── tools/ └── SearchProducts/ ├── SearchProducts.graphql ├── SearchProducts.html └── SearchProducts.json ``` The CLI picks files up by glob (`./mcp/tools/**/*.graphql`, `./mcp/prompts/**/*.json`) and brings sibling `.json` and `.html` files along automatically. ### A tool is a GraphQL operation The minimum a tool needs is a `.graphql` file. The basename is the tool name. GraphQL variables become MCP tool arguments. The result the server returns is what the model sees. `mcp/tools/SearchProducts/SearchProducts.graphql`: GraphQL ``` query SearchProducts($text: String!, $first: Int! = 10) { products(searchText: $text, first: $first) { nodes { id name price pictureUrl } } } ``` With just that file, `SearchProducts` is already a working MCP tool. ### Optional settings Add a sibling `.json` file for a custom title, icons, or behavior hints: `mcp/tools/SearchProducts/SearchProducts.json`: JSON ``` { "title": "Search Products", "annotations": { "openWorldHint": false, "idempotentHint": true } } ``` The title shows up in the host's tool picker. Annotations help the model decide whether the call is safe to retry or whether it could have side effects. ### An optional MCP Apps view [MCP Apps](https://apps.extensions.modelcontextprotocol.io/) is an extension to MCP that lets the server return interactive HTML the host renders inside the chat, alongside the plain-text result. The host loads the HTML into a sandboxed iframe and bridges JSON-RPC over `postMessage`, so the view can read tool results, follow the host's theme, and call other tools. Drop a sibling `.html` file next to your `.graphql` file. The basename matches the tool name. A small JavaScript module connects to the host via the MCP Apps SDK: `mcp/tools/SearchProducts/SearchProducts.html`: HTML ```
    ``` What is happening here: 1. We import the MCP Apps SDK straight from jsDelivr to keep the example self-contained. In production you would bundle it (Vite, esbuild, your tool of choice) and emit a single HTML file as the build output, so the view does not depend on a third-party CDN at runtime. 2. We register an `ontoolresult` handler before calling `app.connect()`. The host can deliver the initial tool result the moment the bridge opens, so wiring the handler up after the connect call would miss it. 3. The handler reads `result.structuredContent`, which is the JSON result of the tool's GraphQL operation, and renders the nodes as a plain list. In hosts that support MCP Apps, the chat will render this list inline. In plain-text hosts the view is ignored and the tool result is displayed as text. Both work without changes. ### A prompt is templated JSON `mcp/prompts/SearchProducts/SearchProducts.json`: JSON ``` { "title": "Search Products", "description": "Search the catalog from a free-text query.", "arguments": [ { "name": "searchQuery", "title": "Search Query", "required": true } ], "messages": [ { "role": "user", "content": { "type": "text", "text": "Find products related to \"{searchQuery}\" in our catalog." } } ] } ``` `{searchQuery}` is interpolated from the matching argument. Whatever you declare in `arguments` is available as `{name}` inside `messages`. ## Publish with the Nitro CLI The CLI does the archive, validate, upload, and publish steps. Three commands take you from disk to a live tool catalog. ### 1\. Create the feature collection A collection is a named container for a set of tools and prompts. Create one for your API: ``` nitro mcp create \ --name "Product Catalog" \ --api-id "" ``` `` comes from `nitro api list` or the Nitro UI. The command prints the new collection ID. Save it. ### 2\. Upload a tagged version Each upload is a complete, immutable snapshot tagged with a name (a release tag, a Git SHA, anything you want): ``` nitro mcp upload \ --mcp-feature-collection-id "" \ --tag "v1" \ --tool-pattern "./mcp/tools/**/*.graphql" \ --prompt-pattern "./mcp/prompts/**/*.json" ``` The CLI walks the glob patterns, picks up sibling `.json` and `.html` files, packages everything into a ZIP, and uploads it. Nitro validates the archive on the server before storing it. ### 3\. Publish to a stage Uploading does not expose anything to clients. Publishing makes a tagged version live on a stage: ``` nitro mcp publish \ --mcp-feature-collection-id "" \ --tag "v1" \ --stage "dev" ``` Stages are independent. Publishing to `dev` does not touch `production`. To roll back, publish an earlier tag to the same stage. The runtime picks up the change over the Nitro change feed and updates its tool set in place, no restart required. ### Optional: smoke-test with MCP Inspector Before you point a chat host at the URL, exercise the tools with [MCP Inspector](https://github.com/modelcontextprotocol/inspector), the official MCP debugging tool: ``` npx @modelcontextprotocol/inspector ``` Open the printed URL in a browser, point it at `https:///graphql/mcp`, and invoke the tools with arbitrary arguments. This catches schema mismatches and bad prompt JSON without round-tripping through a chat host. ## Add the server to ChatGPT ChatGPT exposes remote MCP servers as apps in Developer mode. The flow is similar in Claude, VS Code, and other hosts. The exact menus shift over time, so the canonical references are: - [Connect from ChatGPT (Apps SDK)](https://developers.openai.com/apps-sdk/deploy/connect-chatgpt) - [Claude custom connectors](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp) - [VS Code MCP servers](https://code.visualstudio.com/docs/copilot/customization/mcp-servers) - [Full MCP clients directory](https://modelcontextprotocol.io/clients) In ChatGPT, open **Settings** → **Apps** → **Advanced settings** and enable **Developer mode**. Then add a new app: ![Add an app in ChatGPT](https://chillicream.com/images/blog/2026-05-28-mcp-hotchocolate-fusion/chatgpt-add-app.webp) Fill in a name, the MCP server URL (`https:///graphql/mcp`), and the authentication settings your server requires. A description is optional. Tick the **I understand and want to continue** checkbox to acknowledge the warning about third-party servers: ![Configure the MCP server URL](https://chillicream.com/images/blog/2026-05-28-mcp-hotchocolate-fusion/chatgpt-configure-server.webp) Click **Create**. The app's detail page opens, listing the published tools with a **Connect** button in the top right: ![Tools listed in the app](https://chillicream.com/images/blog/2026-05-28-mcp-hotchocolate-fusion/chatgpt-tools-prompts.webp) Click **Connect**. ChatGPT shows a confirmation dialog that summarizes permissions, data use, and the risks of connecting third-party servers: ![Connect dialog with permissions and data-use information](https://chillicream.com/images/blog/2026-05-28-mcp-hotchocolate-fusion/chatgpt-connect-app.webp) Confirm by clicking **Connect Product Catalog** at the bottom of the dialog. The app is now available in chats. In a new chat, open the **+** menu next to the composer, expand **More**, and select your app: ![Selecting the Product Catalog app from the chat composer menu](https://chillicream.com/images/blog/2026-05-28-mcp-hotchocolate-fusion/chatgpt-select-app.webp) When the model decides to call `SearchProducts`, the host invokes the tool, your gateway runs the GraphQL operation, and the result comes back. If the tool has an Apps view, it renders inline: ![SearchProducts result with Apps view](https://chillicream.com/images/blog/2026-05-28-mcp-hotchocolate-fusion/chatgpt-tool-result.webp) ## What you get from Nitro Once tools are flowing, the management surface is where Nitro pays off: - **Versioning**: every `nitro mcp upload` produces an immutable tagged snapshot. Rollback is republishing an earlier tag. - **Multi-stage**: publish to `dev`, validate, then publish the same tag to `production`. Stages are independent and permissions are stage-scoped. - **Validation**: GraphQL documents are validated against your schema, prompt JSON against its structure, and the validator checks for conflicts with what is already published. Broken collections never reach a stage. - **Telemetry**: per-tool request count, error rate, mean and P95/P99 latency, traces, and structured logs in the Nitro UI. When a new version is published, the change feed pushes it to the runtime. The server picks up the new tools and prompts in place. ## Wrap up Two lines on the server, a folder of files on disk, three CLI commands to publish. If you have a Hot Chocolate server or a Fusion gateway, you have an MCP server. We are building on top of the existing strengths of GraphQL here. Your tools reuse the schema you already have, your arguments reuse the type system you already have, and your results reuse the responses you already serve. The MCP adapter is the protocol shim. Nitro is the control plane. Everything else is your API. If you want to go deeper, the full references are here: - Hot Chocolate adapter: [MCP Adapter](https://chillicream.com/docs/hotchocolate/adapters/mcp) - Fusion adapter: [MCP Adapter](https://chillicream.com/docs/fusion/adapters/mcp) - Nitro authoring and CLI: [Nitro MCP](https://chillicream.com/docs/nitro/adapters/mcp) - MCP specification: [modelcontextprotocol.io](https://modelcontextprotocol.io/) - MCP Apps SDK: [API reference](https://apps.extensions.modelcontextprotocol.io/api/) Give it a try, ship a few tools, and let us know what you build. ## You might also like [View all](https://chillicream.com/blog) --- # What's new in Fusion 16 > Fusion 16 is a GraphQL federation gateway built on ASP.NET Core, featuring Aspire-driven composition, incremental delivery, Semantic Introspection, OpenAPI adapters, and connectors for REST and gRPC. Canonical source: https://chillicream.com/blog/2026-05-15-fusion-16 ![](https://chillicream.com/images/blog/2026-05-15-fusion-16/header.png) [![Michael Staib's avatar](https://chillicream.com/_optimized/images/remote/2bb0dd6e1b9347a1d3732300ea752f39f1ac4f1f76587433ca3a407245b31786.jpg)Michael Staib](https://chillicream.com/authors/michael-staib)2026-05-1514 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-05-15-fusion-16&text=What%27s+new+in+Fusion+16) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-05-15-fusion-16) - [fusion](https://chillicream.com/blog/tags/fusion) - [graphql](https://chillicream.com/blog/tags/graphql) - [federation](https://chillicream.com/blog/tags/federation) - [dotnet](https://chillicream.com/blog/tags/dotnet) - [aspnetcore](https://chillicream.com/blog/tags/aspnetcore) When we first created Fusion, it was built as an extension on top of Hot Chocolate. This approach let us leverage Hot Chocolate’s strengths and saved us significant development and maintenance effort. However, it also imposed constraints on both projects. Hot Chocolate had to avoid breaking Fusion, and Fusion was limited by Hot Chocolate’s architecture. For example, because we couldn’t change the type system to natively carry the metadata Fusion needs for operation planning, we had to rely on generic extension points, which cost us performance. The breaking point came with a Hot Chocolate 14 bug fix that inadvertently broke Fusion’s query planner. A correct fix in one project became a regression in the other. That’s when we knew it was time to untangle the two. Around this time, we noticed other gateway vendors rewriting their solutions in Rust, while others went straight to Go. It’s tempting to follow that trend: pick a new language, claim performance gains, and move on. For us, though, switching platforms did not make sense. We have always **considered ASP.NET Core our biggest asset**, and building on it with C# gave us the strongest foundation for Fusion. > Upgrading from Fusion 15? Head straight to the [migration guide](https://chillicream.com/docs/fusion/migration/migrate-from-15-to-16) for the full list of breaking changes and upgrade notes. ## ASP.NET Core Every GraphQL gateway faces the same challenges and must implement core features like authentication, header propagation, retries, rate limits, observability, and more. While most gateways either reimplement these features or hide them behind layers of configuration and closed binaries, we chose a different path. Fusion is NOT a closed product you install and configure from the outside. Instead, it is an open library that you bring into your own ASP.NET Core application, giving you direct access to every part of the stack and letting you shape the gateway to your needs. Fusion is an OPEN library that sits on top of ASP.NET Core, giving you full access to your Program.cs, middleware pipeline, dependency injection container, and `IHttpClientFactory`. Your application is the gateway, not a closed product or a black box. This single decision shapes everything that follows: - Authentication uses the same AddAuthentication() you already know, whether that is JWT, OIDC, mTLS, cookies, or any other method your platform team prefers. Fusion does not ship its own authentication stack because ASP.NET Core already provides one. - Header propagation, mTLS to subgraphs, connection pooling, hedging and retries are all managed by `IHttpClientFactory`. This component is battle-tested by Microsoft and used at massive scale in Azure, Bing, and Office. - Observability is built on the standard .NET OpenTelemetry pipeline, using the same exporters, conventions, and dashboards as the rest of your fleet. Fusion implements the new GraphQL OpenTelemetry specification, so traces and metrics align with other GraphQL servers. - Extensibility is pure C#. There is no scripting layer, no custom binary to compile, and no out-of-process coprocessors. If you need Redis, just add the StackExchange.Redis package and write your code. Most importantly, Fusion automatically benefits from every security patch Microsoft releases, every performance improvement in the .NET stack, and every new Kestrel release. To get started with a Fusion gateway, first install our templates: Bash ``` dotnet new install HotChocolate.Templates ``` Next, create your project: Bash ``` dotnet new graphql-gateway ``` That’s all it takes. The default gateway is as simple as an empty ASP.NET Core web application. You can ship it as-is, bundled with the composition output: C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddHttpClient("fusion"); builder .AddGraphQLGateway() .AddFileSystemConfiguration("./gateway.far"); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` If you want to add request deduplication, just add a message handler to the HttpClient. There is no need for brittle YAML configuration. Register the HttpClient for the gateway, enable deduplication, and you are done: C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddHttpClient("fusion") .AddRequestDeduplication(); builder .AddGraphQLGateway() .AddFileSystemConfiguration("./gateway.far"); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` For incremental retry, hedging, or other policies, simply add Polly or use the Aspire service defaults: C# ``` builder.Services .AddHttpClient("fusion") .AddRequestDeduplication() .AddStandardResilienceHandler(options => { options.Retry.MaxRetryAttempts = 5; options.Retry.BackoffType = DelayBackoffType.Linear; options.Retry.Delay = TimeSpan.FromMilliseconds(500); }); ``` For a full walkthrough, see the [Getting Started](https://chillicream.com/docs/fusion/getting-started) guide. ## Performance With Fusion 16 we focused on .NET and examined the core challenges for the gateway. These are similar to the problems Kestrel had to solve. In the hot path, a GraphQL gateway repeatedly fetches data from subgraphs and integrates it into the gateway response. Most of this data is JSON. A naive approach would parse each subgraph response into a `JsonDocument`, build the gateway response as a mutable `JsonNode`, and merge them. This method is inefficient, leading to many object allocations and constant data copying. Inspired by Rust’s arena allocation model, where all resources for a request are released together, we sought a better way than relying on the garbage collector. In .NET, it’s common to rent byte arrays to reduce pressure on the garbage collector and keep allocations stable. However, resizing arrays is inefficient and can hurt performance. When you need to store data but do not know its final size, you typically rent an array of a certain size. If the array turns out to be too small, you must rent a larger one, copy the existing data over, and return the old array. This process is slow and reduces the efficiency of the array pool, especially with unpredictable GraphQL response sizes. To make .NET competitive, we adopted several principles: 1. **Everything is managed as bytes**. This allows memory to be reused for metadata, objects, scalars, and JSON. 2. **We do not copy memory**. Instead of copying data from the source schema results into the gateway result, we reference the data directly. The gateway result is composed of many pointers to the memory of the source schemas. This approach reduces the need to duplicate data and keeps the memory footprint small. 3. **Memory is chunked**. We never expand a rented array, which would require copying. Instead, memory is divided into fixed-size chunks. Each request rents chunks and writes into them as needed. Most values fit within a single chunk, but cross-chunk reads are supported and efficient. 4. **Each request owns its memory chunks** and returns them when completed. This prevents memory leaks and simplifies resource management. 5. **The memory pool expands as needed** and only releases capacity when demand drops. This approach avoids sudden garbage collection spikes after brief increases in memory usage. To validate our approach, we forked the GraphQL federation benchmarks from The Guild and integrated Fusion. We expanded the benchmarks and ran them nightly on dedicated hardware to ensure consistent results. Each benchmark runs ten times per gateway for accuracy. In constant-load benchmarks against Rust subgraphs with no added latency, Fusion ranks second only to the Hive Router, outperforming two Rust-based routers and one Go router. | Gateway | Version | Median RPS | Best RPS | Worst RPS | CV% | Notes | | --------------------------- | ------------- | ---------- | -------- | --------- | ---- | ------------------------------------------- | | hive-router | v0.0.49 | 2,889 | 3,082 | 2,866 | 2.6% | | | hotchocolate | 16.1.0-p.1.10 | 2,140 | 2,175 | 2,127 | 0.8% | | | grafbase | 0.53.3 | 2,061 | 2,101 | 2,024 | 1.2% | | | cosmo | 0.307.0 | 1,255 | 1,273 | 1,246 | 0.7% | non-compatible response (2 across 2/9 runs) | | hive-gateway-router-runtime | 2.5.25 | 541 | 553 | 535 | 1.0% | | | apollo-router | v2.13.1 | 424 | 433 | 411 | 1.6% | | | hive-gateway | 2.5.25 | 252 | 257 | 250 | 0.9% | | | apollo-gateway | 2.13.3 | 238 | 240 | 236 | 0.6% | | In a more realistic scenario, where subgraphs have a fixed 4ms cost per request (simulating database access or other IO), the gap between Hive Router and Fusion nearly disappears. | Gateway | Version | Median RPS | Best RPS | Worst RPS | CV% | Notes | | --------------------------- | ------------- | ---------- | -------- | --------- | ---- | ------------------------------------------- | | hive-router | v0.0.49 | 1,590 | 1,618 | 1,585 | 0.7% | | | hotchocolate | 16.1.0-p.1.10 | 1,441 | 1,463 | 1,434 | 0.6% | | | cosmo | 0.307.0 | 1,136 | 1,152 | 1,127 | 0.9% | non-compatible response (2 across 2/9 runs) | | grafbase | 0.53.3 | 1,121 | 1,142 | 1,110 | 0.9% | | | hive-gateway-router-runtime | 2.5.25 | 511 | 522 | 507 | 1.0% | | | apollo-router | v2.13.1 | 394 | 404 | 391 | 1.1% | | | hive-gateway | 2.5.25 | 244 | 248 | 242 | 0.9% | | | apollo-gateway | 2.13.3 | 236 | 239 | 234 | 0.7% | | The full benchmark suite lives in our [federation benchmarks](https://github.com/ChilliCream/graphql-gateway-benchmarks). With .NET 11, Microsoft is moving async execution into the runtime, eliminating the need for compiler tricks. This will allow us to reduce allocations even further and narrow the performance gap to the Hive Router. Fusion delivers an exceptionally fast gateway that ranks near the top in benchmarks, while providing all the benefits of .NET and ASP.NET Core. For tuning knobs like HTTP/2 multiplexing, connection pooling, and concurrency limits, see the [Performance Tuning](https://chillicream.com/docs/fusion/performance-tuning) guide. ## AOT On another note, Fusion 16 now supports AOT compilation. With an AOT compiled gateway, startup is instant because there is no need to wait for JIT compilation. However, we found that a JIT compiled gateway achieves higher throughput once it is warmed up. You can choose between instant startup with AOT or higher peak throughput with JIT. The best option depends on how quickly you need to scale and start new instances. ## Query Planner Overhaul In Fusion 16, we have completely reworked the query planner. Query plans are now fully serializable, so you can export and import them as needed. This enables features like query plan pinning and build-time planning for complex federated setups. The new planner also produces much clearer execution plan views. ![Query Planner view in Nitro](https://chillicream.com/images/blog/2026-05-15-fusion-16/query-plan.png) It’s now easier than ever to understand how operations are executed. You can click into any operation to see which other operations contribute to the result, making it simple to analyze and optimize your queries. ![Query plan operation path](https://chillicream.com/images/blog/2026-05-15-fusion-16/query-plan-path.png) But what was fundamentally difficult in the past was to debug into a plan. Now with Fusion 16 and Nitro we can go into the plan details to see the structure of the subgraph request but also to look at the requirement data that is passed in. ![Query plan details view](https://chillicream.com/images/blog/2026-05-15-fusion-16/query-plan-details.png) In the details you find a button to test the operations which will create a new tab that is configured to run this operation against the subgraph directly. ![Query plan testing view](https://chillicream.com/images/blog/2026-05-15-fusion-16/query-plan-testing.png) ## Aspire The Fusion Aspire integration has been completely redesigned. It no longer depends on command-line tools for composition. Now, you simply annotate your subgraphs and declare that they expose a schema endpoint. The composer fetches the schema from each endpoint and composes the gateway automatically at startup. C# ``` var accountsApi = builder .AddProject("accounts-api") .WithReference(accountsDb) .WithEnvironment("ConnectionStrings__accounts_db", accountsDb.Resource.ConnectionStringExpression) .WithGraphQLSchemaEndpoint() .WaitFor(postgres); ``` Aspire is excellent for the inner development loop. Being able to compose on the fly, right on your development machine, without any CLI tooling, makes iterating on a federated graph feel as fast and seamless as editing a single service. ![Aspire composition with Fusion](https://chillicream.com/images/blog/2026-05-15-fusion-16/aspire-composition.png) The [Local Development](https://chillicream.com/docs/fusion/local-development) guide covers schema endpoint annotations, composition settings, and Nitro-backed remote subgraph composition in detail. ## CI/CD Deployment is another area where we have made significant improvements. Previously, deploying a subgraph required running at least five CLI commands, which was excessive for most setups. The new Nitro CLI streamlines this process. In version 16, you can deploy with a single command: `nitro fusion publish`. The transactional flow is still available if you need it, but for most cases, the new publish command is all you need. We also now provide native integration with both GitHub Actions and Azure DevOps, making deployment even easier on these platforms. ![GitHub Actions integration for Fusion](https://chillicream.com/images/blog/2026-05-15-fusion-16/github.png) See the [Deployment and CI/CD](https://chillicream.com/docs/fusion/deployment-and-ci-cd) guide and the [Nitro CLI reference](https://chillicream.com/docs/fusion/cli) for the full command set. ## Incremental Delivery We’re excited to announce support for `@defer` in the Fusion gateway. Fusion now supports both the v0.1 and v0.2 incremental delivery protocols, as well as the streamlined JSONL format. Defer and stream are fully integrated into your query plans, and we’ve worked hard to make this process efficient. Incremental delivery is enabled by default, but you can also control it explicitly through the gateway options: C# ``` builder .AddGraphQLGateway() .ModifyOptions(o => o.EnableDefer = true); ``` ## Semantic Introspection I am not going to cover all of our AI-focused work in this post, because several of those features deserve their own write-up. One addition is worth a quick mention here though: Semantic Introspection. Classic GraphQL introspection is great when a client wants to inspect the whole schema. For agents, that is often too blunt. They usually do not want the whole schema, they want the right part of the schema for the task in front of them. Dumping thousands of fields into the model costs tokens, pollutes context, and still leaves the model to figure out what matters. On top of that, many enterprise GraphQL schemas are simply too large to fit comfortably, even in a 1M-token context window. Semantic Introspection turns schema discovery into a search problem. With `__search`, an agent can ask for the capabilities that are relevant to a user task and get back the best matching types and fields, together with the paths that lead to them. With `__definitions`, it can then fetch just the precise schema details it needs to build the next query. That is what makes it cool: GraphQL keeps its precision, while discovery becomes a constant-shape two-step process that works the same whether your schema has 10 types or 1000\. In the dedicated post, a discovery-cost comparison also shows it to be markedly more cost-efficient than the other approaches that were measured. Semantic Introspection is enabled by default in development mode, just like introspection itself. C# ``` builder .AddGraphQLGateway() .ModifyOptions(o => o.EnableSemanticIntrospection = false); ``` By default, Fusion indexes the schema with BM25, so there is nothing else to wire up. If you want the full story, including how `__search` and `__definitions` work in practice, the [Semantic Introspection](https://chillicream.com/blog/2026-04-22-semantic-introspection) post goes much deeper. And if you want to see the agent side of it, including the skill prompt that teaches an agent how to use semantic introspection effectively, take a look at the [GraphQL skill prompt](https://github.com/PascalSenn/apidays-singapore/blob/main/case-study/prompt-graphql-skill.md). ## Adapters and Connectors Adapters and Connectors are major additions in Fusion 16, each deserving a deep dive of their own. Here’s a quick overview: With Fusion 16, we introduce two new concepts for the gateway. Adapters allow you to expose your GraphQL schema as other protocols, such as OpenAPI, MCP, and soon gRPC. Connectors on the other hand let you integrate non-GraphQL APIs, including OpenAPI, gRPC, and other federation dialects like Apollo Federation, into Fusion. These APIs are treated as if they were native GraphQL Federation subgraphs. ![Fusion gateway overview: adapters and connectors around the Fusion core](https://chillicream.com/images/blog/2026-05-15-fusion-16/gateway-overview.png) ### Adapters Adapters create API projections on top of your GraphQL schema. For example, the OpenAPI adapter lets you publish a curated REST API based on an existing GraphQL schema. This is especially useful for providing integration surfaces to external partners or for building scenario-specific REST endpoints. Projecting a GraphQL operation as a REST endpoint is simple. Just annotate the operation with a few directives: GraphQL ``` "Fetches a user by their id" query GetUserById($userId: ID!) @http(method: GET, route: "/users/{userId}") { userById(id: $userId) { id name email } } "Creates a user" mutation CreateUser($user: UserInput! @body) @http(method: POST, route: "/users") { createUser(user: $user) { id name email } } ``` ![OpenAPI adapter exposing annotated GraphQL operations as REST endpoints](https://chillicream.com/images/blog/2026-05-15-fusion-16/openapi-adapter.png) We currently ship Adapters for OpenAPI and MCP, with a gRPC Adapter landing in one of the next dot releases. The [MCP Adapter](https://chillicream.com/docs/fusion/adapters/mcp) docs are up now; the OpenAPI Adapter docs follow with its dedicated post. ### Connectors Connectors complement Adapters by letting you plug non-GraphQL APIs directly into the gateway. Fusion currently supports OpenAPI, gRPC, and Apollo Federation. This means Fusion can sit in front of an Apollo Federation graph or a mix of REST and gRPC services, without requiring you to add a GraphQL server for each one. We’ll explore both Adapters and Connectors in more detail in dedicated posts soon. ## Composite Schemas Working Group A few years ago, together with Apollo and The Guild, we began working on an open specification for federated GraphQL under the GraphQL Foundation: the Composite Schema Specification. But the spec itself isn't the only thing the group has produced. Along the way, we've gotten a shared vocabulary, a reference test suite, and implementations across multiple vendors. For the first time, federation is not controlled by any single company, but by the GraphQL community. This specification is now nearing completion, and Fusion 16 fully implements the current version. Once finalized, it will be published as the official GraphQL Federation specification. Already today, there are two gateways implementing the spec and two more that will announce adoption very soon. It's truly amazing to see how the GraphQL ecosystem has worked together to bring this specification to life and create true interoperability. ## Wrapping up When we set out to rewrite Fusion, we had three main goals: remove the constraints imposed by Hot Chocolate, achieve top-tier performance with .NET, and make the gateway feel like a natural extension of your ASP.NET Core app, rather than a black box configured with YAML. Fusion 16 is the first release where all three goals are realized. You get the same Kestrel, the same `IHttpClientFactory`, and the same OpenTelemetry pipeline, but now with a brand-new type system and execution engine designed specifically for Fusion. Everything in this post, Aspire-driven composition, single-command publishing, `@defer`, Semantic Introspection, Adapters and Connectors, is open source, MIT, and built on open standards under the GraphQL Foundation. Give it a spin with `dotnet new graphql-gateway`, or explore an end-to-end setup in the [Fusion demo](https://github.com/ChilliCream/fusion-demo), which shows the new Fusion in action across subgraphs, Aspire composition, and the gateway. Jump into our [Slack](https://slack.chillicream.com/) if you get stuck. Stay tuned: gRPC adapters, deeper Federation interop, and the long-form posts on Adapters, Connectors, and the new execution engine are all queued up. ## You might also like [View all](https://chillicream.com/blog) --- # What's new for Hot Chocolate 16 > Hot Chocolate 16 brings a new type system, better scalar contracts, safer defaults, improved batching, semantic introspection, and a new GraphQL error mode. Canonical source: https://chillicream.com/blog/2026-05-11-hot-chocolate-16 ![](https://chillicream.com/images/blog/2026-05-11-hot-chocolate-16/header.png) [![Michael Staib's avatar](https://chillicream.com/_optimized/images/remote/2bb0dd6e1b9347a1d3732300ea752f39f1ac4f1f76587433ca3a407245b31786.jpg)Michael Staib](https://chillicream.com/authors/michael-staib)2026-05-1112 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-05-11-hot-chocolate-16&text=What%27s+new+for+Hot+Chocolate+16) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-05-11-hot-chocolate-16) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [graphql](https://chillicream.com/blog/tags/graphql) - [dotnet](https://chillicream.com/blog/tags/dotnet) - [aspnetcore](https://chillicream.com/blog/tags/aspnetcore) Hot Chocolate 16 is our first new major release of the platform in more than a year, and it is a big one. There is a lot to talk about across version 16, so this post focuses on the Hot Chocolate server. Some releases are incremental, some are mostly technical. Hot Chocolate 16 is different. We rearchitected the type system, tightened scalar contracts, improved batching, adopted new GraphQL proposals, and made the defaults safer. Most of that starts with the new type system. ## The new type system In previous versions we introduced a small library called **HotChocolate.Skimmed** for editing SDL. It offered a rich type system API similar to Hot Chocolate's core, but with one important difference: the type system was mutable and deliberately allowed invalid intermediate states. The idea behind Skimmed was to provide a modern API for Fusion composition and Strawberry Shake client generation. For Fusion we initially considered spinning up yet another type system, tuned specifically to the gateway's needs. That pushed us into a conundrum: duplicating not just the type systems but also validation, execution, and everything around them would have left us with so much maintenance that we would have been stuck. So instead, we introduced a new abstraction, `HotChocolate.Types.Abstractions`, that describes the basics. On top of it we rewrote everything we had built over the years: validation, execution, and the rest. We then implemented the abstraction for Skimmed (now `HotChocolate.Types.Mutable`), `HotChocolate.Types`, and `HotChocolate.Fusion`. Each has its own extras, but shared concerns like the IBM Cost spec now work across all three. That makes it easy to write things like analyzers that take a GraphQL SDL, transform it, and then run it through validation. ## Scalars As part of this rewrite, we also tackled the scalar API. If you have ever written a custom scalar in Hot Chocolate, you have probably noticed that the surface area is, let's say, ambitious. There is `Serialize`, `Deserialize`, `ParseLiteral`, `ParseValue`, `ParseResult`, `IsInstanceOfType`, `TryDeserialize`, and getting them all to agree with each other was a small art form. The mental model was never quite clean, and that bled into every custom scalar you wrote. In Hot Chocolate 16, we redesigned the scalar API to align with the GraphQL reference implementation. C# ``` public sealed class PolicyType : ScalarType { public PolicyType(string name, BindingBehavior bind = BindingBehavior.Explicit) : base(name, bind) { } // Construct the runtime value from a string literal. protected override Policy OnCoerceInputLiteral(StringValueNode valueLiteral) => new(valueLiteral.Value); // Construct the runtime value from a variable value. protected override Policy OnCoerceInputValue(JsonElement inputValue, IFeatureProvider context) => new(inputValue.GetString()!); // Serialize the runtime value into the GraphQL response format. protected override void OnCoerceOutputValue(Policy runtimeValue, ResultElement resultValue) => resultValue.SetStringValue(runtimeValue.Value); // Construct a GraphQL literal from the runtime value, used for introspection. protected override StringValueNode OnValueToLiteral(Policy runtimeValue) => new(runtimeValue.Value); } ``` That is it. It is a breaking change, but the gain in clarity is well worth it. ## scalars.graphql.org For a long time, the contract of a GraphQL scalar was mostly its name. That is a weak contract for something like `DateTime`. Two servers could both expose a `DateTime` scalar and mean slightly different things, and clients had very little to go on beyond convention and documentation. That is the gap [scalars.graphql.org](https://scalars.graphql.org/) is meant to close. It gives scalar authors a place to publish precise specifications that servers can attach to their schemas through the `@specifiedBy` directive. ChilliCream and Apollo helped define a number of those specs, and while doing that work we took a hard look at the scalars we ship in Hot Chocolate. The result in v16 is fewer built-in scalars, but much better ones. The scalars we keep now follow published specifications and expose them directly in the schema. That means API consumers can inspect a schema, understand exactly what a scalar means, and handle it correctly on the client side instead of guessing from the name alone. The full list, with diffs, lives in the [migration guide](https://chillicream.com/docs/hotchocolate/migrating/migrate-from-15-to-16). ## Date and time scalars Date and time handling was one of the places where older Hot Chocolate versions were too lax. Moving from the built-in .NET types to NodaTime, or back again, was effectively a breaking API change because we let implementation details bleed into the client-facing schema. If you used the standard scalars you got one vocabulary. If you used `HotChocolate.Types.NodaTime`, you got another, with types like `OffsetType`, `InstantType`, and `ZonedDateTimeType`. Clients had to know which world they were in. That was the wrong contract. Your schema should describe the meaning of the data, not which date and time library your server happens to use internally. In v16 we tightened things around a small set of well-specified scalars: `DateTime` for timestamps, `LocalDate` for calendar dates, `LocalTime` for clock times, `LocalDateTime` for local timestamps, and `Duration` for durations. These now follow the published scalar specifications and their ISO 8601 based formats, so the contract is precise and portable. That also changes what `HotChocolate.Types.NodaTime` does. It no longer introduces a parallel, NodaTime-specific schema vocabulary. Instead, it plugs NodaTime in as an alternative runtime representation of those same GraphQL scalars, with the extra precision and correctness NodaTime is known for. For the common date and time mappings, you can move between the built-in .NET types and NodaTime without changing the schema your clients see. One `AddNodaTime()` call and you are set up: C# ``` builder .AddGraphQL() .AddNodaTime(); ``` If you need to bind `System.TimeSpan` to `Duration`, or if you want custom precision settings, register the specific scalar explicitly. The important change is that NodaTime no longer forces a different public schema vocabulary. We are already looking at a few more date and time mappings for the v16.x line. We have not made up our minds yet, so if there is a type you care about, please tell us. ## A new batching engine Efficient batching is one of those things that sounds simple until you build a GraphQL server. DataLoader is still a great tool, but it can feel heavy for straightforward scenarios because you have to split one piece of data-fetching logic across a resolver and a DataLoader. In v16 we reworked the batching engine from the ground up. Execution is now more predictable, and transport-level batch requests are no longer treated as a set of isolated executions that just happen to arrive together. Instead, Hot Chocolate can fold them into a single execution with a shared batching session, which means overlapping work can naturally collapse into fewer round-trips. We also wanted to make common batching scenarios easier to express. That is why Hot Chocolate 16 introduces batch resolvers. Instead of wiring together a resolver and a DataLoader, you can now write a single resolver that receives all parent objects for a field at once: C# ``` [BatchResolver] public static async Task> GetProductCountAsync( [Parent(requires: nameof(Brand.Id))] List brands, [Service] CatalogContext context, CancellationToken cancellationToken) { var brandIds = brands.Select(b => b.Id).ToList(); var counts = await context.Products .Where(p => brandIds.Contains(p.BrandId)) .GroupBy(p => p.BrandId) .Select(g => new { BrandId = g.Key, Count = g.Count() }) .ToDictionaryAsync(g => g.BrandId, g => g.Count, cancellationToken); return brands.Select(b => counts.GetValueOrDefault(b.Id, 0)).ToList(); } ``` To the consumer, this still looks like a simple field in the schema: GraphQL ``` type Brand { productCount: Int } ``` So when we run a query like this: GraphQL ``` { brands(first: 5) { nodes { productCount } } } ``` `GetProductCountAsync` runs once for the five brands in the result set and returns all counts in one go. Does this make DataLoaders obsolete? Not at all. DataLoaders are still the right tool when you want batching to live in your application layer instead of in a resolver. They let you define normalized data-fetching primitives once, reuse them across resolvers, and still get automatic batching and deduplication. ## Variable and Request Batching We have also been investing in the emerging [batching proposal for GraphQL over HTTP](https://github.com/graphql/graphql-over-http/pull/307). In v16, transport batches are folded into a single work scheduler, so variable batching and request batching are executed as if the work had been sent as one colocated request. The result is that batching is no longer just a transport trick, it behaves like a first-class execution mode. Variable batching lets you execute the same operation multiple times with different variable sets in one HTTP request. Request batching lets you send multiple independent GraphQL operations in the same HTTP request. The two compose naturally, so a single batch can contain regular requests and variable-batched requests side by side. Here is what that looks like over the wire: JSON ``` [ { "query": "query GetProduct($id: ID!) { productById(id: $id) { name } }", "operationName": "GetProduct", "variables": { "id": "1" } }, { "query": "query GetProduct($id: ID!) { productById(id: $id) { name } }", "operationName": "GetProduct", "variables": [{ "id": "2" }, { "id": "3" }] } ] ``` And here is a JSON Lines response stream: ``` {"data":{"productById":{"name":"Cup"}},"requestIndex":1,"variableIndex":0} {"data":{"productById":{"name":"Hat"}},"requestIndex":0} {"data":{"productById":{"name":"Plate"}},"requestIndex":1,"variableIndex":1} ``` Results can arrive out of order as soon as they are ready. `requestIndex` tells you which request in the outer array a result belongs to, and `variableIndex` identifies which variable set within a variable-batched request produced that result. ## Semantic Introspection I am not going to cover all of our AI-focused work in this post, because several of those features deserve their own write-up. One addition is worth a quick mention here though: Semantic Introspection. Classic GraphQL introspection is great when a client wants to inspect the whole schema. For agents, that is often too blunt. They usually do not want the whole schema, they want the right part of the schema for the task in front of them. Dumping thousands of fields into the model costs tokens, pollutes context, and still leaves the model to figure out what matters. On top of that, many enterprise GraphQL schemas are simply too large to fit comfortably, even in a 1M-token context window. Semantic Introspection turns schema discovery into a search problem. With `__search`, an agent can ask for the capabilities that are relevant to a user task and get back the best matching types and fields, together with the paths that lead to them. With `__definitions`, it can then fetch just the precise schema details it needs to build the next query. That is what makes it cool: GraphQL keeps its precision, while discovery becomes a constant-shape two-step process that works the same whether your schema has 10 types or 1000\. In the dedicated post, a discovery-cost comparison also shows it to be markedly more cost-efficient than the other approaches that were measured. Semantic Introspection is enabled by default in development mode just like introspection itself. C# ``` builder .AddGraphQL() .ModifyOptions(o => o.EnableSemanticIntrospection = false); ``` By default, Hot Chocolate indexes the schema with BM25, so there is nothing else to wire up. If you want the full story, including how `__search` and `__definitions` work in practice, the [Semantic Introspection](https://chillicream.com/blog/2026-04-22-semantic-introspection) post goes much deeper. And if you want to see the agent side of it, including the skill prompt that teaches an agent how to use semantic introspection effectively, take a look at the [GraphQL skill prompt](https://github.com/PascalSenn/apidays-singapore/blob/main/case-study/prompt-graphql-skill.md). ## A new error mode for GraphQL Hot Chocolate 15 had experimental support for `@semanticNonNull`. That was never meant to be the final answer. It was a bridge while the GraphQL community worked toward a proper way to express error handling. The underlying issue is null propagation. In GraphQL today, if a non-null field errors, that error can bubble up and erase part of the response tree. For clients built around colocated fragments, that is especially painful because an error in one component can wipe out data that belongs to sibling components. The error is no longer contained, it bleeds across the selection set. Hot Chocolate 16 moves to the new [onError proposal](https://github.com/graphql/graphql-spec/pull/1163). Instead of baking the behavior into the schema, the client can ask for it on the request. If you want clients to opt in per request, enable overrides: C# ``` builder .AddGraphQL() .ModifyRequestOptions(o => o.AllowErrorHandlingModeOverride = true); ``` A client can then send: JSON ``` { "query": "...", "onError": "NULL" } ``` With `onError: "NULL"`, Hot Chocolate stops null propagation, returns `null` at the field that failed, and still reports the error. That lets the client decide how to contain the failure, for example at the fragment or component boundary, instead of letting one error erase neighboring parts of the response. If you want that behavior for every request, set `DefaultErrorHandlingMode = ErrorHandlingMode.Null`. We still have clients that understand `@semanticNonNull` but do not support `onError` yet. For those clients, you can enable the new error mode on the server and expose a `@semanticNonNull` schema. That way the runtime behavior matches the semantics the compatibility schema advertises. C# ``` app.MapGraphQLSchema(); app.MapGraphQLSemanticNonNullSchema(); ``` The same trick is available from the CLI via `schema export --semantic-non-null` or programmatically with our `SchemaFormatter` by setting `RewriteToSemanticNonNull = true`. ## Feature lifecycle with opt-in features GraphQL has long had a good story for the end of a feature's life. `@deprecated` lets you signal that something is going away before you remove it. What it did not have was a standard story for the beginning of a feature's life, the phase where something is real, usable, and worth getting feedback on, but not yet stable enough for general use. The `@requiresOptIn` proposal closes that gap and turns the lifecycle into `experimental` \-> `stable` \-> `deprecated` \-> `removed`. Hot Chocolate 16 implements that proposal. You can mark fields, arguments, input fields, and enum values as opt-in, and they stay out of normal introspection until the client explicitly asks for them. That gives you a clean rollout story for experimental capabilities, expensive operations, or APIs that should only be adopted deliberately. C# ``` builder .AddGraphQL() .ModifyOptions(o => o.EnableOptInFeatures = true) .OptInFeatureStability("product-recommendations", "experimental"); public class Product { public int Id { get; set; } [RequiresOptIn("product-recommendations")] public IReadOnlyList? Recommendations { get; set; } } ``` We also extended the proposal. Hot Chocolate lets you declare feature stability at the schema level and expose it through introspection with `__schema.optInFeatures`, `__schema.optInFeatureStability`, and `includeOptIn`. GraphQL ``` schema @optInFeatureStability( feature: "product-recommendations" stability: "experimental" ) { query: Query } type Query { productById(id: ID!): Product } type Product { id: Int! recommendations: [Product] @requiresOptIn(feature: "product-recommendations") } ``` In practice that means schema evolution becomes much more deliberate. You can ship something as experimental, promote it to stable when it has earned it, and later retire it through the existing deprecation flow. ## Incremental delivery, the new format `@defer` and `@stream` also got a wire-format refresh. In v16, the default is now the v0.2 format from the incremental delivery spec, the one with `pending`, `incremental` entries identified by `id`, and `completed`. We use it consistently across multipart, SSE, and JSON Lines. The older v0.1 format, the path-based one, is still fully supported. If you need to keep an existing client on it, you have two options: - **Per-request:** add `incrementalSpec=v0.1` to the `Accept` header. - **Server-wide:** call `AddHttpResponseFormatter(incrementalDeliveryFormat: IncrementalDeliveryFormat.Version_0_1)`. Most clients will not need to do anything. v0.2 is where the GraphQL ecosystem is heading, and it is the better default going forward. But if you have a client that hand-rolls multipart parsing, you can keep the old format until you are ready to move. One more thing: batching performance is now much better for incremental requests too. Like batch requests, an incremental request runs on a single work scheduler, which means it uses a single batching coordinator. ## GraphQL semantic conventions for OpenTelemetry OpenTelemetry has been around in the GraphQL space for a while, but it was never especially well specified. Different servers ended up with their own span names and attributes, which made cross-server tooling and conventions harder than they should have been. That changed with the new [GraphQL semantic conventions for OpenTelemetry](https://github.com/graphql/otel-wg/blob/main/spec), created by the GraphQL OTel working group. With Hot Chocolate 16, we have adopted that specification. In practice, that means our tracing now follows a shared GraphQL vocabulary instead of the server-specific conventions we used before. If you already have dashboards or alerts wired to the old names or values, the migration guide covers the rename and value changes. ## MCP and OpenAPI These deserve their own posts, so I will keep this short. v16 ships two new adapters that work both with Hot Chocolate and with Fusion: - **MCP**, a [Model Context Protocol](https://modelcontextprotocol.io/) server adapter that exposes GraphQL operations as MCP tools for LLM agents. It also supports agentic UI through the MCP app extension. - **OpenAPI**, an adapter for projecting GraphQL operations as OpenAPI definitions. Both will get their own posts in the next few weeks. If you do not want to wait, the docs are already up for the [MCP adapter](https://chillicream.com/docs/hotchocolate/adapters/mcp) and the [OpenAPI adapter](https://chillicream.com/docs/hotchocolate/adapters/openapi). ## Wrapping up If you are upgrading, start with our [migration guide](https://chillicream.com/docs/hotchocolate/migrating/migrate-from-15-to-16). It has the full list of breaking changes, defaults, and migration notes. This is only the beginning. We will follow up with more posts and YouTube episodes that dive deeper into new features across Hot Chocolate, Fusion, and Mocha. We also have a large community on [Slack](https://slack.chillicream.com/), so come join us there. And if you like what we are building, help us out by starring the [project on GitHub](https://github.com/ChilliCream/graphql-platform). ## You might also like [View all](https://chillicream.com/blog) --- # Semantic Introspection > The agentic age of software brings new challenges for our APIs. Semantic Introspection makes GraphQL discoverable, scalable, and precise for LLMs. Canonical source: https://chillicream.com/blog/2026-04-22-semantic-introspection ![](https://chillicream.com/images/blog/2026-04-22-semantic-introspection/header.png) [![Pascal Senn's avatar](https://chillicream.com/_optimized/images/remote/3ebc6212aa4091005799d46f902a92e2eb7e77cd966797ecb5f4eaeab3bb26fe.png)Pascal Senn](https://chillicream.com/authors/pascal-senn)2026-04-227 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-04-22-semantic-introspection&text=Semantic+Introspection) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2026-04-22-semantic-introspection) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [graphql](https://chillicream.com/blog/tags/graphql) - [ai](https://chillicream.com/blog/tags/ai) - [llm](https://chillicream.com/blog/tags/llm) - [semantic-introspection](https://chillicream.com/blog/tags/semantic-introspection) The agentic age of software has just begun, and it brings a whole new set of challenges for our applications. Until recently, the consumers of our APIs, web apps, and mobile apps were human users. Going forward, our APIs will increasingly be consumed by LLMs. Where we used to optimize for request performance, time to first byte, and 3G performance, we now have to think about context window size, LLM cost, turn reduction, and hallucinations. It is interesting to see that the classic Lighthouse metrics we spent so much effort perfecting are largely irrelevant for LLMs. The sweat, blood, and tears we poured into pushing those four numbers close to 100% do not add much value for large language models. It does not matter if your data fetch takes less than 500ms when the LLM needs 15 seconds to process the data and generate a response per turn. What matters now is reducing the number of turns. Your API response should include every piece of relevant information, but it should not include more than that, because every extra byte pollutes the context of your LLM. For an agent to interact with an API, the interface has to be three things: **discoverable**, **scalable**, and **precise**. **Discoverable** Your application has functionality that can be accessed through some sort of interface. If that application wants to interact with an LLM it needs an API. But having an API is not enough. The LLM has to know what functionality the API provides and have a way to discover its capabilities. This could be an OpenAPI document for REST, the `list_tools` tool from MCP, or GraphQL introspection. **Scalable** Applications become increasingly more capable. With the introduction of agents, the age of software really has begun. When the cost of adding a feature drops, we naturally add more features. The consequence is that more and more capabilities end up exposed through our APIs. The interaction model between the API and the agents therefore needs to scale with the amount of available capabilities. The agent should work with 10 available tools, but it should also work the same way when there are 3000. **Precise** Every byte returned by an API has to be processed by an LLM to extract the information it needs. Data returned by an API stays in the context of the model and is sent to the LLM on every subsequent roundtrip. The more context we send, the more input tokens we pay for. At the same time, we want to avoid additional roundtrips whenever possible, because every roundtrip means sending the context again and waiting another 20 seconds for the LLM to respond. We want a precise API that returns all the data we need, and nothing we do not. There are currently different interaction models for agents and APIs. Looking at the API ecosystem today, the most common technology for API documentation is OpenAPI. Through an OpenAPI document an agent can _discover_ the available endpoints, the required parameters, and the expected responses. However, the agent has to either load the whole OpenAPI document into the LLM context, or store it on disk and search through it with `grep` or something similar, which leads to a lot of roundtrips. On top of that, the responses are fixed. There is no way to dynamically adjust what the server returns. All of this leads to context pollution and, combined with the extra roundtrips, higher cost. The AI ecosystem has been focused on MCP over the past months. MCP is already natively integrated with all major LLM providers, and through `list_tools` agents can _discover_ the available tools. Just like OpenAPI, MCP is not _precise_. The response is fixed, and the agent has to pass it to the LLM for verification. MCP also does not _scale_ well. To make use of an MCP server, the whole tool directory has to be sent to the LLM so it has a directory of the available tools. The orchestrator on your machine or browser does not know if the LLM will reply with a tool call or a normal response, so it has to send the whole tool directory on every roundtrip. This adds a lot of input tokens and cost, and it only works while the tool directory stays small. If it gets too big, the LLM cannot process it because it exceeds the context window. It is not just us that have noticed these problems. Even Anthropic, the creator of MCP, has [acknowledged the flaws](https://www.anthropic.com/engineering/code-execution-with-mcp). As an alternative to MCP, Anthropic pushes skills or recommends just using CLIs. The issue with these approaches is that they have a higher barrier to entry. Configuring an MCP server is simple even for non technical users, but using a CLI tool is not, especially if they do not know what a terminal is. So, what about GraphQL? One of the biggest marketing points of GraphQL has always been the "no overfetching" promise. By writing a query, we can specify exactly what data we want from the server, nothing more and nothing less. With Fusion, this data can even be spread across many different backend services, while the client still interfaces with what looks like a single API. (You can check out a sample repository [here](https://github.com/PascalSenn/apidays-singapore), where we combine several APIs from data.gov.sg.) This makes GraphQL a very _precise_ API. In that regard, it does not suffer from context pollution like OpenAPI or MCP. Another core feature built into GraphQL from day one is its introspection capabilities. With GraphQL introspection, an agent can _discover_ the schema of the API and learn exactly which queries and mutations are available, what arguments they take, and what data they return. Yet, like all other technologies, GraphQL has the _scale_ problem. While a GraphQL schema is more compact than an OpenAPI schema, it can still become too big for an LLM to process, and sending it on every turn adds cost. This is where [**Semantic Introspection**](https://github.com/graphql/ai-wg/blob/main/rfcs/semantic-introspection.md) comes in. Semantic Introspection is a proposed extension to GraphQL introspection. Semantic Introspection adds a new field to the GraphQL server, `__search(query: "query text")`. With this field, an agent can ask the server a question, and the server returns the schema members that best match semantically. If the user asks the LLM "What's the weather like in Bedok today and are there any taxis available?", the agent can forward the question to the server via `__search`. GraphQL ``` { __search(query: "What's the weather like in Bedok today and are there any taxis available", first: 10) { coordinate score pathsToRoot definition { __typename ... on __Field { # left out for brevity } ... on __Type { # left out for brevity } } } } ``` The GraphQL server then returns the best matching schema members ranked by score. JSON ``` { "data": { "__search": [ { "coordinate": "Area.availableTaxis", "score": 1, "pathsToRoot": [["Query.areaByName", "Area.availableTaxis"]], "definition": { "__typename": "__Field", "fieldName": "availableTaxis", "description": "Returns the number of available taxis in the area", "type": { "name": null, "kind": "NON_NULL", "ofType": { "name": "Int", "kind": "SCALAR" } }, "args": [] } }, { "coordinate": "WeatherStation", "score": 0.5979468822479248, "pathsToRoot": [["Query.areaByName", "Area.nearestStation"]], "definition": { "__typename": "__Type", "name": "WeatherStation", "kind": "OBJECT", "description": "A weather station that provides weather information for an area" } } // left out for brevity ] } } ``` The LLM now knows which parts of the schema are relevant for the user query. Thanks to the precomputed paths to root, the agent also knows how to reach the relevant parts of the schema from the query root. To know how to build a query, the LLM needs a bit more detail about the path. It can use `__definitions(coordinates: ["Query.areaByName", "Area.nearestStation"])` to fetch the details for those coordinates. Put together, this makes GraphQL's _discovery_ capabilities _scalable_ too. Discovery of any capability becomes a simple two-step process: first, search for the relevant capabilities with `__search`, then fetch the details with `__definitions`. The process stays the same whether your schema has 10 types or 1000\. By providing descriptions for types and fields, you can also make the search more effective and improve the score of relevant schema members, simply by improving the documentation of your schema. If we run a small experiment comparing the cost of discovery across OpenAPI, MCP, and GraphQL with Semantic Introspection, GraphQL with Semantic Introspection comes out significantly more cost effective than the other two approaches. | Discovery Approach | Tokens sent to LLM | Cost (USD) | | ----------------------------------- | ------------------ | ---------- | | OpenAPI | 665,564 | $0.3950 | | GraphQL Schema | 133,441 | $0.1072 | | GraphQL with Semantic Introspection | 59,067 | $0.0895 | The latest Hot Chocolate preview already supports Semantic Introspection. You can just turn it on with `.ModifyOptions(x => x.EnableSemanticIntrospection = true)`. By default it indexes the schema with BM25, which comes at no additional cost. We will soon provide an option to hook the semantic search up to Nitro and back it with embeddings, which will provide even better search results. Check out the demo repository with all the code here: [Semantic Introspection Demo](https://github.com/PascalSenn/apidays-singapore) and let us know what you think about Semantic Introspection! ## You might also like [View all](https://chillicream.com/blog) --- # Open Telemetry for All Your Services > Send OpenTelemetry logs and traces from GraphQL APIs, REST services, and background workers to Nitro, with PAT and CLI automation updates. Canonical source: https://chillicream.com/blog/2025-03-17-open-telemetry-for-everyone ![](https://chillicream.com/images/blog/2025-03-17-open-telemetry-for-everyone/header.png) [![Pascal Senn's avatar](https://chillicream.com/_optimized/images/remote/3ebc6212aa4091005799d46f902a92e2eb7e77cd966797ecb5f4eaeab3bb26fe.png)Pascal Senn](https://chillicream.com/authors/pascal-senn)2025-03-174 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2025-03-17-open-telemetry-for-everyone&text=Open+Telemetry+for+All+Your+Services) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2025-03-17-open-telemetry-for-everyone) - [nitro](https://chillicream.com/blog/tags/nitro) - [open-telemetry](https://chillicream.com/blog/tags/open-telemetry) - [dotnet](https://chillicream.com/blog/tags/dotnet) - [aspnetcore](https://chillicream.com/blog/tags/aspnetcore) ## Open Telemetry for All Your Services (and More!) We’re thrilled to introduce **OpenTelemetry support for all your .NET-based services** \- not just your GraphQL Servers. Whether you have REST APIs, background workers, or any other .NET applications, you can now unify and analyze your telemetry data in Nitro. This marks a significant step in helping you gain deeper insights across your entire infrastructure. With **ChilliCream.Nitro.Telemetry** version 15.0.0 and 14.1.0, simply call the extension method `AddNitroTelemetry` in your service registration to integrate Nitro with any .NET service. All you need to do is configure OpenTelemetry exporters, and Nitro will collect and visualize your logs and traces: C# ``` services.ConfigureOpenTelemetryTracerProvider(x => x.AddNitroExporter()); services.ConfigureOpenTelemetryLoggerProvider(x => x.AddNitroExporter()); services.AddNitroTelemetry(options => { options.ApiId = apiId; options.ApiKey = apiKey; options.Stage = stage; }); ``` On the trace overview of your API in the Nitro dashboard, select **OpenTelemetry** from the dropdown on the top right. You’ll be able to inspect all your HTTP requests, background workers, or anything else you’re tracking with OTEL. We’ve also **drastically improved telemetry performance**, so these insights will load and refresh faster than ever. ![Telemetry Overview](https://chillicream.com/images/blog/2025-03-17-open-telemetry-for-everyone/otel1.png) In this post, you’ll also learn about: - **Personal Access Tokens (PATs)**, which bring more secure and granular authentication options to your automation workflows. - **Enhanced Non-Interactive Command Execution** in the Nitro CLI, enabling full automation for API lifecycle tasks. - The difference between **API Keys** and **PATs**, helping you choose the right authentication mechanism for every scenario. Read on to discover how you can level up your .NET observability and API management automation, all within one powerful platform. --- ### Introducing Personal Access Tokens (PATs) To provide more flexibility and security in your automation processes, we have introduced **Personal Access Tokens (PATs)**. PATs allow you to authenticate with the Nitro platform in a secure and granular manner, ideal for scripting and automated tasks. #### What are PATs? Personal Access Tokens are tokens associated with your user account that grant access to the Nitro API. - **Automation-Friendly**: PATs are perfect for use in CI/CD pipelines, scripts, and other automated workflows where you need to authenticate non-interactively. - **User-Specific**: Unlike API keys tied to a specific API, PATs are linked to your user account, providing access across multiple APIs. #### How to Create a PAT You can create a PAT using the following command: ``` nitro pat create --description "My Automation Token" --expires 180 ``` - `--description`: A description for the token to help you identify it later. - `--expires`: The number of days after which the token will expire (default is 180 days). ### Non-Interactive Command Execution We have improved the Nitro CLI to support full non-interactive execution for all commands. This enhancement empowers you to automate every aspect of your API lifecycle management, from creating APIs and editing stages to generating API keys. #### Benefits of Non-Interactive Commands - **Automation**: Integrate Nitro CLI commands into your scripts and CI/CD pipelines without manual intervention. - **Consistency**: Ensure consistent execution of tasks across different environments. - **Efficiency**: Automate repetitive tasks to save time and reduce human error. #### How to Use Commands Non-Interactively All commands now accept input via command-line options or environment variables, allowing you to bypass interactive prompts. For example, to create an API non-interactively: ``` nitro api create --name "My API" --path "/my-api" --workspace-id "workspace123" ``` You can also set environment variables for inputs: ``` export NITRO_API_NAME="My API" export NITRO_API_PATH="/my-api" export NITRO_WORKSPACE_ID="workspace123" nitro api create ``` #### Parsing Command Output By default, Nitro CLI provides human-readable output. When automating, you might need machine-readable output. Use the `--output json` option to get the output in JSON format: ``` nitro api-key list --output json ``` This output can then be parsed using tools like `jq`: ``` nitro api-key list --output json | jq '.' ``` ### API Keys vs. PAT **API Keys** - **Purpose**: Designed for application-level authentication, such as telemetry reporting from your GraphQL server. - **Scope**: Tied to a specific API and workspace. - **Creation**: Generated using the `nitro api-key create` command. - **Usage**: Best for telemetry, fusion, client registry, or other application-level tasks where you need to authenticate a specific API. **Personal Access Tokens (PATs)** - **Purpose**: Intended for user-level authentication, suitable for automating tasks that require broader access across APIs. - **Scope**: Associated with your user account, has workspace permissions. - **Creation**: Generated using the `nitro pat create` command. - **Usage**: Best for scripts, automation tools, and CI/CD pipelines that need to perform various operations on the Nitro platform. --- ### Get Started Today With **expanded OpenTelemetry integration**, **personal access tokens**, and **full non-interactive command support**, it’s never been easier to automate your API management workflows while simultaneously gaining comprehensive insights into your.NET services. Try out the improved telemetry overview, create your first PAT, and add Nitro to your automation pipelines to take full advantage of these new features. #### Need Help? We’ve Got You Covered! Whether you’re integrating OpenTelemetry for the first time or looking to streamline your API automation, our **support contracts** provide expert guidance to help you get up and running quickly. From troubleshooting to best practices, our team is here to ensure your success. [Learn more about our support plans](https://chillicream.com/services/support) and get tailored assistance for your specific needs. #### Resources - **Documentation**: Check out our [updated documentation](https://chillicream.com/docs/nitro) for a deeper look at the new commands and options. - **Support**: If you have any questions or need assistance, feel free to reach out to our support team on slack! ## You might also like [View all](https://chillicream.com/blog) --- # What's new for Hot Chocolate 15 > Hot Chocolate 15 updates the type system and supported .NET versions, and adds new projection, filtering, sorting, pagination, and DataLoader capabilities. Canonical source: https://chillicream.com/blog/2025-02-01-hot-chocolate-15 ![](https://chillicream.com/images/blog/2025-02-01-hot-chocolate-15/hot-chocolate-15.png) [![Michael Staib's avatar](https://chillicream.com/_optimized/images/remote/2bb0dd6e1b9347a1d3732300ea752f39f1ac4f1f76587433ca3a407245b31786.jpg)Michael Staib](https://chillicream.com/authors/michael-staib)2025-02-0111 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2025-02-01-hot-chocolate-15&text=What%27s+new+for+Hot+Chocolate+15) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2025-02-01-hot-chocolate-15) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [graphql](https://chillicream.com/blog/tags/graphql) - [dotnet](https://chillicream.com/blog/tags/dotnet) - [aspnetcore](https://chillicream.com/blog/tags/aspnetcore) Originally, we did not plan on releasing another major version of Hot Chocolate before we release the next major version of Fusion. Really the whole team was focused on working in Fusion, our platform for building distributed GraphQL services. However, while I was upgrading Microsoft’s Azure Data API Builder to Hot Chocolate 14, I stumbled upon a regression introduced between Hot Chocolate 12 and Hot Chocolate 14\. Unfortunately, fixing it in 14.4 would have required breaking changes. ## Type System Microsoft’s Azure Data API Builder uses type interceptors to build a GraphQL schema from a database schema. The intermediary schema it produces is a heavily annotated GraphQL schema document. This approach is straightforward and we do something similar with Hot Chocolate Fusion. However, in the case of Azure Data API Builder, the directives used are more complex and utilize input object structures. The issue here is that, when we complete a type while initializing the schema, we do not yet have the entire schema available. Parsing directives is thus not possible before all input types are available. While we have tests covering input objects in directives, they did not cover the specific complexity used by Azure Data API Builder. Also, a bit of randomness in the order of initialization exacerbated the problem. In Hot Chocolate 15, we fixed this by introducing two additional steps to the initialization process of the type system: - Complete types – This step no longer completes directives and default values; instead, it only completes types and directive definitions. - Complete metadata – Directive annotations on type-system members and default values are now completed here, once all types have been fully established. However, this change had broader implications, so we also introduced another step afterward to complete resolvers and make the schema executable. **Why does this matter to you?** If you are using type interceptors, the `OnAfterCompleteTypes` hook may no longer behave as before. This might cause errors that do not break compilation per se but do break the runtime behavior. Because of these changes, we decided to rename the Hot Chocolate 14.4 release to Hot Chocolate 15. ## Opportunities If we are doing a major release, let’s not waste a version bump just for a regression fix. We decided to merge the changes of our current development branch and turn it into a proper major release, shipping a few nice features in the process. ## Supported .NET Versions With Hot Chocolate 15, we have realigned the frameworks we support and dropped support for .NET Standard 2.0, .NET 6.0, and .NET 7.0\. Going forward, Hot Chocolate 15 supports .NET 8.0 and .NET 9.0\. This allows us to modernize a lot of code and remove many conditional compilation directives. ## Projections One long-standing feature of Hot Chocolate is the `HotChocolate.Data` integration, which makes it easy to build on top of _Entity Framework_, Marten, _RavenDB_ or _MongoDB_. It also rich data features like pagination, sorting, filtering, and projections with minimal setup: C# ``` [UsePaging] [UseProjection] [UseFiltering] [UseSorting] public static IQueryable GetProducts(CatalogContext dbContext) => dbContext.Products.AsNoTracking(); ``` The drawback to this approach is that your GraphQL layer requires direct access to your data layer, which can be undesirable. It can also lead to a **cartesian explosion** issue, where users can traverse deeply into the graph, retrieving a huge number of rows in the process. This can be not only slow but also quite expensive in terms of database and network load. Sure, you can use split queries in the case of Entity Framework: C# ``` [UsePaging] [UseProjection] [UseFiltering] [UseSorting] public static IQueryable GetProducts(CatalogContext dbContext) => dbContext.Products.AsSplitQuery(); ``` We introduced some **experimental** features in Hot Chocolate 14 to address these problems, but we weren’t fully satisfied with the implementation. The general direction was good, but the specifics needed work. So we took a step back, refined what worked, and turned it into a proper feature that is no longer experimental. ## DataLoader Basics Let’s first discuss **DataLoader**, a fundamental concept in GraphQL for batching and deferring data fetching. What many people don’t know is that Meta (formals Facebook) used the concept of a "data loader" before GraphQL even existed, calling it "preparables" or "loader." The idea is to have a unified, interface for efficiently fetching data. A DataLoader typically lives close to the data-access layer, and your business logic uses it to fetch data. ![Application Building Blocks](https://chillicream.com/images/blog/2025-02-01-hot-chocolate-15/dataloader-1.png) The business logic itself should remain simple. Don’t burden your business logic or your consumer with batching concerns or other complexities in fetching data. **Lets start at the top!** Ideally, we want our GraphQL layer to be as thin as possible. It serves us to expose our business logic to the outside world but should not be the business logic. C# ``` public static async Task> GetBrandByIdAsync( int id, BrandService brandService, CancellationToken cancellationToken) => await brandService.GetBrandsAsync(id, cancellationToken); ``` There can be many variants of this approach (e.g., using MediatR), but the important bit is: **the resolver is thin and has no direct access to your data layer**. It merely hooks into your business logic. **Example using MediatR:** C# ``` [UsePaging] public static async Task> GetBrandByIdAsync( int id, ISender sender, CancellationToken cancellationToken) => await sender.Send(new GetBrandByIdQuery(id), cancellationToken); ``` The business logic enforces rules, handles authorization, or any other checks. The same business logic applies whether you expose it through GraphQL, REST, or some internal service call. Below is a simplified query/handler: C# ``` public record GetBrandByIdQuery(int BrandId) : IRequest; public class GetBrandByIdQueryHandler(IBrandByIdDataLoader dataLoader) : IRequestHandler { public Task Handle( GetBrandsQuery request, CancellationToken cancellationToken) => dataLoader.LoadAsync(request.BrandId, cancellationToken); } ``` In this example, we’re using a DataLoader (IBrandByIdDataLoader) that fetches a single brand by ID. Regardless of how many times a brand is requested in a GraphQL query, the DataLoader will batch these requests together into a single database call. That’s the power of DataLoader. GraphQL ``` query GetBrandById { brand(id: 1) { id name } } query GetBrandsByProducts { products { nodes { name brand { name } } } } ``` With Hot Chocolate, writing a DataLoader only requires you to implementing your batch fetch logic. Our source generator takes care of interfaces, dispatching logic, and so forth: C# ``` [DataLoader] public static async Task> GetBrandByIdAsync( IReadOnlyList ids, CatalogContext context, CancellationToken cancellationToken) => await context.Brands .Where(t => ids.Contains(t.Id)) .ToDictionaryAsync(t => t.Id, cancellationToken); ``` So, the example above will lead to the DataLoader interface `IBrandByIdDataLoader`. ## Adding Projections, Filtering, and Sorting How do we now introduce _client-driven_ projections, filtering, and sorting without exposing `IQueryable` from our business layer? In Hot Chocolate 15, we rethought and rewrote the **GreenDonut** (DataLoader) implementation and introduced some new packages that provide a few primitives to pass between layers while keeping them isolated. ![GreenDonut Packages](https://chillicream.com/images/blog/2025-02-01-hot-chocolate-15/greendonut-1.png) These packages introduce four foundational types: - `Page` represents a slice (page) of a larger data set. - `PagingArguments` are used to page through a data set, define what slice you want to have the the larger data set. - `QueryContext`: Encapsulates a selector expression, a filter expression, and a sort definition. - `SortDefinition`: Defines how to sort a data set. These primitives let us build _data-driven_ interactions into our business layer. Let’s have a look how we might update our resolver to support projections or filtering by adding a `QueryContext`. First lets update our `GetBrandByIdQueryHandler` by adding the query context as optional argument that we pass on to the DataLoader. C# ``` public record GetBrandByIdQuery(int BrandId, QueryContext? Query = null) : IRequest; public class GetBrandByIdQueryHandler(IBrandByIdDataLoader dataLoader) : IRequestHandler { public Task Handle( GetBrandByIdQuery request, CancellationToken cancellationToken) => dataLoader.With(Query).LoadAsync(request.BrandId, cancellationToken); } ``` The `QueryContext` is simple to use and pass around. In this specific case where we do not need sorting or filtering, we could also just pass an `Expression>` to describe the properties requested by the client. Both will work just fine. C# ``` public record GetBrandByIdQuery(int BrandId, Expression>? Selector = null) : IRequest; public class GetBrandByIdQueryHandler(IBrandByIdDataLoader dataLoader) : IRequestHandler { public Task Handle( GetBrandByIdQuery request, CancellationToken cancellationToken) => dataLoader.Select(Selector).LoadAsync(request.BrandId, cancellationToken); } ``` > if you are using DTOs in your solution the mapping would already be done by the DataLoader. So the selector would be on the DTO type. This is really a minimal optional change to our business layer but now allows us the specify a selector. Within our DataLoader we can now inject the `QueryContext` as DataLoader state and apply this state to our queryable, which will rewrite the queryable to only select the properties the client requested. C# ``` [DataLoader] public static async Task> GetBrandByIdAsync( IReadOnlyList ids, QueryContext query, CatalogContext context, CancellationToken cancellationToken) => await context.Brands .Where(t => ids.Contains(t.Id)) .With(query) .ToDictionaryAsync(t => t.Id, cancellationToken); ``` Within our batching method it will never be null. The source generator will either give us an empty context which will select all properties from Brand or pass along the one from the business layer, that reflects the selection choices of the client request. Within Hot Chocolate you can now register the `QueryContext` so that the resolver compiler recognizes it and compiles an expression for it which defines the field selection. C# ``` services .AddGraphQLServer() ... .AddQueryContext(); ``` With that setup we can update our resolver. C# ``` [UsePaging] public static async Task> GetBrandByIdAsync( int id, QueryContext query, ISender sender, CancellationToken cancellationToken) => await sender.Send(new GetBrandByIdQuery(id, query), cancellationToken); ``` This is a very powerful feature that allows you to keep your business layer clean while still enabling client-driven data fetching. Since we are using DataLoader, we do not have to worry about split queries or other inefficiencies. ## Pagination So, how does this change our resolver when we want to filter and sort? Lets say we have a top-level query that fetches all brands. C# ``` [UsePaging] [UseFiltering] [UseSorting] public static async Task GetBrandsAsync( ISender sender, CancellationToken cancellationToken) => await sender.Send(new GetBrands(), cancellationToken); ``` Lets first look at the `GetBrandsQuery` so that we understand what we need to do under the hood. C# ``` public record GetBrandsQuery( PagingArguments PagingArguments, QueryContext? Query = null) : IRequest>; ``` The `GetBrandsQuery` is a simple record that takes paging arguments and an optional query context. The paging arguments specify which portion of the dataset to retrieve. The query returns a page of brands, which is one of our four GreenDonut primitives. C# ``` public class GetBrandQueryHandler(CatalogContext context) : IRequestHandler> { public async Task> Handle( GetBrandsQuery request, CancellationToken cancellationToken) => await context.Brands .With(request.Query) .ToPageAsync(request.PagingArguments, cancellationToken); } ``` The handler in this case simply uses the `CatalogContext` and applies the query context to the queryable. With GreenDonut, we introduced the `ToPageAsync` extension method, which paginates the dataset and implements cursor-based paging algorithms. For cursor pagination to work, we need a guaranteed order in the dataset. This ensures that we filter directly into the correct section rather than skipping rows and wasting performance on the database. The cursors encode the properties used for ordering. If we were not using client-controlled ordering, I would simply define an order with LINQ. C# ``` context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(request.PagingArguments cancellationToken); ``` The important point here is that the order must produce a unique cursor. That’s why we added the `Id` property as the last `ThenBy`. This is a common pattern to ensure the cursor remains unique With client-controlled sorting, we cannot simply use LINQ, as we do not know whether the user has already applied an order. This is where the Wither method comes in — it allows us to rewrite the order to ensure that the sort definition produces a unique cursor. C# ``` public class GetBrandQueryHandler(CatalogContext context) : IRequestHandler> { public async Task> Handle( GetBrandsQuery request, CancellationToken cancellationToken) => await context.Brands .With(request.Query, s => s.AddAscending(t => t.Id)) .ToPageAsync(request.PagingArguments, cancellationToken); } ``` In the example above, we added the `Id` property in ascending order. However, we could also define a default order if the user has not specified one. C# ``` public class GetBrandQueryHandler(CatalogContext context) : IRequestHandler> { public async Task> Handle( GetBrandsQuery request, CancellationToken cancellationToken) => await context.Brands .With(request.Query, DefaultOrder) .ToPageAsync(request.PagingArguments, cancellationToken); private static SortDefinition DefaultOrder(SortDefinition sort) => sort.IfEmpty(o => o.AddDescending(t => t.Name)).AddAscending(t => t.Id); } ``` Basically, ordering by name is only added if the user has not defined an order, whereas ordering by ID is always appended. C# ``` [UsePaging] [UseFiltering] [UseSorting] public static async Task GetBrandsAsync( PagingArguments pagingArgs, QueryContext query, ISender sender, CancellationToken cancellationToken) => await sender.Send(new GetBrands(pagingArgs, query), cancellationToken); ``` The resolver itself changes only slightly, simply passing along the selection, filter, and sorting context wrapped in a `QueryContext`, along with some paging arguments. However, this alone is not sufficient. In GraphQL, the paging type is a connection, whereas in our business layer, we have designed it as a `Page`. Therefore, we still need to convert the `Page` to a `Connection`. For this the `HotChocolate.Data` package provides an extension method called `ToConnectionAsync`. C# ``` [UsePaging] [UseFiltering] [UseSorting] public static async Task> GetBrandsAsync( PagingArguments pagingArgs, QueryContext query, ISender sender, CancellationToken cancellationToken) => await sender.Send(new GetBrands(pagingArgs, query), cancellationToken).ToConnectionAsync(); ``` Awesome, we’re done! But wait — we didn’t use a DataLoader in this case since it’s a top-level functionality. DataLoaders are useful when fetching data by a specific key. For instance, if we were retrieving a product’s brand, we would need a DataLoader. Otherwise, a query like the following could result in an excessive number of database queries. GraphQL ``` query GetBrandsByProducts { brands { nodes { name products { nodes { name } } } } } ``` If we were to fetch 50 brands, we would end up making 50 database requests to retrieve the products for each brand in view. To optimize this, we could build a DataLoader following the same approach as we did for fetching a brand by ID. However, in this case, we need to batch, paginate, and slice the dataset using cursor pagination efficiently. C# ``` [DataLoader] public static async Task>> GetProductsByBrandAsync( IReadOnlyList brandIds, PagingArguments pagingArgs, QueryContext queryContext, CatalogContext context, CancellationToken cancellationToken) { return await context.Products .Where(t => brandIds.Contains(t.BrandId)) .With(queryContext) .ToBatchPageAsync(t => t.BrandId, pagingArgs, cancellationToken); } ``` Again, we pass along the `QueryContext`. However, instead of using `ToPageAsync`, we use ToBatchPageAsync, which batches data fetching and slicing into a single database request. C# ``` public async Task> Handle( GetProductsByBrandsQuery request, CancellationToken cancellationToken = default) => await productsByBrand .With(request.PagingArguments, request.Query) .LoadAsync(brandId, cancellationToken) ?? Page.Empty; ``` The beauty of this approach is that the complexity in my business layer remains unchanged. Similarly, in my resolver, the complexity is the same as implementing a top-level query. Overall, while I gain full control over what happens in each layer, the complexity within each layer remains constant. ![GreenDonut Packages By Layer](https://chillicream.com/images/blog/2025-02-01-hot-chocolate-15/greendonut-2.png) ## DataLoader Branching But wait — if we use a DataLoader and fetch by key and path in different query contexts, wouldn’t that lead to conflicting data fetches? It would if we were using the same DataLoader. However, DataLoaders are immutable. When we apply a wither method, we are effectively branching the DataLoader as we are effectively changing what we fetch and how we fetch it. Essentially, we create a unique key based on the state passed into the DataLoader, which generates a new branch. If another resolver with the same state requests a different entity key, we look up the corresponding branch of the DataLoader and delegate the request to the correct instance. You can even branch further on top of the Wither method. For example, if you always need to ensure that data is queried within a specific customer context, you could add an additional where clause on top of our DataLoader. This would create a new branch of our DataLoader ensuring that only this `Handle` method will restrict data fetching. C# ``` public async Task> Handle( GetProductsByBrandsQuery request, CancellationToken cancellationToken = default) => await productsByBrand .With(request.PagingArguments, request.Query) .Where(t => t.CustomerId == session.CustomerId) .LoadAsync(brandId, cancellationToken) ?? Page.Empty; ``` We distinguish between ordering, where clauses, and selections. Selections, for instance, can be merged within the same instance, resulting in slight overfetching but still retrieving the correct data within a single request. C# ``` public async Task> Handle( GetProductsByBrandsQuery request, CancellationToken cancellationToken = default) => await productsByBrand .With(request.PagingArguments, request.Query) .Where(t => t.CustomerId == session.CustomerId) .Include(t => t.SomeInternalId) .LoadAsync(brandId, cancellationToken) ?? Page.Empty; ``` In this case, the Include is merged into the same DataLoader branch. The new DataLoader allows you to define custom branching rules and introduce custom state, enabling you to extend the base functionality as needed. ## Conclusion While we hadn’t originally planned to release this at this time, I believe it brings a fantastic set of additions and will empower you to build layered and clean GraphQL services better than ever. With the new APIs — and I’ve only shown a fraction of them — you can now create well-structured GraphQL services without compromising performance or abstraction. At the same time, you can keep complexity low and productivity high. Try it out and let us know what you think! We’re always looking to improve Hot Chocolate based on your feedback. I will follow up the blog post with a couple of more detailed YouTube episodes on how to use these new features. In the meantime, we’re hard at work on the next major Fusion update, which is going to be huge. I’d love to share some tidbits with you, but I don’t want to spoil the surprise! 😃 Join our community on [Slack](https://slack.chillicream.com/) or follow us on Twitter — we’re always happy to help and chat with you! ## You might also like [View all](https://chillicream.com/blog) --- # Newsletter October > Hot Chocolate 14 is released, BCP is now Nitro and there is a new DDD Workshop Canonical source: https://chillicream.com/blog/2024-10-30-newsletter-october ![](https://chillicream.com/images/blog/2024-10-30-newsletter-october/header.png) [![Pascal Senn's avatar](https://chillicream.com/_optimized/images/remote/3ebc6212aa4091005799d46f902a92e2eb7e77cd966797ecb5f4eaeab3bb26fe.png)Pascal Senn](https://chillicream.com/authors/pascal-senn)2024-10-305 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2024-10-30-newsletter-october&text=Newsletter+October) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2024-10-30-newsletter-october) - [bananacakepop](https://chillicream.com/blog/tags/bananacakepop) - [graphql](https://chillicream.com/blog/tags/graphql) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [workshops](https://chillicream.com/blog/tags/workshops) - [cloud](https://chillicream.com/blog/tags/cloud) - [release](https://chillicream.com/blog/tags/release) Dear ChilliCream Community, We hope this message finds you well and as excited as we are about the future of GraphQL development! We've been hard at work, and we have some big news to share with you. ## Hot Chocolate 14 is Here! After over a year of dedication and over 500 commits from more than 50 contributors, we're thrilled to announce the release of **Hot Chocolate 14**. This is our biggest release since version 10.5 and marks a significant shift in how you build GraphQL servers. **What's New:** Hot Chocolate 14 brings a host of new features and improvements designed to make your development experience more intuitive, efficient, and secure. - **Ease of Use and Simplified Dependency Injection:** We've streamlined dependency injection, allowing you to inject services directly into your resolvers without extra configuration. This leads to cleaner, more maintainable code. No more `[Service]` attribute needed! - **Enhanced Query Inspection:** Easily check which fields are being requested within a resolver without complex syntax tree traversals. Optimize data fetching based on actual query needs with our new fluent selector inspection API. - **Improved Pagination:** Implementing pagination is now more straightforward, whether you're building layered applications or using `DbContext` in your resolvers. We've introduced new primitives like `Page` and `PagingArguments`, along with keyset pagination support for Entity Framework Core, enhancing performance and stability. - **Advanced DataLoader Capabilities:** DataLoader now supports stateful operations, enabling you to batch multiple nested paging requests into a single database query. This optimizes performance, especially for complex queries with nested pagination. Projections in DataLoaders are now possible! - **Source Generators for Resolvers:** We've expanded our use of source-generated code, allowing for the generation of resolvers and improving build-time feedback. This feature is opt-in and works with our new type extension API, combining the power of the implementation-first approach with the code-first fluent API. Checkout `[ObjectType]`! - **Enhanced Relay Support:** Hot Chocolate 14 offers better integration with Relay, including support for custom data on edges, control over the shape of connection types, and updated node ID serializers for more efficient handling. - **Security Enhancements:** We've integrated the IBM cost specification directly into the core of Hot Chocolate. This means that even if you don't configure any security-related options, your GraphQL server is more secure by default. The cost analysis helps prevent expensive operations from overwhelming your server. - **Optimized Transport Layer:** We've adopted the latest changes from the GraphQL over HTTP specification and reimplemented our persisted operation pipeline. This introduces end-to-end traceability and allows for more efficient operation execution with features like semantic routes. - **Improved Fusion Support:** While focusing on stability, we've made it easier to configure Fusion with new attributes and improved error handling from source schemas to the composite schema. **Learn More:** For a detailed overview of these (and more) new features and improvements, please read our in-depth blog post: 🔗 **[Sneak Peek at Hot Chocolate 14](https://chillicream.com/blog/2024-08-30-hot-chocolate-14)** --- ## Introducing Nitro: A Unified GraphQL Ecosystem We're also excited to unveil **Nitro**, the new name that brings together our suite of GraphQL tools under one unified ecosystem. Inspired by the smooth yet powerful kick of nitrogen-infused drinks, Nitro embodies the speed, efficiency, and energy we aim to provide in your development workflow. **Why Nitro?** As our products evolved, we wanted a name that reflects our commitment to delivering a seamless and powerful GraphQL experience. By **rebranding Banana Cake Pop and Barista to Nitro**, we're simplifying our ecosystem to make it more cohesive and easier to navigate. **What's Included in Nitro:** - **Nitro App (Formerly BananaCakePop):** Your all-in-one tool for developing, testing, and optimizing GraphQL APIs. - **Get the Nitro App:** [Download Here](https://get-nitro.chillicream.com/) - **Try Nitro Cloud:** [Launch Now](https://nitro.chillicream.com/) - **Nitro CLI (Formerly Barista):** Manage APIs, publish schema versions, and deploy clients—all from your command line. - **Nitro Server (Formerly Banana Cake Pop Services):** The backbone of Nitro, providing essential backend services for managing your GraphQL schemas and monitoring API performance. **Migration Information:** For details on migrating to Nitro and the changes to our NuGet and NPM packages, please refer to our blog post: 🔗 **[Introducing Nitro: A New Name, A Unified GraphQL Ecosystem](https://chillicream.com/blog/2024-10-07-introducing-nitro)** ### Full Open Telemetry While our telemetry integration was previously focused only on GraphQL operations, we're excited to announce that we're expanding our telemetry capabilities to include full OpenTelemetry support. This means you can now also monitor your REST APIs, gRPC services, background jobs, and more—all within the same dashboard. ![image](https://chillicream.com/images/blog/2024-10-30-newsletter-october/img1.png) ## Get Hands-On with DDD and GraphQL in Our One-Day Workshop Join us for a focused, one-day workshop on Domain-Driven Design with GraphQL, where we’ll guide you through practical DDD concepts and their implementation in .NET 9, ASP.NET Core 9, Aspire, Hot Chocolate and Fusion. Learn how CQRS and Domain Events work with GraphQL, and how to manage complex application domains with clean architecture practices. This session is ideal for developers who want to deepen their understanding of DDD principles and see them in action in a GraphQL environment. Claim your **30% discount NOW**: 👉 [Register for the Workshop](https://www.eventbrite.com/e/enterprise-graphql-with-ddd-cqrs-and-clean-architecture-tickets-1057250156679) ## Share Your Success Story with Us! Do you love using **Hot Chocolate**, **Fusion**, or **Nitro**? Have you built something amazing that you'd like the world to know about? **We want to hear from you!** We're on the lookout for testimonials and case studies to feature on our website. Your experiences can inspire others and showcase the real-world impact of our tools. **Interested in Sharing?** 👉 **[Click here to share your story with us!](https://tally.so/r/3j7R4E)** --- ## Thank You! ❤️ A huge congratulations and thank you to our incredible team and community contributors who poured countless hours into making these releases possible. We're eager to continue pushing the boundaries of what's possible with GraphQL, and we couldn't do it without your support. Thank you for being a vital part of the ChilliCream community. Let's build the future of GraphQL together! Warm regards, The ChilliCream Team ## You might also like [View all](https://chillicream.com/blog) --- # Introducing Nitro: A New Name, A Unified GraphQL Ecosystem > Meet Nitro, the unified name for the former Banana Cake Pop app and services and Barista CLI, with the package and local-data migration details. Canonical source: https://chillicream.com/blog/2024-10-07-introducing-nitro ![](https://chillicream.com/images/blog/2024-10-07-introducing-nitro/introducing-nitro.png) [![Rafael Staib's avatar](https://chillicream.com/_optimized/images/remote/47f10f3229fb2cfedca9993792f328fe15c7670ac3ed9d4e777b11374b3e4907.png)Rafael Staib](https://chillicream.com/authors/rafael-staib)2024-10-073 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2024-10-07-introducing-nitro&text=Introducing+Nitro%3A+A+New+Name%2C+A+Unified+GraphQL+Ecosystem) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2024-10-07-introducing-nitro) - [graphql](https://chillicream.com/blog/tags/graphql) - [nitro](https://chillicream.com/blog/tags/nitro) - [products](https://chillicream.com/blog/tags/products) - [telemetry](https://chillicream.com/blog/tags/telemetry) At ChilliCream, we’ve always had a playful and creative approach to naming our products. From **HotChocolate** to **StrawberryShake**, our GraphQL tools have been inspired by delicious drinks that keep things fresh and exciting. Today, we’re taking that tradition to the next level with **Nitro** – the newest addition to our product family that unifies and simplifies how you build, manage, and scale your GraphQL APIs. ## Why Nitro? As we continue to evolve our offerings, we wanted to create a name that embodies the energy, speed, and efficiency of our tools. Inspired by nitrogen-infused drinks that deliver a smooth yet powerful kick, **Nitro** felt like the perfect fit. Just like those drinks, our Nitro tools are designed to give your GraphQL development process a boost – offering a fast, streamlined, and unified experience. By renaming **Banana Cake Pop** and **Barista** to **Nitro**, we’re simplifying our ecosystem and making it easier for developers to navigate and interact with our suite of products. Nitro brings everything under one umbrella, making your workflow more cohesive and efficient. ## What’s in the Nitro Ecosystem? - **Nitro App (Formerly Banana Cake Pop)** The Nitro App is your go-to tool for developing, testing, and optimizing GraphQL APIs. Whether you’re inspecting queries, visualizing schemas, or collaborating with your team, Nitro App provides the power and precision needed to supercharge your development workflows. Get the Nitro App at [get-nitro.chillicream.com.](https://get-nitro.chillicream.com/) or try the Cloud version at [nitro.chillicream.com](https://nitro.chillicream.com/). - **Nitro CLI (Formerly Barista)** Nitro CLI offers full control from the command line. It’s perfect for managing APIs, publishing new schema versions, and deploying clients with ease. Whether automating tasks or handling complex GraphQL operations, Nitro CLI simplifies your workflow, allowing you to focus on what matters most – building great APIs. - **Nitro Server (Formerly Banana Cake Pop Services)** Nitro Server is the backbone of the Nitro ecosystem. It provides essential backend services for managing your GraphQL schemas, monitoring API performance, and ensuring smooth operations across your gateways and services. With Nitro Server, you can confidently manage your GraphQL infrastructure, ensuring your clients and APIs remain stable, secure, and scalable as your business grows. ## Local Data Migration for Nitro App Before upgrading from Banana Cake Pop to the Nitro App, please note that **local data will not be automatically migrated**. To avoid losing any of your documents, make sure to explicitly save them before signing in and syncing your data. Only saved documents will be synced to the cloud, ensuring they are safely stored and accessible when you switch to the Nitro App. Unsaved documents will not be included in the sync. ## New NuGet and NPM Packages As part of our transition to Nitro, we’ve renamed our NuGet and NPM packages to create a more cohesive experience: ### NuGet Packages | Old Name | New Name | | -------------------------- | --------------------- | | Banana Cake Pop.Middleware | ChilliCream.Nitro.App | | Banana Cake Pop.Services | ChilliCream.Nitro | | Barista | ChilliCream.Nitro.CLI | ### NPM Packages | Old Name | New Name | | ------------------------------------------------ | ---------------------------------------- | | @chillicream/bananacakepop-graphql-ide | @chillicream/nitro-embedded | | @chillicream/bananacakepop-express-middleware | @chillicream/nitro-express-middleware | | @chillicream/bananacakepop-server-adapter-plugin | @chillicream/nitro-server-adapter-plugin | This unified naming approach isn’t just cosmetic – it simplifies how you interact with our tools, making it easier to find, install, and use everything Nitro has to offer. ## Why This Matters The Nitro name represents more than just speed and energy. It symbolizes a unified, streamlined ecosystem designed to make your GraphQL development experience seamless. Whether you’re using the desktop app, cloud app, or the CLI tools, Nitro provides all the power and flexibility you need, without the complexity of managing disconnected tools. ## Ready to Experience Nitro? We’re incredibly excited about this new chapter and can’t wait for you to try Nitro. Visit [nitro.chillicream.com](https://nitro.chillicream.com/) to experience the Nitro Cloud App, or download the Nitro Desktop App at [get-nitro.chillicream.com.](https://get-nitro.chillicream.com/). Let’s build the future of GraphQL together, with Nitro powering the way forward. ## You might also like [View all](https://chillicream.com/blog) --- # What's new for Hot Chocolate 14 > Hot Chocolate 14 introduces simpler dependency injection, query inspection, source-generated DataLoaders, pagination improvements, and stronger security. Canonical source: https://chillicream.com/blog/2024-08-30-hot-chocolate-14 ![](https://chillicream.com/images/blog/2024-08-30-hot-chocolate-14/hot-chocolate-14.png) [![Michael Staib's avatar](https://chillicream.com/_optimized/images/remote/2bb0dd6e1b9347a1d3732300ea752f39f1ac4f1f76587433ca3a407245b31786.jpg)Michael Staib](https://chillicream.com/authors/michael-staib)2024-08-3028 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2024-08-30-hot-chocolate-14&text=What%27s+new+for+Hot+Chocolate+14) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2024-08-30-hot-chocolate-14) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [graphql](https://chillicream.com/blog/tags/graphql) - [dotnet](https://chillicream.com/blog/tags/dotnet) - [aspnetcore](https://chillicream.com/blog/tags/aspnetcore) We are almost ready to release a new major version of Hot Chocolate, and with it come many new exciting features. We have been working on this release for quite some time, and we are thrilled to share it with you. In this blog post, we will give you a sneak peek at what you can expect with Hot Chocolate 14. I will be focusing mainly on the Hot Chocolate server, but we have also been busy working on Hot Chocolate Fusion and the Composite Schema Specification. We will be releasing more information on these projects in the coming weeks. ## Ease of Use We have focused on making Hot Chocolate easier to use and more intuitive. To achieve this, we have added many new features that will simplify your workflow. This will be apparent right from the start when you begin using Hot Chocolate 14\. One major area where you can see this improvement is in dependency injection. Hot Chocolate 13 was incredibly flexible in this area, allowing you to specify which services are multi-thread capable, which services are pooled resources, or which services must be synchronized. While this was a powerful feature, it could be somewhat complex to use, especially for newcomers to our platform. You either ended up with lengthy configuration code that essentially re-declared all services, or you ended up with very cluttered resolvers. With Hot Chocolate 14, we have simplified this process by putting dependency injection on auto-pilot. Now, when you write your resolvers, you can simply inject services without the need to explicitly tell Hot Chocolate that they are services or what kind of services they are. C# ``` public static IQueryable GetSessions( ApplicationDbContext context) => context.Sessions.OrderBy(s => s.Title); ``` This leads to dramatically clearer code that is more understandable and easier to maintain. For instance, the resolver above injects the `ApplicationDbContext`. There is no need to tell Hot Chocolate that this is a service or what characteristics this service has; it will just work. This is because we have simplified the way Hot Chocolate interacts with the dependency injection system. In GraphQL, we essentially have two execution algorithms. The first, used for queries, allows for parallelization to optimize data fetching. This enables us to enqueue data fetching requests transparently and execute them in parallel. The second algorithm, used for mutations, is a sequential algorithm that executes one mutation after another. So, how is this related to DI? In Hot Chocolate 14, if we have an async resolver that requires services from the DI container, we create a service scope around it, ensuring that the services you use in the resolver are not used concurrently used by other resolvers. Since query resolvers are, by specification, defined as side-effect-free, this is an excellent default behavior where you as the developer can just focus on writing code without concerning yourself with concurrency between resolver instances. For mutations, the situation is different, as mutations inherently cause side effects. For instance, you might want to use a shared DbContext between two mutations. When executing a mutation Hot Chocolate will use the default request scope as it's guaranteed by the execution algorithm that there will only ever be a single mutation resolver executed at the same time for a request. While the new default execution behavior is much more opinionated, it leads to a dramatically easier experience when implementing resolvers. However, we recognize that there are reasons you may want to use the request scope everywhere. That's why you can change the default configuration with the default schema options. C# ``` builder .AddGraphQL() .AddTypes() .ModifyOptions(o => { o.DefaultQueryDependencyInjectionScope = DependencyInjectionScope.Resolver; o.DefaultMutationDependencyInjectionScope = DependencyInjectionScope.Request; }); ``` Also, you can override the defaults configured in the schema options, on a per resolver basis. C# ``` [UseRequestScope] [UsePaging] public static async Task> GetBrandsAsync( PagingArguments pagingArguments, BrandService brandService, CancellationToken cancellationToken) => await brandService .GetBrandsAsync(pagingArguments, cancellationToken) .ToConnectionAsync(); ``` > We have applied the same DI handling to source generated DataLoader which by default will now use an explicit service scope for each DataLoader fetch. ## Query Inspection Another area where we have made significant improvements is with query inspections. With Hot Chocolate 14, it’s now incredibly simple to check which fields are being requested within the resolver without the need for complex syntax tree traversals. You can now formulate a pattern with the GraphQL selection syntax and let the executor inject a simple boolean that tells you if your pattern matched the user query. C# ``` public sealed class BrandService(CatalogContext context) { public async Task GetBrandAsync( int id, [IsSelected("products { details }")] bool includeProductDetails, CancellationToken ct = default) { var query = context.Brands .AsNoTracking() .OrderBy(t => t.Name) .ThenBy(t => t.Id); if (includeProductDetails) { query = query.Include(t => t.Products.Details); } return await query.FirstOrDefaultAsync(ct); } } ``` The patterns also support inline fragments to match abstract types. GraphQL ``` products { ... on Book { isbn } } ``` However, even with these complex patterns, it can be beneficial to write your own traversal logic without dealing with complex trees. For this, you can now simply inject the resolver context and use our fluent selector inspection API. C# ``` public sealed class BrandService(CatalogContext context) { public async Task GetBrandAsync( int id, IResolverContext context, CancellationToken ct = default) { var query = context.Brands .AsNoTracking() .OrderBy(t => t.Name) .ThenBy(t => t.Id); if (context.Select("products").IsSelected(details)) { query = query.Include(t => t.Products.Details); } return await query.FirstOrDefaultAsync(ct); } } ``` If you want to go all in and have the full power of the operation executor, you can still inject `ISelection` and traverse the compiled operation tree. ## Pagination Pagination is a common requirement in GraphQL APIs, and Hot Chocolate 14 makes it easier than ever to implement, no matter if you are building layered applications or using `DbContext` right in your resolvers. For layered application patterns like DDD, CQRS, or Clean Architecture, we have built a brand new paging API that is completely separate from the Hot Chocolate GraphQL core. When building layered applications, pagination should be a business concern and should be handled in your repository or service layer. Doing so brings some unique concerns, like how the abstraction of a page looks. For this, we have introduced a couple of new primitives like `Page`, `PagingArguments`, and others that allow you to build your own paging API that fits your needs and interfaces well with GraphQL and REST. We have also implemented keyset pagination for Entity Framework Core, which you can use in your infrastructure layer. The Entity Framework team is planning to have, at some point, a paging API for keyset pagination natively integrated into EF Core ([Holistic end-to-end pagination feature](https://github.com/dotnet/efcore/issues/33160)). Until then, you can use our API to get the best performance out of your EF Core queries when using pagination with a layered application. C# ``` public sealed class BrandService(CatalogContext context) { public async Task> GetBrandsAsync( PagingArguments args, CancellationToken ct = default) => await context.Brands .AsNoTracking() .OrderBy(t => t.Name) .ThenBy(t => t.Id) .ToPageAsync(args, ct); } ``` We are focusing on keyset pagination because it’s the better way to do pagination, as performance is constant for each page accessed, as opposed to a linearly growing performance impact with offset pagination. Apart from the better performance, keyset pagination also allows for stable pagination results even if the underlying data changes. We also worked hard to allow for pagination in your DataLoader. In GraphQL, where nested pagination is a common requirement, having the capability to batch multiple nested paging requests into one database query is essential. Let’s assume we have the following GraphQL query and we are using a layered architecture approach. GraphQL ``` query GetBrands { brands(first: 10) { nodes { id name products(first: 10) { nodes { id name } } } } } ``` Let's assume we have the following two resolvers for the above query, fetching the brands and the products. C# ``` [UsePaging] public static async Task> GetBrandsAsync( PagingArguments pagingArguments, BrandService brandService, CancellationToken cancellationToken) => await brandService .GetBrandsAsync(pagingArguments, cancellationToken) .ToConnectionAsync(); [UsePaging] public static async Task> GetProductsAsync( [Parent] Brand brand, PagingArguments pagingArguments, ProductService productService, CancellationToken cancellationToken) => await productService .GetProductsByBrandAsync(brand.Id, pagingArguments, cancellationToken) .ToConnectionAsync(); ``` With the above resolvers, the execution engine would first call the `BrandService`, and then for each `Brand`, it would call the `ProductService` to get the products per brand. This would lead to an N+1 query problem within our GraphQL server. To solve this, we can use a DataLoader within our `ProductService` and batch the product requests. To enable this, we have worked extensively on DataLoader and now support stateful DataLoader. This means we can pass on state to a DataLoader separate from the keys. If we were to peek into the `ProductService`, we would see something like this: C# ``` public async Task> GetProductsByBrandAsync( int brandId, PagingArguments args, ProductsByBrandIdDataLoader productsByBrandId, CancellationToken ct = default) => await productsByBrandId .WithPagingArguments(args) .LoadAsync(brandId, ct); ``` Our DataLoader in this case would look like the following: C# ``` public sealed class ProductDataLoader { [DataLoader] public static async Task>> GetProductsByBrandIdAsync( IReadOnlyList keys, PagingArguments pagingArguments, CatalogContext context, CancellationToken ct) => await context.Products .AsNoTracking() .Where(p => keys.Contains(p.BrandId)) .OrderBy(p => p.Name).ThenBy(p => p.Id) .ToBatchPageAsync(t => t.BrandId, pagingArguments, ct); } ``` The `ToBatchPageAsync` extension method will rewrite the paging query so that each `brandId` will be a separate page, allowing us to make one database call to get, in this case, 10 products per brand for 10 brands. An important aspect of keyset pagination is maintaining a stable order, which requires a unique key. In the above case, we order by `Name` and then chain the primary key `Id` in at the end. This ensures that the order remains stable even if the `Name` is not unique. > If you want to read more about keyset pagination, you can do so [here](https://use-the-index-luke.com/no-offset). We have brought the same capabilities to non-layered applications, where you now have a new paging provider for EF Core that allows for transparent keyset pagination. So if you are doing something like this in your resolver: C# ``` [UsePaging] public static async IQueryable GetBrands( CatalogContext context) => context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id); ``` By default, Hot Chocolate would emulate cursor pagination by using `skip/take` underneath. However, as I mentioned, we now have a new keyset pagination provider for EF Core that you can opt into. It's not the default, as it is not compatible with SQLite for instance. C# ``` builder.Services .AddGraphQLServer() ... .AddDbContextCursorPagingProvider(); ``` But what about user-controlled sorting? The above example would fall apart when using `[UseSorting]`, as we could not guarantee that the order is stable. To address this, we have added a couple of helpers to the `ISortingContext` that allow you to manipulate the sorting expression. C# ``` [UsePaging] [UseSorting] public static async IQueryable GetBrands( CatalogContext context, ISortingContext sorting) { // this signals that the expression was not handled within the resolver // and the sorting middleware should take over. sorting.Handled(false); sorting.OnAfterSortingApplied>( static (sortingApplied, query) => { if (sortingApplied && query is IOrderedQueryable ordered) { return ordered.ThenBy(b => b.Id); } return query.OrderBy(b => b.Id); }); return context.Brands; } ``` With the `ISortingContext`, we now have a hook that is executed after the user sorting has been applied. This allows us to append a stable order to the user sorting. Typically, this could be generalized and moved into a user extension method to make the resolver look cleaner. C# ``` [UsePaging] [UseSorting] public static async IQueryable GetBrands( CatalogContext context, ISortingContext sorting) { sorting.AppendStableOrder(b => b.Id); return context.Brands; } ``` You could even go further and bake this into a custom middleware. C# ``` [UsePaging] [UseCustomSorting] public static async IQueryable GetBrands( CatalogContext context) => context.Products; ``` With the new paging providers, we now also inline the total count into the database query that slices the page, meaning you have a single call to the database. The paging middleware will inspect what data is actually needed and either fetch the page and the total count in one database query, just the page if the total count is not needed, or just the total count if the page is not needed. All of this is built on top of the new `IsSelected` query inspection API. ## DataLoader Let's talk about DataLoader. As we already touched on how DataLoader is now more flexible with pagination, what's underneath all of this is the new state that can be associated with DataLoader. Since DataLoader can be accessed from multiple threads concurrently and also be dispatched at multiple points during execution, you have unreliable state that can be used when it's available but should not cause the DataLoader to fail. However, you can also have state that is used to branch a DataLoader, where the state is guaranteed within that branch. Let me give you some examples. In the following example, we are fetching brands for ID 1 and 2\. We also provide some state when we ask for brand 2\. The state is guaranteed to be there when I fetch the second brand, but it could be there for the first brand — this all depends on the dispatcher in this case. C# ``` var task1 = brandById.LoadAsync(1); var task2 = brandById.SetState("some-state", "some-value").LoadAsync(2); Task.WaitAll(task1, task2); ``` However, in some cases like paging, we want the state to be guaranteed. This is where branching comes in. We can branch a DataLoader, and into this branch, we pass in some data that represents the context. C# ``` var branch = brandById .Branch("SomeKey") .SetState("some-state", "some-value"); var task1 = branch.LoadAsync(1); var task2 = branch.LoadAsync(2); Task.WaitAll(task1, task2); ``` When we look at paging, for instance, we use the paging arguments to create a branch key. So, whenever you pass in the same paging arguments, you will get the same branch. This allows us to batch the paging requests for the same paging arguments. C# ``` productsByBrandId.WithPagingArguments(args).LoadAsync(brandId, ct); ``` We also use the same state mechanism for DataLoader with projections. C# ``` public class Query { public async Task GetBrandByIdAsync( int id, ISelection selection, BrandByIdDataLoader brandById, CancellationToken cancellationToken) => await brandById .Select(selection) .LoadAsync(id, cancellationToken); } ``` You can pass an `ISelection` into the DataLoader. Any selection that is structurally equivalent will point to the same DataLoader branch and be batched together. We can even chain other things to that branched state like properties we want include even if they were not requested by the user and even if they are not part of the schema. C# ``` public class Query { public async Task GetBrandByIdAsync( int id, ISelection selection, BrandByIdDataLoader brandById, CancellationToken cancellationToken) => await brandById .Select(selection) .Include(b => b.Products) .LoadAsync(id, cancellationToken); } ``` From the DataLoader side, we can inject these selections and apply them to our queryable. C# ``` internal static class BrandDataLoader { [DataLoader] public static async Task> GetBrandByIdAsync( IReadOnlyList ids, CatalogContext context, ISelectorBuilder selector, CancellationToken ct) => await context.Brands .AsNoTracking() .Select(selector, key: b => b.Id) .ToDictionaryAsync(b => b.Id, ct); } ``` When using our DataLoader projections, we are utilizing a new projection engine that is separate from `HotChocolate.Data`, and we are using this to redefine what projections are in Hot Chocolate. This is why `IsProjectedAttribute` is not supported by DataLoader projections. Instead, we have modified the `ParentAttribute` to specify requirements. C# ``` public static class ProductExtensions { [UsePaging] public static async Task> GetProductsAsync( [Parent(nameof(Brand.Id))] Brand brand, PagingArguments pagingArguments, ProductService productService, CancellationToken cancellationToken) => await productService .GetProductsByBrandAsync(brand.Id, pagingArguments, cancellationToken) .ToConnectionAsync(); } ``` The optional argument on the `ParentAttribute` specifies a selection set that describes the requirements for the parent object. In the example above, it defines that the brand ID is required. However, you could also specify that you need the IDs of the products as well, such as `Id Products { Id }`. The parent that is injected is guaranteed to have the properties filled with the required data. We evaluate this string representing the requirement in the source generator, and if it does not match the object structure, it would yield a compile-time error. The whole DataLoader projections engine is marked as experimental, and we are looking for feedback. Apart from this, we have invested a lot into `GreenDonut` to ensure that you can use the source-generated DataLoader without any dependencies on `HotChocolate`, since DataLoader is ideally used between the business layer and the data layer, and is transparent to the REST or GraphQL layer. With Hot Chocolate 14, you can now add the `HotChocolate.Types.Analyzers` package and the `GreenDonut` package to your data layer. The analyzers package is just the source generator and will not be a dependency of your own package. We will generate the DataLoader code plus the dependency injection code for registering your DataLoader. You simply need to add the `DataLoaderModuleAttribute` to your project like the following: C# ``` [assembly: DataLoaderModule("CatalogDataLoader")] ``` Lastly, on the topic of DataLoader, we have made the DataLoader cache observable, allowing you to share entities between DataLoader for even more efficient caching. Let's for instance say that we have two Brand DataLoader, one fetches the entity by ID and the other one by name. How can we make sure that we do not fetch the same entity twice just because we have different keys? C# ``` internal static class BrandDataLoader { [DataLoader] public static async Task> GetBrandByIdAsync( IReadOnlyList ids, CatalogContext context, CancellationToken ct) => await context.Brands .AsNoTracking() .Where(t => ids.Contains(t.Id)) .ToDictionaryAsync(t => t.Id, ct); [DataLoader] public static async Task> GetBrandByNameAsync( IReadOnlyList names, CatalogContext context, CancellationToken ct) => await context.Brands .AsNoTracking() .Where(t => names.Contains(t.Name)) .ToDictionaryAsync(t => t.Name, ct); } ``` This can be easily done by writing two observer methods that create a new cache lookup for the same object. So, at the moment one of the DataLoader instances is instantiated, it will subscribe for `Brand` entities on the cache and create lookups. After that, the DataLoader will receive real-time notifications if any other DataLoader has fetched a `Brand` entity and will be able to use the cached entity. C# ``` internal static class BrandDataLoader { [DataLoader(Lookups = [nameof(CreateBrandByIdLookup)])] public static async Task> GetBrandByIdAsync( IReadOnlyList ids, CatalogContext context, CancellationToken ct) => await context.Brands .AsNoTracking() .Where(t => ids.Contains(t.Id)) .ToDictionaryAsync(t => t.Id, ct); private static int CreateBrandByIdLookup(Brand brand) => brand.Id; [DataLoader(Lookups = [nameof(CreateBrandByNameLookup)])] public static async Task> GetBrandByNameAsync( IReadOnlyList names, CatalogContext context, CancellationToken ct) => await context.Brands .AsNoTracking() .Where(t => names.Contains(t.Name)) .ToDictionaryAsync(t => t.Name, ct); private static string CreateBrandByNameLookup(Brand brand) => brand.Name; } ``` Where this really shines is with optional includes. For instance, when using the `BrandByIdDataLoader`, we could include the products in one request because we know that we will need them later. C# ``` public sealed class BrandService(CatalogContext context) { public async Task> GetBrandByIdAsync( PagingArguments args, BrandByIdDataLoader brandById, CancellationToken ct = default) => await brandById .AsNoTracking() .Include(b => b.Products) .ToPageAsync(args, ct); } ``` In this case, we can subscribe to `Brand` entities on the cache and check if they have the products list populated. If they do, we can create lookups for the products. C# ``` internal static class ProductDataLoader { [DataLoader(Lookups = [nameof(CreateProductByIdLookups)])] public static async Task> GetProductByIdAsync( => ... private static IEnumerable> CreateProductByIdLookups(Brand brand) => brand.Products.Select(p => new KeyValuePair(p.Id, p)); } ``` ## Source Generators With Hot Chocolate 14, we have started to expand our use of source-generated code. We have already used source generators in the past to automatically register types or generate the boilerplate code for DataLoader. With Hot Chocolate 14, we are now beginning to use source generators to generate resolvers. This feature is opt-in and, at the moment, only available for our new type extension API. The new `ObjectTypeAttribute` will, over the next few versions, replace the `ExtendObjectType` attribute. The new attribute works only in combination with the source generator and combines the power of the implementation-first approach with the code-first fluent API. C# ``` [ObjectType] public static partial class BrandNode { static partial void Configure(IObjectTypeDescriptor descriptor) { descriptor.Ignore(t => t.Subscriptions); } [UsePaging] public static async Task> GetProductsAsync( [Parent] Brand brand, PagingArguments pagingArguments, ProductService productService, CancellationToken cancellationToken) => await productService .GetProductsByBrandAsync(brand.Id, pagingArguments, cancellationToken) .ToConnectionAsync(); } ``` The beauty of the source generator is that, in contrast to expression compilation, the results are fully inspectable, and we can guide you by issuing compile-time warnings and errors. The source generator output can be viewed within your IDE and is debuggable. ![Rider - Source Generators](https://chillicream.com/images/blog/2024-08-30-hot-chocolate-14/screen-source-generator-1.png) With the new type extension API, we also allow for new ways to declare root fields and colocate queries, mutations, and subscriptions. C# ``` public static class Operations { [Query] public static async Task> GetBrandsAsync( BrandService brandService, PagingArguments pagingArgs, CancellationToken ct) => await brandService.GetBrandsAsync(pagingArgs, ct); [Mutation] public static async Task CreateBrand( CreateBrandInput input, BrandService brandService, CancellationToken ct) => await brandService.CreateBrandAsync(input, ct); } ``` Operation fields can even be colocated into extension types. C# ``` [ObjectType] public static partial class BrandNode { static partial void Configure(IObjectTypeDescriptor descriptor) { descriptor.Ignore(t => t.Subscriptions); } [UsePaging] public static async Task> GetProductsAsync( [Parent] Brand brand, PagingArguments pagingArguments, ProductService productService, CancellationToken cancellationToken) => await productService .GetProductsByBrandAsync(brand.Id, pagingArguments, cancellationToken) .ToConnectionAsync(); [Query] public static async Task> GetBrandsAsync( BrandService brandService, PagingArguments pagingArgs, CancellationToken ct) => await brandService.GetBrandsAsync(pagingArgs, ct); [Mutation] public static async Task CreateBrand( CreateBrandInput input, BrandService brandService, CancellationToken ct) => await brandService.CreateBrandAsync(input, ct); } ``` This allows for more flexibility in addition to the already established `QueryTypeAttribute`, `MutationTypeAttribute`, and `SubscriptionTypeAttribute`, we now have the new `QueryAttribute`, `MutationAttribute`, and `SubscriptionAttribute`. With the new version of Hot Chocolate, we are also introducing a new type extension API for interfaces, which allows you to add base resolvers for common functionality. Think of this like base classes. C# ``` public interface IEntity { [ID] int Id { get; } } [InterfaceType] public static partial class EntityInterface { public static string SomeField([Parent] IEntity entity) => ...; } ``` The field definition and the resolver are inherited by all implementing object types. So, if an object type does not declare `someField`, it will inherit the resolver from the interface declaration. This is also available through the fluent API, where you now have `Resolve` descriptors on interface fields. ## Relay Support With Hot Chocolate 14, we have also improved our Relay support. We have made it easier to integrate aggregations into the connection type and to add custom data to edges. You now have more control over the shape of the connection type, allowing you to disable the `nodes` field — either to remove it as unnecessary or to replace it with a custom field. C# ``` builder .AddGraphQL() .ModifyPagingOptions(o => o.IncludeNodesField = false) ``` Additionally, we have reworked the node ID serializers to be extendable and support composite identifiers. C# ``` builder .AddGraphQL() .AddNodeIdValueSerializer() ``` The new serializer is more efficient and aligns better with the ID serialization format of other GraphQL servers, where the encoded ID has the following format: `{TypeName}:{Id}`. The new serializer still allows for the old format to be passed in, and you can also register the legacy serializer if you prefer the way we handled it before. Relay remains the best GraphQL client library, with others still trying to catch up by copying Relay concepts. We have always been very vocal about this and use Relay as our first choice in customer projects. Relay is a smart GraphQL client that would benefit immensely from a feature called fragment isolation, where an error in one fragment would not cause the erasure of data from a colocated fragment. The issue here is that the GraphQL specification defines that if a non-null field either returns null or throws an error, the selection set is erased, and the error is propagated upwards. This is a problem for Relay because it would cause the erasure of data from colocated fragments. We have been working on a solution to this problem for years now within the GraphQL foundation, and Hot Chocolate has implemented, in past versions, a proposal called CCN (Client-Controlled-Nullability) where the user could change the nullability of fields. However, there is now a new push to solve this problem in a simpler way with a proposal called true-nullability, which allows smart clients to simply disable null bubbling. In this case, a smart client could create a sort of fragment isolation on the client side by only deleting the fragment affected by an error or non-null violation. With Hot Chocolate 14, we have decided to remove CCN and add a new HTTP header `hc-disable-null-bubbling` that allows you to disable null bubbling for a request. This is a first step towards true-nullability, which would also introduce a new semantic nullability type to the type system. We have prefixed the header with `hc-` to signal that this is a Hot Chocolate-specific header and to avoid collision with the eventual GraphQL specification header. ## Data To make it easier to integrate new data sources into Hot Chocolate, we have made our `IExecutable` abstraction simpler to implement and integrated it more fully into our resolver pipeline. This allows for easier integration of `IQueryable`\-based data drivers, like Entity Framework Core or Cosmos DB, without the need to branch the entire data provider in Hot Chocolate. We have integrated the current Cosmos DB driver with the new `HotChocolate.Data.Cosmos` package and added the new `AsCosmosExecutable` extension method to the `IQueryable` interface. This allows you to easily convert your Cosmos DB queryable into an `IExecutable` that can be used within the default Filter, Sorting, and Projection middleware. C# ``` [QueryType] public static class Query { [UsePaging] [UseFiltering] [UseSorting] public static IExecutable GetBooks(Container container) => container .GetItemLinqQueryable(allowSynchronousQueryExecution: true) .AsCosmosExecutable(); } ``` However, if you are already trying out EF Core 9, you should give the new Cosmos driver within EF Core a second look, as it was rewritten from the ground up and is now on par with the Cosmos DB SDK driver. ## Query Conventions Our mutation conventions were very well received by the community when we introduced them. They help to implement a complex GraphQL pattern around mutations and errors. With mutation conventions, we provided consistency and removed the boilerplate from your code. Ever since we introduced the mutation conventions, we have been asked to provide a similar pattern for queries. While in most cases, I would not recommend resorting to error patterns like those used for mutations — because queries are typically side-effect-free and should be easily queried without concern for complex result types — there are cases where you want to return a domain error as part of your query. For these situations, we recognized the need for a consistent pattern. However, queries are different from mutations, and there is a better pattern than introducing payload-esque types. With our new query conventions, we are embracing a union type as the result type, where the first entry in the union represents success, and the following entries represent errors. GraphQL ``` type Query { book(id: ID!): BookResult } union BookResult = Book | BookNotFound | BookAccessDenied ``` This allows us to query like the following: GraphQL ``` query { book(id: "1") { ... on Book { title } ... on Error { code: __typename message } ... on BookNotFound { bookId } ... on BookAccessDenied { requiredRoles } } } ``` To opt into the query conventions you can chain into the configuration builder `AddQueryConventions`. C# ``` builder .AddGraphQL() .AddTypes() .AddQueryConventions(); ``` This in turn allows you, as with mutation conventions, to annotate errors on your resolver or use the `FieldResult` type. C# ``` public class Query { [Error] [Error] public async Task GetBook( int id, BookService bookService, CancellationToken ct) => await bookService.GetBookAsync(id, ct); } ``` ## Transport Let's talk about the GraphQL transport layer and what has changed with Hot Chocolate 14\. The GraphQL over HTTP spec is now in its final stages, and we have been adopting the latest changes. This means that we no longer return status code 500 when the full result has been erased due to a non-null violation. Instead, we return status code 200 with a JSON body that contains the error information and `data` as null. If you are interested in the spec, you can find the current version [here](https://github.com/graphql/graphql-over-http). We have also reintroduced the error code for not authenticated errors to make it easier for authentication flows. This was something we originally dropped in Hot Chocolate 13, but because many of you struggled with this, we have reintroduced it. Apart from these smaller bits and pieces, we have completely rewritten our persisted operation pipeline, aka trusted document pipeline, to introduce end-to-end traceability across the entire transport layer. We have done this by implementing a feature we call semantic routes. The idea here is that each operation has a unique URI that is derived from the document hash and the operation name. This new persisted operation transport pipeline can be mapped separately, as shown in the following example: C# ``` app.MapGraphQLPersistedOperations(); ``` > In production you could drop the standard GraphQL middleware and only map the persisted operations middleware. By default, we would map the persisted operations to `/graphql/persisted/{documentHash}/{operationName}`, but you can change the root for this path. Now, with this setup, only the variables and extensions are posted to the server. If you are using a query, you can also use a GET request, like the following: C# ``` GET /graphql/persisted/1234/GetBook?variables={id:1} ``` This also makes it much easier to work with CDNs or to reroute certain operations to different servers. For this release, we have also reimplemented our batching transport layer and now support both variable batching and request batching. Variable batching is a new batching proposal we have created for the upcoming Composite Schema Specification to transparently use batching in combination with standard GraphQL queries, instead of relying on special fields like the `_entities` field or the batching fields in Fusion. With variable batching, you can batch multiple sets of variables for the same operation. JSON ``` { "query": "query GetBooks($id: ID!) { book(id: $id) { title } }", "variables": [{ "id": "1" }, { "id": "2" }] } ``` Since a variable batch request has the same structure as a standard GraphQL request, except for the `variables` field, which in this case is a list, we can also batch these within a batch request. JSON ``` [ { "query": "query GetBooks($id: ID!) { book(id: $id) { title } }", "variables": [{ "id": "1" }, { "id": "2" }] }, { "query": "query GetBooks($id: ID!) { book(id: $id) { title } }", "variables": { "id": "3" } } ] ``` This new batching API within your backend allows for new use cases and is a great way to optimize your GraphQL server. ## Security We have seen countless GraphQL servers over the last year as part of our consulting engagements, and in many cases, they were not configured in a secure way. This was not due to a lack of functionality in Hot Chocolate but because engineers transitioning to GraphQL often did not know good security practices for GraphQL. GraphQL, as Facebook created and used it, was built around flexibility during development and persisted operations in production. This means that when Facebook deploys to production, the GraphQL server essentially becomes a REST server — there is no open GraphQL endpoint in production. The GraphQL server is only able to execute trusted operations that were exported from the various frontends into an operation store. In the build pipeline, operations are stripped from the frontend code and replaced with a unique identifier. The stripped operation documents are stored in an operation store. In production, the frontend sends the unique identifier to the GraphQL server instead of a full operation. The GraphQL server only executes operations stored in the operations store and will deny execution of an arbitrary GraphQL requests. This is the BEST way to do GraphQL and provides the best approach for schema evolvability, as operations are centrally known and can be statically analyzed. It also ensures that you know the performance characteristics and impact of operations on your backend. With Banana Cake Pop, you can set up a schema registry and an operation store in less than 5 minutes. Have a look [here](https://chillicream.com/docs/nitro/apis/schema-registry) for more information. However, most new developers are not aware of how to do this or do not understand why they should do it in the first place. Another problem is that there is no easy path from an open GraphQL server to a closed system once you have clients working against your API. With Hot Chocolate 14, we wanted to ensure that your GraphQL server is secure even if you do not configure any security related options, even if you do not know about persisted operations, or even if you explicitly want an open GraphQL server. Going forward, we have built into the core of Hot Chocolate the IBM cost specification to weigh the impact of your requests and to restrict expensive operations right from the start. When you export your schema with Hot Chocolate 14, you will see that we have added cost directives to certain fields. We estimate costs automatically so that you do not have to do this manually. You can override these estimates where necessary. The IBM cost spec has two weights it calculates: type cost, which estimates the objects being produced (essentially the data cost), and field cost, which estimates the computational cost. > With Hot Chocolate 14, we have implemented static analysis, but we will add runtime analysis and result analysis in later updates as well. The static analysis estimates maximums, meaning if you request a list of 50 elements, it will estimate 50 elements, not the actual number of elements that is returned. This ensures that you do not overwhelm your server with a single request and provides a good estimate of what the request could mean for your backend. You can combine the cost analysis scores with rate limiting to ensure that a user stays within cost boundaries over time. C# ``` .UseRequest(next => { var rateLimiter = new SlidingWindowRateLimiter( new SlidingWindowRateLimiterOptions { PermitLimit = 10000, Window = TimeSpan.FromHours(1), SegmentsPerWindow = 6, // 10-minute segments QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, }); return async context => { if (context.ContextData.TryGetValue(WellKnownContextData.CostMetrics, out var value) && value is CostMetrics metrics) { using RateLimitLease lease = await rateLimiter.AcquireAsync( permitCount: (int)metrics.TypeCost, context.RequestAborted); if (!lease.IsAcquired) { context.Result = OperationResultBuilder.New() .AddError(ErrorBuilder.New() .SetMessage("Rate limit exceeded.") .SetCode("RATE_LIMIT_EXCEEDED") .Build()) .SetContextData( WellKnownContextData.HttpStatusCode, HttpStatusCode.TooManyRequests) .Build(); return; } } await next(context); }; }) ``` While you would need a more sophisticated setup in production, such as using Redis to have a distributed rate limiter, this is a good start to ensure that your server is not overwhelmed and as predictable performance characteristics. With the cost spec, you can also estimate a request's impact without executing the actual request by sending the header `GraphQL-Cost:validate`. If you want the request to be executed but still want to see the cost, even if the request is valid, you can send the header `GraphQL-Cost:report`. With the IBM cost spec baked into the core, it's always on, making your GraphQL server more secure and predictable. However, it will also reveal the true cost of your requests, which might be challenging when you migrate. We have also ensured that migrating from an open GraphQL server to trusted documents can now be done in a few minutes by integrating with Banana Cake Pop. Over a period of 30, 60, or 90 days, the GraphQL server will report executed operations to Banana Cake Pop which will store them in the operation store. You can manually decide which queries to exclude. After that period, you can switch to trusted operations, and only operations tracked in the operation store will be allowed from that day forward. Another change we made with Hot Chocolate 14 is around introspection. When we detect a production environment in ASP.NET Core, we will automatically disable introspection and provide a schema file at the route `/graphql?sdl`, which is a one-time computed schema file that will be served as a simple file from your server. The misunderstanding with introspection is often that people think it's about hiding the schema. This actually is not the case since it's quite simple to infer the schema from requests observed in a web application. The problem with introspection is that it can easily produce very large results. When I say large, I mean 200-300 MB, depending on your schema. Most tools will work fine with a schema file, which is much smaller than the introspection result and costs virtually nothing in terms of compute and memory. You can override this behavior as follows: C# ``` builder .AddGraphQLServer() .DisableIntrospection(false); ``` Also the schema file can be disabled like the following. C# ``` builder .AddGraphQLServer() .ModifyRequestOptions(o => o.EnableSchemaFileSupport = false); ``` ## Fusion OK, with that, let's talk about Fusion, our GraphQL solution for federated APIs. With version 14, we have focused heavily on stability. Based on feedback from the community, we have improved how errors traverse from the source schemas to the composite schema. We have also made the configuration process easier by providing a new package that offers attributes for Fusion. This allows you to use C# instead of GraphQL extension files. C# ``` public static class Query { [Lookup] public static async Task GetBrandByProductIdAsync( [Is("product { id }")] int id, ISelection selection, BrandByProductIdDataLoader brandByProductId, CancellationToken cancellationToken) => await brandByProductId .Select(selection) .LoadAsync(id, cancellationToken); } ``` This is especially nice when we talk about `@require`. C# ``` public static int EstimateShippingTime( [Require("dimension { weight }")] int productWeight) ``` We have also worked on experimental support for Aspire, which gives you a much nicer development workflow around distributed GraphQL. Apart from these smaller changes, we are currently working on three major areas for Fusion. The first is implementing the composite schema specification, which will align Hot Chocolate Fusion with the open spec proposal. The second effort is achieving AOT compatibility for the gateway. This is a major undertaking, as we are essentially creating a second GraphQL server from scratch, focused solely on the gateway. Additionally, recognizing that many people use Apollo Federation and may want to migrate to a pure .NET solution, we are also working on compatibility with the Apollo Federation spec. As the composite schema specification merges Fusion concepts around lookups and the Apollo Federation spec around schema evolution and traffic steering, the step from Fusion to supporting Apollo Federation is not that big anymore. However, we have moved these tasks from Hot Chocolate 14 to Hot Chocolate 15 as we still have lots to do here. ## Client For Hot Chocolate Fusion, we have created a low-level GraphQL client that supports a variety of GraphQL protocols. We have refactored Strawberry Shake to use this basic client for HTTP traffic. For many server-to-server use cases, we recommend using this client as it is geared toward performance and allows you to bring your own models. C# ``` var client = new DefaultGraphQLHttpClient(httpClient); var query = """ query($episode: Episode!) { hero(episode: $episode) { name } } """; var variables = new Dictionary { ["episode"] = "JEDI", }; var response = await client.PostAsync(query, variables); using var body = await response.ReadAsResultAsync(cts.Token); var mode = body.Data.Deserialize() ``` ## GraphQL Cockpit With Banana Cake Pop, we have further shifted to give you more control over your applications with an end-to-end GraphQL cockpit that provides a schema registry, client registry, operation store, GraphQL telemetry, end-to-end OpenTelemetry tracing, logging, metrics, and strong schema evolution workflows that put you in control. ![Banana Cake Pop](https://chillicream.com/images/blog/2024-08-30-hot-chocolate-14/screen-banana-cake-pop-1.png) With Banana Cake Pop you have the best solution to manage your distributed GraphQL setup. ## Community In this release, we had a staggering **30** new contributors who helped alongside the team of core contributors. Overall, we had 46 contributors working on Hot Chocolate 14\. These contributions ranged from fixing typos to optimizing our filter expressions, like the [pull request](https://github.com/ChilliCream/graphql-platform/pull/7311) from @nikolai-mb. We are very grateful to have such a vibrant community that helps us make Hot Chocolate better every day. For this reason, we have now created a GitHub DevContainer template so that you can get started with contributing in about 2 minutes. You can either run the DevContainer directly on GitHub: ![GitHub Codespaces](https://chillicream.com/images/blog/2024-08-30-hot-chocolate-14/screen-codespaces-1.png) Or you can run it locally on your own Docker. If you do not know what DevContainers are, you can read up on them [here](https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers). ## Documentation and Courses We are still hard at work updating the documentation and are also taking feedback on this version. This post is based on 14.0.0-rc.1 which will be out in a couple of days. If you want to learn all about the new features of Hot Chocolate, I have made a course on DomeTrain that gives you the ultimate introduction to GraphQL and uses Hot Chocolate in its preview builds. If you use the code `STAIB`, you will get a 20% discount on the course. Apart from the in-depth workshop at DomeTrain we have also reworked our Getting Started workshop that you can now find [here](https://github.com/ChilliCream/graphql-workshop). ## Hot Chocolate 15 Lastly, let's talk about the roadmap ahead. We have already started work on Hot Chocolate 15, which is slated for release in December/January. Hot Chocolate 15 will have a heavy focus on Hot Chocolate Fusion and will introduce a brand new gateway and new composition tooling. As I outlined in the Fusion section, we are working on three key areas that will reinvent what Fusion is. Other areas we will focus on include DataLoader, with a new batch scheduler that uses its own `TaskScheduler` to better track DataLoader promises in batching and defer scenarios. We already have a PR up for this but had stability concerns for version 14\. With version 15, we will have the time to get this right and provide a much more efficient DataLoader implementation. Projections is another area where we are all in, working on a brand new projections engine. You can already see bits and pieces in Hot Chocolate 14 with the experimental features we've introduced around DataLoader projections. The new projection engine in `HotChocolate.Data` will be built on top of DataLoader and will offer a much more efficient way to project your data with proper data requirements. With Hot Chocolate 15, we are dropping support for `.NETStandard 2.0`, `.NET 6.0`, and `.NET 7.0`. Going forward, you will need to run on `.NET 8.0` or `.NET 9.0`. This change will allow us to modernize a lot of code and eliminate many precompile directives. Looking beyond Hot Chocolate 15, we will shift our focus back to Strawberry Shake, which will undergo a major overhaul. With that, I encourage you to try out Hot Chocolate 14 RC.1 and give us your feedback as soon as it will drop on nuget.org. We have planned for three more RCs after RC.1 to address issues our community finds. ## You might also like [View all](https://chillicream.com/blog) --- # Logging in Banana Cake Pop > We just released logging in Banana Cake Pop. Checkout the blog post to learn more! Canonical source: https://chillicream.com/blog/2024-08-11-logging ![](https://chillicream.com/images/blog/2024-08-11-logging/header.png) [![Pascal Senn's avatar](https://chillicream.com/_optimized/images/remote/3ebc6212aa4091005799d46f902a92e2eb7e77cd966797ecb5f4eaeab3bb26fe.png)Pascal Senn](https://chillicream.com/authors/pascal-senn)2024-08-112 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2024-08-11-logging&text=Logging+in+Banana+Cake+Pop) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2024-08-11-logging) - [bananacakepop](https://chillicream.com/blog/tags/bananacakepop) - [graphql](https://chillicream.com/blog/tags/graphql) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [workshops](https://chillicream.com/blog/tags/workshops) - [cloud](https://chillicream.com/blog/tags/cloud) - [release](https://chillicream.com/blog/tags/release) - [open-telemetry](https://chillicream.com/blog/tags/open-telemetry) - [logging](https://chillicream.com/blog/tags/logging) We’re thrilled to announce a new feature in Banana Cake Pop that will enhance your development and debugging experience—**Logging**! Now, you can seamlessly send logs to Banana Cake Pop and analyze them directly within the app, making it easier than ever to monitor and troubleshoot your APIs. ## What’s New? ### Service Logs ![Api Logs](https://chillicream.com/images/blog/2024-08-11-logging/api-logs-1.png) ![Api Logs - Expanded](https://chillicream.com/images/blog/2024-08-11-logging/api-logs-2.png) APIs now have a dedicated **Logs** tab. This new tab allows you to view all the logs associated with a specific API. Whether you're tracking requests, debugging issues, or monitoring performance, this feature gives you a comprehensive view of what's happening under the hood. ### Trace Logs ![Trace Logs](https://chillicream.com/images/blog/2024-08-11-logging/api-logs-3.png) We’ve also added the ability to inspect logs within individual traces. When you open a trace, you’ll now see all the logs corresponding to each trace. This granular level of detail is invaluable for pinpointing issues and analyze traces in detail. ### Log Retention - **Shared Clusters:** Log retention in shared clusters is set to 1 day. This ensures that you can review recent logs. - **Dedicated Clusters:** For those using dedicated clusters, we offer **dynamic log retention times**. This means you can configure log retention according to your specific needs, offering greater flexibility and control over your logging data. ## Getting Started with Logging To start using this new logging feature, ensure that you are using **Banana Cake Pop version 13.9.0 or 14.x.x-preview.8**. Below is a sample setup to get you started: C# ``` builder.Services .AddGraphQLServer() .AddInstrumentation() ... // your configuration here .AddBananaCakePopServices(x => { x.ApiId = ""; // <-- Replace with your API ID x.ApiKey = ""; // <-- Replace with your API key x.Stage = "dev"; }); builder.Services .AddLogging(x => x .AddBananaCakePopExporter() .AddOpenTelemetry(x => { x.IncludeFormattedMessage = true; x.IncludeScopes = true; })); ``` You can find the full example over [in the example repository](https://link.chillicream.com/2024/08/11/logging-example) or check out the [documentation](https://link.chillicream.com/2024/08/11/logging-docs) for more details. We hope this new logging capability helps you gain deeper insights into your APIs and streamline your development workflow. As always, we’re here to help with any questions or feedback you might have. Don’t hesitate to reach out on [contact@chillicream.com](mailto:contact@chillicream.com) or on [slack.chillicream.com](https://link.chillicream.com/2024/08/11/slack) ## 🛠️ Announcing Our Enterprise GraphQL Workshop In the fast-paced world of enterprise software development, mastering advanced architectural patterns is crucial for building robust and scalable applications. Our upcoming Enterprise GraphQL with DDD, CQRS, and Clean Architecture Workshop is an immersive one-day experience designed to elevate your skills. This workshop will guide you through the process of integrating GraphQL with DDD, CQRS, and Clean Architecture. You'll gain hands-on experience in constructing a sophisticated enterprise-level system, starting from the basics and moving towards complex implementations. Discover more about the workshop here: [DDD Workshop](https://link.chillicream.com/2024/08/11/ddd-workshop) Happy logging! 🚀 ## You might also like [View all](https://chillicream.com/blog) --- # Recent Highlights > We just released the Operation Builder, Telemetry, and a new Full Stack GraphQL Workshop. Checkout the blog post to learn more! Canonical source: https://chillicream.com/blog/2024-05-21-newsletter-may ![](https://chillicream.com/images/blog/2024-05-21-newsletter-may/header.png) [![Pascal Senn's avatar](https://chillicream.com/_optimized/images/remote/3ebc6212aa4091005799d46f902a92e2eb7e77cd966797ecb5f4eaeab3bb26fe.png)Pascal Senn](https://chillicream.com/authors/pascal-senn)2024-05-213 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2024-05-21-newsletter-may&text=Recent+Highlights) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2024-05-21-newsletter-may) - [bananacakepop](https://chillicream.com/blog/tags/bananacakepop) - [graphql](https://chillicream.com/blog/tags/graphql) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [workshops](https://chillicream.com/blog/tags/workshops) - [cloud](https://chillicream.com/blog/tags/cloud) - [release](https://chillicream.com/blog/tags/release) We’re excited to bring you some significant updates from ChiliCream that can genuinely make a difference in your day-to-day development. This isn't just about new features—it's about making your workflow more effective and your projects more successful. ## Operation Builder We’re proud to introduce the Operation Builder in Banana Cake Pop, a tool designed to make creating and managing your GraphQL operations a breeze. ![Operation Builder](https://chillicream.com/images/blog/2024-05-21-newsletter-may/img1.png) The Operation Builder simplifies the process of creating and managing queries, making it easier than ever to draft, edit, and inspect your operations. Dive deep into your schema, seamlessly navigate fields and fragments, and gain instant insights into your data structure. This is perfect for both quick edits and detailed explorations, helping you understand and optimize your queries with ease. Try out the Operation Builder today and transform the way you work with GraphQL! **[Check out the video here](https://link.chillicream.com/2024/05/21/ops-builder-video)** ## Telemetry ![Telemetry](https://chillicream.com/images/blog/2024-05-21-newsletter-may/img2.png)We’re want to put a spotlight on our Telemetry integration. Why did we build this? The answer is simple. Understanding your application’s performance shouldn't be a guessing game and GraphQL Telemetry is difficult. With our telemetry integration, you can have complete visibility into your GraphQL server. ![Telemetry](https://chillicream.com/images/blog/2024-05-21-newsletter-may/img3.png) - **Trace Visualization:** See every trace in detail. This helps you pinpoint precisely where your system can be improved. - **Latency Monitoring:** Track average latency and critical percentiles to ensure top-notch performance. - **Throughput Metrics:** Keep an eye on operations per minute, so you can manage and scale your resources effectively. - **Client Insights:** Identify which clients impact your system the most, helping you make data-driven decisions. - **Error Tracking:** Stay ahead of potential issues with real-time error reports. - **In-depth Operation Analysis:** Gain a comprehensive overview of each operation's latency, throughput, and error rates. The fusion dashboard offers extensive monitoring capabilities, presenting real-time tracing and telemetry insights of your gateway and subgraphs. The topology view reveals interconnections and client activities, while status indicators provide a quick overview of latency, throughput, and error rates. _Create, Collaborate, Conquer! Get Started with Banana Cake Pop Pro. Use the promo code **BCPROCKS** to get a discount on your first year and start using our GraphQL IDE to enhance your projects efficiently._ **[Check out the video here](https://link.chillicream.com/2024/05/21/telemetry-video)** or read the docs: [Open Telemetry Documentation](https://link.chillicream.com/2024/05/21/otel-docs) ## Announcing Our New Full Stack GraphQL Workshop In today's rapidly evolving technology landscape, staying ahead requires not only understanding the latest technologies but also knowing how to implement them effectively. Our brand-new Full Stack GraphQL Workshop is a two-day, hands-on journey designed to demystify advanced concepts. ![Full Stack GraphQL Workshop](https://chillicream.com/images/blog/2024-05-21-newsletter-may/img4.png) We'll start with the basics and progressively build a fully functional distributed web shop using HotChocolate, Relay.js, Fusion, multiple subgraphs, and .NET Aspire. We’ll also delve into foundational principles like domain-driven design, CQRS, and clean architecture. Learn more about the workshop here: [learn.chillicream.com](https://link.chillicream.com/2024/05/21/learn) ### We Want to Hear From You Your insights are invaluable to us. If you have questions, need more information, or want to discuss how our tools can fit into your projects, don’t hesitate to reach out on [contact@chillicream.com](mailto:contact@chillicream.com) or on [slack.chillicream.com](https://link.chillicream.com/2024/05/21/slack) ### Thank You We appreciate your engagement and are thrilled to support your projects with our evolving GraphQL solutions. Keep an eye out for HotChocolate 14, and let us help you take your projects to the next level. ## You might also like [View all](https://chillicream.com/blog) --- # Full Stack GraphQL Workshop > We're excited to announce our new Full Stack GraphQL Workshop. Learn more about the workshop here! Canonical source: https://chillicream.com/blog/2024-04-01-fullstack-workshop ![](https://chillicream.com/images/blog/2024-04-01-fullstack-workshop/header.png) [![Pascal Senn's avatar](https://chillicream.com/_optimized/images/remote/3ebc6212aa4091005799d46f902a92e2eb7e77cd966797ecb5f4eaeab3bb26fe.png)Pascal Senn](https://chillicream.com/authors/pascal-senn)2024-04-014 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2024-04-01-fullstack-workshop&text=Full+Stack+GraphQL+Workshop) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2024-04-01-fullstack-workshop) - [bananacakepop](https://chillicream.com/blog/tags/bananacakepop) - [graphql](https://chillicream.com/blog/tags/graphql) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [workshops](https://chillicream.com/blog/tags/workshops) In today's rapidly evolving technology landscape, staying ahead requires not only understanding the latest technologies but also how to implement them effectively. This two-day workshop is a hands-on journey designed to demystify advanced concepts by building from the ground up. We'll start with the basics and progressively build a fully functional distributed web shop using HotChocolate, Relay.js, Fusion, multiple subgraphs .NET Aspire and also concepts like domain driven design, CQRS and clean architecture. Book your seat now and learn more about the workshop [here](https://learn.chillicream.com/blog/2024-04-01/fullstack-workshop). ![Full Stack GraphQL Workshop](https://chillicream.com/images/blog/2024-04-01-fullstack-workshop/img1.png) Over two days, we'll cover everything from basic concepts to advanced techniques. We start by getting to know GraphQL, learning about its features and benefits compared to traditional methods. Later, we introduce Relay.js, focusing on how it works with GraphQL to improve data handling and application performance. **Day 1:** We begin with basic GraphQL concepts, then move on to how you can build efficient APIs. In the afternoon, we'll learn about Relay.js, starting with the fundamentals and advancing to more complex topics. **Day 2:** We explore deeper topics like how to change and improve your GraphQL setup, how to handle data updates smoothly, and how other advanced techniques fit into the GraphQL world. We end by learning about real-time data updates with subscriptions. For the next online workshop you [find more information here](https://learn.chillicream.com/blog/2024-04-01). This workshop can also be tailored to meet your company's specific needs. We offer the flexibility to customize the content and focus areas to best match your team's requirements and goals. Here are the detailed modules available: **Module 1: Getting Started with GraphQL** Kick off with GraphQL by understanding its fundamental concepts such as operations, types system, syntax, and reasons for using GraphQL over other APIs. The session ends with setting up a first GraphQL server, providing hands-on experience from the start. **Module 2: Building a Database Driven Application** Explore how to build GraphQL apis using Entity Framework Core, with features like paging, filtering, sorting, and projections. The session will also cover some advanced concepts like field middlewares. **Module 3: Building APIs with Simple Layering** This session focuses on API with simple layering, including applying filtering and pagination in layered architectures. It also covers best practices using DataLoaders for optimized data fetching operations. **Module 4: GraphQL Query Patterns and Best Practices** Detailed exploration of advanced GraphQL patterns such as evolving schemas, entity and connection patterns for large-scale applications, ensuring best practices are met for enterprise development. **Module 5: Getting Started with Relay.js** Introduction to Relay.js, focusing on queries, using fragments and arguments effectively. Module 6: Advanced Fetching Patterns This module covers complex data fetching strategies in Relay.js, including transitions, refetching, and pagination, essential for managing data in applications and providing peak user experience. **Module 7: Understanding Relay** Expands on Relay's core concepts, including store management, data prefetching methods, and internal workings of Relay for performance improvements and predictable state management. **Module 8: GraphQL Mutations Patterns and Best Practices** Deep dive into the structure and patterns of GraphQL mutations, focusing on how to effectively manage errors and ensure robust mutation operations. **Module 9: Mutations In Relay** This module focuses on teaching effective methods for managing mutations, error handling, and executing optimistic updates within Relay. **Module 10: GraphQL Schema Evolution** Review of techniques to evolve a GraphQL schema over time without breaking existing operations, including the use of client and schema registries and implementing open telemetry. **Module 11: Introduction to Distributed GraphQL** Covering the concepts and implementation of distributed GraphQL with fusion to allow scalable, efficiently distributed data across different services and servers. **Module 12: Authentication / Authorization** Detailed breakdown of implementing authentication and authorization in GraphQL applications, ensuring secure and controlled access to data through proper practices. **Module 13: CQRS, DDD and GraphQL, the Perfect Fit?** Exploration of how CQRS and Domain Driven Design can be integrated with GraphQL to optimize complexity management in large-scale domains. **Module 14: GraphQL Subscriptions Patterns and Best Practices** In-depth look at implementing real-time functionalities via GraphQL subscriptions, with specific focus on patterns. Implemented in both the backend and frontend.. **Closing Session: Q&A** An open session where attendees can ask questions or clarify doubts about the topics covered, facilitating deeper understanding and practical implementations. ## We Want to Hear From You Your insights are invaluable to us. If you have questions, need more information, or just want to talk to use, don’t hesitate to reach out on [contact@chillicream.com](mailto:contact@chillicream.com) or on [slack.chillicream.com](https://slack.chillicream.com/blog/2024/04/01/fullstack-workshop) ## You might also like [View all](https://chillicream.com/blog) --- # GraphQL-Fusion: An open approach towards distributed GraphQL > Together, we'll explore the new GraphQL-Fusion, the open approach towards distributed GraphQL. Canonical source: https://chillicream.com/blog/2023-08-15-fusion ![](https://chillicream.com/images/blog/2023-08-15-fusion/fusion-banner.png) [![Michael Staib's avatar](https://chillicream.com/_optimized/images/remote/2bb0dd6e1b9347a1d3732300ea752f39f1ac4f1f76587433ca3a407245b31786.jpg)Michael Staib](https://chillicream.com/authors/michael-staib)2023-08-1518 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2023-08-15-fusion&text=GraphQL-Fusion%3A+An+open+approach+towards+distributed+GraphQL) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2023-08-15-fusion) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [graphql](https://chillicream.com/blog/tags/graphql) - [federation](https://chillicream.com/blog/tags/federation) - [fusion](https://chillicream.com/blog/tags/fusion) - [micro-services](https://chillicream.com/blog/tags/micro-services) ## In the beginning Right from the beginning, people saw the potential of GraphQL as a gateway technology. GraphQL promised a single integrated schema to the API consumer while offering the flexibility to leverage various technologies and services behind the scenes. When GraphQL was introduced, front-end engineers were the first to glimpse the power of it and started wrapping their REST services with GraphQL. This made data fetching more efficient by aggregating data calls close to downstream services and rendered the data more accessible. The straightforward, human-understandable schema made it easier to trace relations and reason about data and its connections in an entirely new way. GraphQL offered a way to model an interface to our core business domain that often diverged from the technical realities of the REST, gRPC, or other APIs behind it. It eliminated the complexity of knowing which micro-service would provide the necessary data or mutations for a particular use case. While micro-service or domain-service architectures provided technical means to scale more efficiently and align with organizational needs, GraphQL introduced simplicity with its unified schema approach. From the outset, GraphQL server developers were challenged to find ways to simplify distributed GraphQL setups. Over time, we've witnessed the evolution of various methods, from schema stitching techniques to federated solutions like Apollo Federation. However, many of these restrict users within a single-vendor ecosystem or, on the other end, are too rudimentary to cater to sophisticated enterprise requirements. ## Expectations We believe that distributed GraphQL services — or composite GraphQL services — should be straightforward to set up and seamlessly integrate with the diverse range of CI/CD tools, schema registries, composition utilities, and gateways that enterprises might prefer. The current landscape should not dictate the choice of tools but provide flexibility. Up to this point, the GraphQL landscape has lacked an open specification tailored for distributed setups – a framework designed from the ground up for extensibility and integration with diverse toolchains. We envisioned a platform where tools from various vendors could effortlessly work in tandem, ensuring that developers and enterprises never feel constrained by their technical choices. ## Let's share and compete Late last year, [ChilliCream](https://chillicream.com/) and [The Guild](https://the-guild.dev/) met in Paris and discussed their approaches towards distributed GraphQL. It became clear that both companies were solving similar problems, and we decided to join forces on this project. ChilliCream would provide the initial work on the Fusion spec and implementation. At the same time, The Guild would start specifying their work on [GraphQL Mesh Gateway](https://the-guild.dev/graphql/mesh) with [OpenAPI support](https://the-guild.dev/graphql/mesh/docs/handlers/openapi) and help shape the initial Fusion spec. As we started, work on prototypes and the initial spec texts, we reached out to more companies in the community to see if there was interest in collaboration. It turns out that the GraphQL community is hungry for an open specification to standardize distributed GraphQL application gateways. [Hasura](https://hasura.io/), [IBM](https://www.ibm.com/), [solo.io](https://www.solo.io/), [AWS AppSync](https://aws.amazon.com/de/appsync/), [WunderGraph](https://wundergraph.com/) have all joined the effort for creating a common spec. Today, we are thrilled to unveil GraphQL-Fusion, an open specification under the **MIT license**. This initiative empowers everyone to craft tools and solutions centered around distributed GraphQL services. Complementing this announcement, we're also introducing [Hot Chocolate](https://chillicream.com/docs/hotchocolate) Fusion, an early implementation of the GraphQL-Fusion spec draft. The GraphQL-Fusion spec goes beyond what traditional federation approaches went after. It establishes GraphQL as an application gateway that allows integrating GraphQL APIs, REST APIs, gRPC APIs, or even databases. For this reason, in addition to The Guild's work on the Open API to GraphQL spec, Hasura will start specifying GraphQL Data Compliant APIs, the AWS AppSync team will focus on specs for throttling, authentication, and subscriptions and WunderGraph will specify adapter specs for gRPC and Kafka (AsyncApi). As mentioned initially, GraphQL is a great gateway technology, although it started from a different place. It gives the consumer the simplicity of the single schema and hides behind that schema the technical complexities of a heterogenous service landscape. ## A new way to distribute GraphQL schema components GraphQL-Fusion presents a fresh approach to streamlining the complexities of assembling distributed schemas. At its heart, GraphQL-Fusion pivots around two foundational principles: schema composition and query planning. But before delving into these concepts, it's essential to retrace our steps. Let's revisit the challenges surfaced when people first tried GraphQL as a Gateway for constructing their GraphQL servers over REST APIs. The ideal scenario is one where our teams operate autonomously, deploying updates at their pace. However, positioning a GraphQL server at the forefront as the gateway introduced an unexpected bottleneck to the development flow. Suddenly, updating downstream APIs required updates to the central GraphQL server, leading to inevitable synchronization hurdles. Burdening teams with higher maintenance and reduced flexibility. While GraphQL schema stitching solutions simplified the composition of GraphQL Gateways, they still suffered from the same coordination dilemma since the gateway retained pivotal configuration logic. Federated GraphQL solutions emerged as a remedy, redistributing this configuration logic across subgraphs, thus enabling teams to work and release subgraphs autonomously. Fusion represents a fully federated approach but also incorporates the capabilities of stitching solutions to rewrite and transform subgraph schemas. Further, Fusion removes the requirement of subgraph protocols we see in many federated GraphQL solutions. This means you can use any GraphQL server as a Fusion subgraph, and the capabilities of your subgraph within a Fusion setup are defined by the GraphQL spec version your GraphQL server implements. The Fusion schema composition aims to infer the semantic meaning of a GraphQL schema, reducing annotations to the schema. Fusion schema composition recognizes GraphQL best practices like the Relay patterns or naming patterns and their semantics. Instead of treating fields and types bearing identical names as collisions, Fusion recognizes them as overlaps. For clarity, consider the following GraphQL query type example: GraphQL ``` type Query { userByID(id: ID!): User productBySKU(sku: String!): Product articleBySlug(slug: String!): Article } ``` In this example, fields follow the `{type}By{key}` naming convention: - userByID for fetching users by ID - productBySKU for retrieving products by SKU - articleBySlug for obtaining articles by slug Things we can fetch by one or multiple keys are entities to Fusion, allowing the Fusion query planner to create query plan tasks to fill in data from various subgraphs. Fusion does not need to know their keys spelled out, as this is inferred from their resolver signature. Let's consider two subgraphs - one for product reviews and another for user data. **Subgraph 1: Product Reviews** GraphQL ``` type Review { id: ID! body: String! product: Product! author: User! } type User { id: ID! name: String! reviews: [Review!] } type Product { sku: String! reviews: [Review!] } type Query { reviews: [Review!] reviewById(id: ID!): Review userById(id: ID!): User productBySKU(sku: String!): Product } ``` **Subgraph 2: User Data** GraphQL ``` type User { id: ID! name: String! email: String! } type Query { userById(id: ID!): User } ``` The outcome? An annotated Fusion graph document, which provides all the metadata for the Fusion gateway query planner. **Composed Fusion Graph** GraphQL ``` type Review @variable(subgraph: "Reviews", name: "Review_id", select: "id") @resolver( subgraph: "Reviews" select: "{ reviewById(id: $Review_id) }" arguments: [{ name: "Review_id", type: "ID!" }] ) { id: ID! @source(subgraph: "Reviews") body: String! @source(subgraph: "Reviews") product: Product! @source(subgraph: "Reviews") author: User! @source(subgraph: "Reviews") } type User @variable(subgraph: "Reviews", name: "User_id", select: "id") @variable(subgraph: "Account", name: "User_id", select: "id") @resolver( subgraph: "Reviews" select: "{ userById(id: $id) }" arguments: [{ name: "User_id", type: "ID!" }] ) @resolver( subgraph: "Account" select: "{ userById(id: $id) }" arguments: [{ name: "User_id", type: "ID!" }] ) { id: ID! @source(subgraph: "Reviews") @source(subgraph: "Account") name: String! @source(subgraph: "Reviews") @source(subgraph: "Account") email: String! @source(subgraph: "Account") } type Product @variable(subgraph: "Reviews", name: "Product_sku", select: "sku") @resolver( subgraph: "Reviews" select: "{ productBySKU(sku: $Product_sku) }" arguments: [{ name: "Product_sku", type: "String!" }] ) { sku: String! @source(subgraph: "Reviews") reviews: [Review!] @source(subgraph: "Reviews") } type Query { reviews: [Review!] @resolver(subgraph: "Reviews", select: "{ reviews }") userById(id: ID!): User @resolver( subgraph: "Reviews" select: "{ userById(id: $id) }" arguments: [{ name: "id", type: "ID!" }] ) @resolver( subgraph: "Account" select: "{ userById(id: $id) }" arguments: [{ name: "id", type: "ID!" }] ) reviewById(id: ID!): Review @resolver( subgraph: "Reviews" select: "{ reviewById(id: $id) }" arguments: [{ name: "id", type: "ID!" }] ) productBySKU(sku: String!): Product @resolver( subgraph: "Reviews" select: "{ productBySKU(id: $id) }" arguments: [{ name: "id", type: "ID!" }] ) } ``` ## Query Planning and Optimizations The above-annotated schema document allows the Fusion gateway to plan data fetching from its subgraphs efficiently. When executing a query like the following: GraphQL ``` query GetReviews { reviews { body author { name email } } } ``` The query planner might produce two downstream queries: **Query 1** GraphQL ``` query GetReviews_1 { reviews { body author { name __export__1: id } } } ``` **Query 2** GraphQL ``` query GetReviews_2($__export__1: ID!) { userById(id: $__export__1) { email } } ``` We are essentially doing an initial call to the reviews services, collecting all user ids, and then doing a call to our accounts subgraph for each user id we collected to get the emails. While this is not efficient, as we would have to make multiple subgraph requests, the Fusion composition and query planner also understand batching fields and how to integrate them into the query planning process. If we introduced the following root field to our accounts subgraph and recomposed: GraphQL ``` extend type Query { usersById(ids: [ID!]!): [User!] } ``` The composition would add a batching resolver to the `User` type: GraphQL ``` extend type User @resolver( subgraph: "Account", select: "{ usersById(ids: $User_Id) }", arguments: [ { name: "User_Id", type: "[ID!]!" } ], kind: "BATCH_BY_KEY" ) { } ``` With this new field in place, the query planner can prioritize batch resolvers whenever we branch off a request in a list context, even if that list context spreads multiple levels deep. **Query 1** GraphQL ``` query GetReviews_1 { reviews { body author { name __export__1: id } } } ``` **Query 2** GraphQL ``` query GetReviews_2($__export__1: [ID!]!) { usersById(id: $__export__1) { email } } ``` The Fusion query plan is another standardized component that tooling (like [Banana Cake Pop](https://eat.bananacakepop.com/)) can use to give you insights into how efficiently the gateway can resolve the requested data. ![Banana Cake Pop - Query Plan Viewer](https://chillicream.com/images/blog/2023-08-15-fusion/bcp-1.png) _Also available in black ;)_ The Fusion Query plan consists of the following query plan node kinds: `Compose`, `Defer`, `Stream`, `If`, `Introspect`, `Parallel`, `Resolve`, `ResolveByKeyBatch`, `ResolveNode`, `Sequence`, and `Subscribe`. With these abstract nodes, the query planner is able to create complex query plans that support every GraphQL feature and best practice right out of the gate. While the `Fetch` and `Batch` nodes are clear about what they do in our query plan, the compose step might be a mystery to you. In essence, the query planner can fetch data that does not align with the current structure of the request. Compose will take in the raw data fetched by resolve nodes and composes it into the GraphQL request structure. It also ensures that result coercion rules are correctly applied to be GraphQL spec-compliant. ![Banana Cake Pop - Query Plan Viewer](https://chillicream.com/images/blog/2023-08-15-fusion/bcp-5.png) _In this case, compose creates the result of a single selection set from multiple resolve nodes._ The Hot Chocolate Fusion Gateway implementation supports all supported subscription protocols, from the legacy Apollo subscription protocol over graphql-ws to graphql-sse. Further, it supports file uploads with the GraphQL multipart request protocol, Facebook-style batching with the `@export` directives, the newest `@defer` and `@stream` spec draft, the newest Client Controlled Nullability spec draft, and many more GraphQL features. Distributed GraphQL should **not limit** what you can do with GraphQL. ## Relay The schema composition can also introduce aspects such as the Relay conventions to your Gateway schema, even if they aren't implemented in your subgraphs. Alternatively, if your subgraphs implement the Relay conventions, such as the global object identity convention, the schema composition will detect this and incorporate it into the Fusion graph document. For example, this can optimize your query planning by utilizing the node field. Further, this makes all object types that implement the `Node` interface an entity to Fusion. GraphQL ``` extend type User @resolver( subgraph: "User", select: "{ node(id: $User_id) { ... on User { ... User } } }", arguments: [ { name: "User_id", type: "ID!" } ]) @resolver( subgraph: "User", select: "{ nodes(ids: $User_id) { ... on User { ... User } } }", arguments: [ { name: "User_id", type: "[ID!]!" } ], kind: "BATCH_BY_KEY") { } ``` While using the `node` field to fetch entity data is straightforward for exposing the `node` fields to the gateway, we found it necessary to equip the Fusion gateway with data sharding capabilities. This is the ability to dispatch a query at runtime to a specific subgraph based on user-provided data. This can be applied to simple tasks like the node field but can also be harnessed to isolate data partitions by region or any other discriminants you desire. ![Banana Cake Pop - Query Plan Viewer](https://chillicream.com/images/blog/2023-08-15-fusion/bcp-6.png) _`node` field query plan._ If we zoom into the JSON representation of our query plan, we can see in detail the branches of our `ResolveNode` in the query plan. Depending on the type in our encoded `ID`, one of the branches will be executed. If the encoded types have different names in the subgraphs, Fusion will reencode the ID for the particular subgraph. ![Banana Cake Pop - Query Plan Viewer](https://chillicream.com/images/blog/2023-08-15-fusion/bcp-7.png) _JSON representation of our query plan_ ## Going Further While the subgraph inference of the schema composition is quite powerful, there are a lot of cases where we can go further by declaring the semantics of a GraphQL schema. We do not need to integrate such annotations into our subgraph schema directly but can pass additional GraphQL documents into the schema composition that hold type extensions with additional directive annotations. Let's say the batching field we introduced did not follow the conventions of the other fields in our GraphQL schema. GraphQL ``` extend type Query { users(ids: [ID!]!): [User!] } ``` In this case, the schema composition cannot just guess what `ids` is. `ids` could be identities for whatever. This is where we can use the fusion subgraph directives to bring meaning to the schema. GraphQL ``` extend type Query { users(ids: [ID!]! @is(field: "id")): [User!] } ``` The `@is` directive allows us to specify that the argument on our field `users` is semantically identical to the output field `id` on their returning `User` type. Since `ids` is a list that returns a list of users, we can now infer that this field allows us to batch-fetch users by user ids. ## Requirements Where subgraph directives really become necessary is with requirements. Data requirements let us integrate two or more subgraphs with each other without bleeding internal data requirements into the public schema. Let's say we have the following schema: GraphQL ``` type Product { sku: String! name: String! dimension: ProductDimension } ``` Also, let's say we have a subgraph that can calculate a delivery estimate for a product. GraphQL ``` type Product { deliveryEstimate(zip: String!, width: Float!, height: Float!): Int! } ``` We need the ZIP code and the product's width and height to calculate the delivery estimate in our shipping subgraph. The product's dimension (width and height) is actually available in the product catalog service, which holds all the information about the product itself. In this case, we want to create a public API for our consumer where we only have to pass in the ZIP code to the `deliveryEstimate` field on the `Product` type on our Fusion graph. GraphQL ``` type Product { sku: String! name: String! dimension: ProductDimension deliveryEstimate(zip: String!): Int! } ``` We can express this by using the `@require` directive and referring to the required information relative to the `Product` type. GraphQL ``` type Product { deliveryEstimate( zip: String! width: Float! @require(field: "dimension { width }") height: Float! @require(field: "dimension { height }") ): Int! } ``` We could also design that slightly differently and introduce an input to our subgraph representing the required data we need. GraphQL ``` input ProductDimensionInput { width: Float! height: Float! } type Product { deliveryEstimate( zip: String! dimension: ProductDimensionInput! @require(field: "dimension") ): Int! } ``` The outcome will stay the same, and we will get this nice API for our users. The query planner will resolve the required data under the hood. ![Banana Cake Pop - Query Plan Viewer](https://chillicream.com/images/blog/2023-08-15-fusion/bcp-2.png) Again, this brings clarity to your subgraph as the field is very clear about what it needs and becomes easily testable in the process. ## Reshaping things When we rethink a bit the shipping subgraph we actually should realize that the `deliveryEstimate` does not really need to be on the `Product` type as the argument has clear requirements which are expressed by its field arguments. Instead of having the field `deliveryEstimate` on the `Product` type itself, it could very well be exposed through the `Query` type, at least in the context of our subgraph. GraphQL ``` type Query { estimateShipping(zip: String!, width: Float!, height: Float!): Int! } ``` In the case the subgraph is isolated and does not fully integrate with our intended public model, we can also reshape the subgraph to make it fit. All the annotations can be put into separate graphql documents providing the extending metadata. This allows us to keep our actual subgraph schema clean. It also helps when you do not fully own the schema that you integrate, like, for instance, the GitHub schema. Just create a `schema.extensions.graphql` and put your annotations and extension in that file, and you're good to go. First, let's make the whole query type private; we do not want to include anything by default from this subgraph. GraphQL ``` extend type Query @private ``` Next, we introduce some product metadata. GraphQL ``` extend type Product { estimateShipping( zip: String! width: Float! @require(field: "dimension { width }") height: Float! @require(field: "dimension { height }") ): Int! } extend type Query @private ``` Last, we want to declare how estimate wires up to our internal `Query` type. GraphQL ``` extend type Product { estimateShipping( zip: String! width: Float! @require(field: "dimension { width }") height: Float! @require(field: "dimension { height }") ): Int! @resolve } extend type Query @private ``` Since the field and arguments 100% match between the `Query` type and the `Product` type extension, we only need to put the `@resolve` directive on the field without specifying any mapping of arguments. But let's imagine we call it `calculateDelivery` on the product type. In this case, we need to become more explicit. GraphQL ``` extend type Product { calculateDelivery( zip: String! width: Float! @require(field: "dimension { width }") height: Float! @require(field: "dimension { height }") ): Int! @resolve(select: "estimateShipping") } extend type Query @private ``` Again, arguments match, so we do not need to map them, but we could. Each argument would become an implicit variable in this case. GraphQL ``` extend type Product { calculateShipping( zip: String! width: Float! @require(field: "dimension { width }") height: Float! @require(field: "dimension { height }") ): Int! @resolve(select: "estimateShipping(zip: $zip)") } extend type Query @private ``` Arguments that you do not map are again inferred, allowing you always just to specify the minimum. Since Fusion compiles the Fusion Graph at build time, this is fine, as the schema composition will always tell you what is missing and precisely in which file you have to specify more information for the composition and query planner to work. Let's move on from the requirements case and dig deeper into the type reshaping capabilities. Often when we build our data silos, we also do not have all the stub types in there. It would sometimes be tedious to always have them around. Think of the review service. GraphQL ``` type Review { id: ID! body: String! product: Product! author: User! } ``` This `Product` type is essentially just the `sku` field of the public `Product` type. In many cases, people are more tempted to create something like the following in the subgraph schema since it's just less cluttered. GraphQL ``` type Review { id: ID! body: String! productSKU: String! author: User! } ``` With Fusion, you can provide us with some metadata in the schema extension file, and we will ensure that the references are introduced on our publicly exposed gateway schema. GraphQL ``` extend type Review { productSKU: String! @is(coordinate: "Product.sku") @private product: Product! @resolve } ``` By declaring the semantics of the field `productSKU`, we can infer a way to resolve a product using one existing way to fetch a `Product` entity. Again, if there is no way to resolve it, we will give you a composition error telling you which subgraphs you could or should introduce `Query` fields to, to resolve the `Product` entity. In many cases, we want to connect our types from both sides. GraphQL ``` extend type Product { reviews: [Review!] @resolve } extend type Review { productSKU: String! @is(coordinate: "Product.sku") @private product: Product! @resolve } ``` For this to work, we would need to introduce a `reviewsBySKU` to our reviews subgraph. But in any case, we will be told by our schema composition if it is missing on our subgraph. Like with our case for estimate delivery, we can be more or less explicit with our `@resolve` directive. GraphQL ``` extend type Product { reviews: [Review!] @resolve(select: "reviewsBySku(sku: $sku)") } ``` Since `sku` is available on the product, it is automatically a variable available to inject. But because of collisions with field arguments or if the actual `sku` is not directly on the `Product` type, we could also explicitly declare the variable and state what we mean. GraphQL ``` extend type Product { reviews: [Review!] @declare(variable: "sku", select: "someOtherField { sku }") @resolve(select: "reviewsBySku(sku: $sku)") } ``` > The `select` argument represents a field selection or selection set syntax and also allows for more complex query constructs that refer to GraphQL query files using fragments and other query constructs. For this introduction to Fusion as a concept, we keep it simple. When I said at the beginning that Fusion lends concepts from both schema stitching and federation approaches, then its this kind of flexibility that I mean, you can build your graph in a federated structure, and you will, in most cases, not need to declare anything to the composition as everything can be inferred, but you can become very explicit with hints or even with the more precise `@resolve` directive. ## Open Telemetry for Federated Tracing This brings me to another aspect of Fusion: telemetry. While the GraphQL-Fusion spec isn't primarily concerned with tracing itself, we've decided to leverage OpenTelemetry for the Hot Chocolate Fusion Gateway implementation. We're working to establish a more precise semantic convention for GraphQL in collaboration with the OpenTelemetry community. This effort aims to enable standard GraphQL servers to use OpenTelemetry to expose the intricate processes that occur when a GraphQL server handles a request. These traces, correlated from the gateway to the subgraph, allow any vendor to digest tracing events and provide profound insights into where performance bottlenecks exist in your distributed system. Combined with the GraphQL subgraphs, which use instead of a generic `_entities` field actual semantic fields like `reviewsBySKU` in Fusion to retrieve data from subgraphs, the traces become very clear to read and expose optimization potential to the developers. The best part? There's no need for specialized approaches — it's as straightforward as crafting a conventional resolver. With the release of [Banana Cake Pop](https://eat.bananacakepop.com/) version 9, we're introducing our revamped query plan viewer. This tool signifies our initial step towards integrating telemetry data from GraphQL-Fusion, aiming to provide comprehensive insights into the operations of your distributed GraphQL setup. However, thanks to the open nature of the GraphQL-Fusion spec and the OpenTelemetry definitions, you're not confined to our tools—alternatives like [The Guild's Hive](https://the-guild.dev/graphql/hive), [WunderGraph's Cosmo](https://wundergraph.com/cosmo), or even a plain Elastic Cloud integration are also available. ## CI/CD integrations from the start We considered CI/CD from the start when conceptualizing Fusion and structured it so that you can easily integrate your solution. Right out of the gate, you can start with [Banana Cake Pop](https://eat.bananacakepop.com/), which provides a schema registry, easy rollbacks of changes introduced by your subgraphs, and deployment pipeline synchronization. ![Banana Cake Pop - Stages](https://chillicream.com/images/blog/2023-08-15-fusion/bcp-3.png) But the core principle is that this is open and built into the GraphQL-Fusion spec. To provide tooling a single file containing all the information needed for gateway configuration and even space for gateway-specific features, we've adopted the [Open Packaging Convention](https://en.wikipedia.org/wiki/Open%5FPackaging%5FConventions) as a container for the GraphQL-Fusion Configuration (.fgp). The [Open Packaging Convention](https://en.wikipedia.org/wiki/Open%5FPackaging%5FConventions) is an open standard provided by [Microsoft](https://microsoft.com/), used for everything from Word Documents (.docx) to VSCode extension packages (.vsix). Simply put, think of it as a ZIP container with metadata and relations between its artifacts. The GraphQL-Fusion convention contains the mandatory Fusion Graph document. This document is all that's needed to run and configure a Gateway implementing the core specification. Additionally, it contains all subgraph schema documents, the publicly exposed Gateway schema, and composition settings the user has opted into. Having all these artifacts in one place gives us a single artifact that we can pass on from the schema composition in a CI/CD pipeline to the schema registry and from there to the actual gateway. We have customers already using this with their custom solutions for distributing the configuration from their deployment pipeline to their gateway or by using our Cloud Services ([Banana Cake Pop](https://eat.bananacakepop.com/)). Besides these standard artifacts included in the package, it also allows Gateway implementers to store custom configurations to specify GraphQL WAF settings and more. ![Simple Deployment Pipeline](https://chillicream.com/images/blog/2023-08-15-fusion/pipeline-1.png) ## Apollo Federation We recognize that some of you may have opted into Apollo Federation as a solution, and that is why we designed the Fusion schema composition so that it can compose any Apollo Federation subgraph into a Fusion subgraph. This allows you to seamlessly migrate from an Apollo Federation setup to a GraphQL-Fusion setup without the need to rewrite anything. ## Conclusion We are still working on GraphQL-Fusion, but you can try an early version already today with the Hot Chocolate Fusion Gateway. We plan to release the GraphQL-Fusion spec as it matures later this year as **MIT license**. While the current iterations are primarily concerned with GraphQL, as mentioned before The Guild, in collaboration with IBM and StepZen, has begun specifying the Open API to GraphQL transformation they've developed in Mesh, and we'll integrate this as it becomes available. Hasura is working on GraphQL Compliant Data APIs spec and WunderGraph wants to contribute a spec for gRPC and Kafka (AsyncApi) to take federated GraphQL to a whole new level. There's much more from the query plan engine to the schema composition we're eager to showcase in detail. Today I wanted to start talking about GraphQL-Fusion as a concept. We are also working on step-by-step YouTube tutorials for later this year that shows you how to create a GraphQL-Fusion setup from scratch or how to migrate from an Apollo Federation setup without skipping a beat to a GraphQL-Fusion setup. The one crucial thing behind this effort is creating a truly open spec, which leans heavily on the GraphQL spec and describes the algorithms behind the schema composition and the query planning. No single company will own the spec as the GraphQL Foundation will take ownership of it. This ensures that we have a level playing field where companies can provide services, tooling, or gateways. It gives a better choice to developers building distributed systems with GraphQL as they can easily connect tools from different vendors. You can join me at [GraphQL conf in San Francisco](https://graphql.org/conf/schedule/4a4e842d1cd0c06083f484d31225abd1/?name=GraphQL%20Fusion:%20Rethinking%20Distributed%20GraphQL%20-%20Michael%20Staib,%20ChilliCream%20Inc) as I will be giving a talk about GraphQL-Fusion with the latest bits. ## You might also like [View all](https://chillicream.com/blog) --- # Let’s Boost Your Productivity With APIs > Together, we'll explore the new API feature coming with Banana Cake Pop 5 very soon. Canonical source: https://chillicream.com/blog/2023-03-15-banana-cake-pop-graphql-apis [![Rafael Staib's avatar](https://chillicream.com/_optimized/images/remote/47f10f3229fb2cfedca9993792f328fe15c7670ac3ed9d4e777b11374b3e4907.png)Rafael Staib](https://chillicream.com/authors/rafael-staib)2023-03-151 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2023-03-15-banana-cake-pop-graphql-apis&text=Let%E2%80%99s+Boost+Your+Productivity+With+APIs) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2023-03-15-banana-cake-pop-graphql-apis) - [bananacakepop](https://chillicream.com/blog/tags/bananacakepop) - [graphql](https://chillicream.com/blog/tags/graphql) - [ide](https://chillicream.com/blog/tags/ide) - [cloud](https://chillicream.com/blog/tags/cloud) - [release](https://chillicream.com/blog/tags/release) In version 5, the biggest change that will arrive soon is _APIs_, which will introduce a way to reorganize your documents completely. Let’s find out how this will change your life when working with _GraphQL_ _APIs_ in **Banana Cake Pop**. Go to [bananacakepop.com](https://bananacakepop.com/) and check out the latest _Insider_ app or web version to get a taste of _APIs_. Watch the video to get more info about what APIs are and how they work. ## Subscribe To stay up to date, subscribe to our [ChilliCream YouTube Channel](https://www.youtube.com/c/ChilliCream) to get notified whenever we publish new videos. I'm Rafael Staib, and as soon as **Banana Cake Pop 5** is released, I'll be right here to tell you what's new in **Banana Cake Pop**! ## You might also like [View all](https://chillicream.com/blog) --- # What's new for Hot Chocolate 13 > Hot Chocolate 13 improves GraphQL over HTTP, developer experience, authorization, subscriptions, data access, performance, and Strawberry Shake. Canonical source: https://chillicream.com/blog/2023-02-08-new-in-hot-chocolate-13 ![](https://chillicream.com/images/blog/2023-02-08-new-in-hot-chocolate-13/hot-chocolate-13-banner.png) [![Michael Staib's avatar](https://chillicream.com/_optimized/images/remote/2bb0dd6e1b9347a1d3732300ea752f39f1ac4f1f76587433ca3a407245b31786.jpg)Michael Staib](https://chillicream.com/authors/michael-staib)2023-02-0816 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2023-02-08-new-in-hot-chocolate-13&text=What%27s+new+for+Hot+Chocolate+13) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2023-02-08-new-in-hot-chocolate-13) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [graphql](https://chillicream.com/blog/tags/graphql) - [dotnet](https://chillicream.com/blog/tags/dotnet) - [aspnetcore](https://chillicream.com/blog/tags/aspnetcore) The last major release of Hot Chocolate was on the 27th of September, and since then, I have stopped writing blogs and focused more attention on YouTube. But for this occasion, it feels right to write and would have anyway resulted in a video that is too long. ## What is Version 13 about? When we started on Hot Chocolate 13, the release focused on our Gateway, aka schema stitching. As we worked on schema stitching, it became apparent to us that we wanted to change and make it much easier than the current solutions that are out there. Distributed graphs should work with GraphQL and not force you to build them in a certain way but still yield best-in-class performance. At some point, our work branched off the original stitching project, and we created a new component called Hot Chocolate Fusion. As we were working on Hot Chocolate Fusion, we saw the time pass by and estimated that it would take considerable time more to get it done in the quality it should be. At this point, we already had so many great features and bugfixes merged into version 13 that we decided to focus development on delivering a Hot Chocolate 13 core with many improvements and ship Fusion as a dot release of 13 when it's ready. If you asked me what the focus is of version 13, then I would say developer experience and more :) ## GraphQL over Internet One major focus we put on Hot Chocolate 13 is transport. With Hot Chocolate 13, we are one of two servers (GraphQL-yoga and Hot Chocolate) fully supporting the new GraphQL over HTTP spec draft. The transport spec defines when to use which HTTP status code and introduces a new response content-type, `application/graphql-response+json`. The new transport spec makes proper use of the HTTP accept headers, meaning your client can now define what response content-types it understands and can handle. If your client, for instance, can only deal with `application/json` as a response content-type, then you can define that now in your request. ``` curl 'https://api-crypto-workshop.chillicream.com/graphql' \ -H 'authority: api-crypto-workshop.chillicream.com' \ -H 'accept: application/json' \ -H 'content-type: application/json' \ --data-raw '{"query":"{ __typename }\n","variables":{}}' \ --compressed ``` [GraphQL over HTTP Spec](https://github.com/graphql/graphql-over-http) If you do not want to use the new GraphQL over HTTP spec draft, then you can opt into our legacy mode, which uses `application/json` and 200 HTTP status codes. C# ``` using HotChocolate.AspNetCore; using HotChocolate.AspNetCore.Serialization; var builder = WebApplication.CreateBuilder(args); builder.Services .AddHttpResponseFormatter( new HttpResponseFormatterOptions { HttpTransportVersion = HttpTransportVersion.Legacy }); builder.Services .AddGraphQLServer() .AddTypes(); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` > Note: After 2025-01-01T00:00:00Z, GraphQL servers are no longer required to support the legacy transport mode. Apart from GraphQL over HTTP, we also focused on supporting even more GraphQL transport protocols. So, with Hot Chocolate 13, we now implement the GraphQL-SSE protocol, which allows you to use server-sent events for subscriptions or even queries that use defer. GraphQL-SSE, for me, has become the go-to solution for subscriptions. But we also brought the WebSocket transport up to speed with GraphQL-WS. We now support the legacy Apollo subscription protocol and the new GraphQL-WS protocol. ![Banana Cake Pop - Subscription Protocol Selection Dialog](https://chillicream.com/images/blog/2023-02-08-new-in-hot-chocolate-13/bcp-1.png) [GraphQL-SSE Protocol](https://github.com/enisdenjo/graphql-sse) / [GraphQL-WS Protocol](https://github.com/enisdenjo/graphql-ws) ### Cache-Control We now have implemented the GraphQL cache-control feature, which allows you to specify cache-control headers for GraphQL query responses based on the entities you query. To enable Cache-Control, you will need to install the package `HotChocolate.Caching`. Bash ``` dotnet add package HotChocolate.Caching ``` Next, we will need to add the following to your GraphQL configuration. by default. C# ``` var builder = WebApplication.CreateBuilder(args); builder.Services .AddGraphQLServer() .AddTypes() .AddCacheControl() .UseQueryCachePipeline(); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` Hot Chocolate will apply defaults to your fields which you can override on a by-field basis. C# ``` [QueryType] public static class Query { [CacheControl(maxAge: 10_000)] public static Book GetBook() => new Book("C# in depth.", new Author("Jon Skeet")); } ``` The GraphQL cache-control header will collect the allowed amount of time the response is cacheable and exposes this as a cache-control header which consequently can be used by CDNs or browsers to cache the result. ### Null Values Another smaller optimization option we have introduced to Hot Chocolate is the null value erasure. C# ``` using HotChocolate.AspNetCore.Serialization; using HotChocolate.Execution.Serialization; var builder = WebApplication.CreateBuilder(args); builder.Services .AddHttpResponseFormatter( new HttpResponseFormatterOptions { Json = new JsonResultFormatterOptions { NullIgnoreCondition = JsonNullIgnoreCondition.Fields } }); builder.Services .AddGraphQLServer() .AddTypes(); var app = builder.Build(); app.MapGraphQL(); app.Run(); ``` Writing now a query where we fetch a field that is null ... GraphQL ``` { book { title descriptionIsNull } } ``` ... will yield the following result. JSON ``` { "data": { "book": { "title": "C# in depth." } } } ``` So, by opting into this formatter feature, we will no longer serialize null fields. Relay now supports this, and you can opt for the same thing when using it. ## Developer Experience We developers generally like to write less code, or more precisely, to write less repetitive code. The more we can focus on building awesome APIs, the happier we are. This is one of our guiding principles when looking at features. This is why I like source generators so much: we can offload the tedious bits and let someone else write those. The other plus side is that we can still get best-in-class performance since things analyzed and generated with source generators at build time are already computed, with no overhead and unpredictability at runtime. ### Type Auto Registration With Hot Chocolate 13, we are embracing more features driven by source generators. Let me give you an example here. The following code shows you the GraphQL configuration of a smaller project with five entities without our source generators. C# ``` builder.Services .AddGraphQLServer() .AddQueryType() .AddMutationType() .AddSubscriptionType() .AddTypeExtension() .AddTypeExtension() .AddTypeExtension() .AddTypeExtension() .AddDataLoader() .AddTypeExtension() .AddTypeExtension() .AddTypeExtension() .AddTypeExtension() .AddDataLoader() .AddDataLoader() .AddTypeExtension() .AddTypeExtension() .AddTypeExtension() .AddDataLoader() .AddDataLoader() .AddTypeExtension() .AddTypeExtension() .AddTypeExtension() .AddDataLoader() .AddUploadType() .AddFiltering() .AddSorting() .AddGlobalObjectIdentification() .AddInMemorySubscriptions() .AddFileSystemQueryStorage("./persisted_queries") .UsePersistedQueryPipeline(); ``` And now, let's have a look at the same project with Hot Chocolate 13 and source generators. C# ``` builder.Services .AddGraphQLServer() .AddTypes() .AddUploadType() .AddFiltering() .AddSorting() .AddGlobalObjectIdentification() .AddInMemorySubscriptions() .AddFileSystemQueryStorage("./persisted_queries") .UsePersistedQueryPipeline(); ``` This is amazing! You focus on your code, and the Hot Chocolate source generator will write all those registrations for you. In our example which we migrated from Hot Chocolate 11 to 13, we reduced the configuration code from 32 lines to 10 lines of code. The best thing here is you will never again forget to register a type or DataLoader. ### DataLoader But this is not where this ends. One of the most dreaded pieces of code in a GraphQL project is the class DataLoader. DataLoader are amazing as they help you write APIs that take advantage of batched fetches to data sources and ensure that your graph is consistent. But they are just so much fricking code. C# ``` using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; using GreenDonut; namespace ConferencePlanner.GraphQL.DataLoader { public class TrackByIdDataLoader : BatchDataLoader { private readonly IDbContextFactory _dbContextFactory; public TrackByIdDataLoader( IDbContextFactory dbContextFactory, IBatchScheduler batchScheduler, DataLoaderOptions options) : base(batchScheduler, options) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); } protected override async Task> LoadBatchAsync( IReadOnlyList keys, CancellationToken cancellationToken) { await using ApplicationDbContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.Tracks .Where(s => keys.Contains(s.Id)) .ToDictionaryAsync(t => t.Id, cancellationToken); } } } ``` With Hot Chocolate 13, we are making DataLoader seamless and reducing them to the fetch function. Moreover, you can now co-locate them with the GraphQL-specific code you have for your entities. C# ``` [DataLoader] internal static async Task> GetTrackByIdAsync( IReadOnlyList ids, ApplicationDbContext context, CancellationToken cancellationToken) => await dbContext.Tracks .Where(s => ids.Contains(s.Id)) .ToDictionaryAsync(t => t.Id, cancellationToken); ``` The source generator will take the above code and generate the actual DataLoader for us, which you can consequently use in your resolvers, just as if you wrote all of this on your own. This works even with things like entity framework, where we could not execute with multiple threads on the same context. In the past, this would have made you write a ton of additional code to create scope or handle DBContextFactory. With Hot Chocolate 13, it's just one additional switch on the `DataLoaderAttribute`. C# ``` [DataLoader(ServiceScope = DataLoaderServiceScope.DataLoaderScope)] ``` ### Resolver Compiler While we love source generators, we also use runtime code generation to remove clutter further. When we register a DBContext globally, we actually register an `IParameterExpressionBuilder` that will analyze resolver code and generate and compile expressions at runtimes so that you get the best-optimized resolver possible with the least amount of code. We simplified how you can now write your own expression builder to handle global states or other things you want to simplify. C# ``` builder.Services .AddGraphQLServer() ... .AddParameterExpressionBuilder( ctx => ctx.GetGlobalStateOrDefault(nameof(ServiceState))) ``` For a deep dive into resolver compilers, you can watch the following YouTube episode: ### Directives Directives are one of the last APIs we had that were Code-First and Schema-First only but could not be created with the annotation-based approach. With Hot Chocolate 13, we have revamped directives, and they are now super sleek. C# ``` [DirectiveType(DirectiveLocation.Field)] public class MyQueryDirective { public MyQueryDirective(string myArg) { MyArg = myArg; } public string MyArg { get; } } ``` The above translates to the following directive. ``` directive @myQuery(myArg: String!) on FIELD ``` We can use this directive now right in our query. GraphQL ``` { book { title @myQuery(myArg: "abc") } } ``` If you want to learn more about the improvements we have made for GraphQL directives in Hot Chocolate 13, you can head over into the following video: ### JSON Scalar For some time, we had a scalar called Any, which allowed us to have some untyped data in our graph. But it was ugly how it was constructed with dictionary structures in our resolvers. Further, many of you just wanted to use clean JSON to specify the data. With Hot Chocolate 13, we are now introducing a clean JSON scalar that uses JsonElement as its runtime type. C# ``` [ExtendObjectType] public class BookResolvers { public JsonElement Variant1 => JsonDocument.Parse( """ { "a": 123 } """) .RootElement; [GraphQLType] public string Variant2 => """ { "a": 123 } """; } ``` We called it JSON and did not rework Any to keep your existing code working. You can, however, register the JSON scalar as any type if you want to use it in place of the Any scalar. C# ``` builder.Services .AddGraphQLServer() .AddTypes() .AddType(new JsonType("Any", BindingBehavior.Implicit)); ``` ### Generic Attributes With Hot Chocolate 13 we are taking advantage of generic attributes in .NET 7\. Instead of writing an ugly attribute like the following: C# ``` [ExtendObjectType(typeof(Foo))] public static class FooResolvers ``` You can no use it's generic version. C# ``` [ExtendObjectType] public static class FooResolvers ``` The same goes for many other projects. ### Entity Framework In the past, we have optimized Hot Chocolate to use the pooled factory approach when using Entity Framework. This did not sit well with many developers since it forced them to rewrite their long-established code patterns with scoped repositories. Hot Chocolate 13 will help you here and reduce the code and complexity of using Entity Framework to almost nothing. First, when you register a DBContext as a global service with the GraphQL schema, we will handle it as a resolver-scoped service. This means that the executor will create a service scope at the resolver level and retrieve this service from there. All other services that you might use in the resolver are still retrieved from the request service provider. This is important, especially as things like DataLoader enter the scene. The DBContext, in this case, can still be coming from a pool, but instead of using the factory configuration, you can now use the standard `AddDbContext` or the `AddDbContextPool`. Whatever makes you happy. C# ``` builder.Services.AddDbContextPool(o => o.UseSqlite("Data Source=assets.db")); ``` On our schema, we just register the `AssetContext` as a DBContext. C# ``` builder.Services .AddGraphQLServer() .AddTypes() .RegisterDbContext(); ``` With this registration, we essentially tell our resolver compiler about this well-known service and how to handle it. We now can just use it in our resolver, no attributes, no special code, nothing, just use it. C# ``` public static IQueryable GetAssets(AssetContext context) => context.Assets; ``` But I talked about repositories, and this again is about the DBContext. The DBContext is just a specialized well-known service to the GraphQL engine. You can do the same with any repository or service object registered with the DI. C# ``` builder.Services .AddGraphQLServer() .AddTypes() .RegisterService(ServiceKind.Resolver); ``` Just in the case of `RegisterService`, you have to explicitly opt into the resolver scoping since we default here to the request scope. But again, it's now one line of code in the GraphQL configuration, and you can use it everywhere without any clutter, as the resolver compiler will generate the code to keep the state. C# ``` public static async Task> GetAssets(AssetRepository repository) => await repository.GetAssetsAsync(); ``` ## Authorization Using the built-in authorization directives in Hot Chocolate was a pain. They only worked on fields and were executed for each field they were annotated to. So, basically, like with MVC, and this does not really fit into our graph world. Let me give you an example, given then the following schema: GraphQL ``` type Query { me: User userById(id: ID!): User } type User { name: String! friends: [User!] } ``` To secure our user object, we would need to annotate three fields. GraphQL ``` type Query { me: User @authorize userById(id: ID!): User @authorize } type User { name: String! friends: [User!] @authorize } ``` But if we now work on our schema and introduce new ways to get a user, we will need to continue ensuring it does not leak. This is tedious, and if we throw in unions and interfaces becomes very hard to manage. This is where our new authorization approach comes in. You can still annotate fields, but annotating object types will ensure that all fields they are retrievable through are secured with the specified authorization rules. This change alone makes it much easier to ensure your data is secure. GraphQL ``` type Query { me: User #secured because user is authorized userById(id: ID!): User #secured because user is authorized } type User @authorize { name: String! friends: [User!] #secured because user is authorized } ``` But we also wanted to improve the performance of authorization checks and move them, when possible, out of the execution phase. In Hot Chocolate 13, by default, authorization checks are done before the execution by analyzing the query document. If the document has authorization directives that cannot be fulfilled, it will not even execute. But, sometimes, we need our authorization logic to run in the resolver, either to get the data and authorize by using the actual data it protects or to use the context in the resolver to authorize. This can be easily done by specifying when an `@authorize` directive shall be applied. GraphQL ``` type Query { me: User #secured because user is authorized userById(id: ID!): User #secured because user is authorized } type User @authorize @authorize(policy: "READ_USER", apply: AFTER_RESOLVER) { name: String! friends: [User!] #secured because user is authorized } ``` | Phase | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | VALIDATION | The authorization directives are collected and applied in a batch during the validation phase of the document. | | BEFORE\_RESOLVER | The authorization directives are merged into the resolver pipeline and executed before the resolver. The authorize policies have access to the IMiddlewareContext but not to the resolved data. | | AFTER\_RESOLVER | The authorization directives are merged into the resolver pipeline and executed after the resolver. The authorize policies have access to the IMiddlewareContext and the resolved data. | ### Open Policy Agent With Hot Chocolate 13, we have abstracted our authorization API and can now support multiple authorization solutions. You can even create your own if you want to. All you have to do is to implement the `IAuthorizationHandler` interface. C# ``` public interface IAuthorizationHandler { ValueTask AuthorizeAsync( IMiddlewareContext context, AuthorizeDirective directive, CancellationToken cancellationToken = default); ValueTask AuthorizeAsync( AuthorizationContext context, IReadOnlyList directives, CancellationToken cancellationToken = default); } ``` See [IAuthorizationHandler.cs](https://github.com/ChilliCream/graphql-platform/blob/main/src/HotChocolate/Core/src/Authorization/IAuthorizationHandler.cs) for more details. Out of the box, we support Microsoft's authorization policies that come with ASP.NET Core and OPA (Open Policy Agent). OPA is getting increasingly popular and can be applied to things from Kubernetes to your database, and it is now just one package away from your favorite GraphQL server. Bash ``` dotnet install HotChocolate.AspNetCore.Authorization.Opa ``` If you want to learn more about Open Policy agent, you can find more information [here](https://www.openpolicyagent.org/). ## Subscriptions Subscription is another area where we put a lot of effort into. With Hot Chocolate 12, we had support for Redis as a backing pub-sub, and if you ran a single instance of your service, you could have used our in-memory implementation. Now with Hot Chocolate 13, we have added support for NATS by using the [AlterNATS C# library](https://github.com/Cysharp/AlterNats). We also added support for RabbitMQ, a popular solution many of you asked us to support for subscriptions. Implementing a new subscription provider now also has become so much easier. If you want to support another system, look at the [NATS implementation](https://github.com/ChilliCream/graphql-platform/tree/main/src/HotChocolate/Core/src/Subscriptions.Nats). ## Data As with almost every release, we have added more integrations to HotChocolate.Data. With Hot Chocolate 13, we are happy to announce built-in support for RavenDB and Marten. Here is an example of how easy it is now to integrate RavenDB with Hot Chocolate 13. 1. Install the RavenDB provider to your project. Bash ``` dotnet install HotChocolate.Data.Raven ``` 1. Register your document store. C# ``` builder.Services.AddSingleton( _ => new DocumentStore { Urls = new[] { "http://localhost:8080" }, Database = "Test" }.Initialize()); ``` 1. Register, Filtering, Sorting, and Paging providers for RavenDB. C# ``` builder.Services .AddGraphQLServer() .AddTypes() .AddRavenFiltering() .AddRavenProjections() .AddRavenSorting() .AddRavenPagingProviders(); ``` 1. Next, we can introduce some resolvers, and we are done. C# ``` [QueryType] public class Query { [UsePaging] [UseProjection] [UseFiltering] [UseSorting] public IRavenQueryable GetPersons(IAsyncDocumentSession session) => session.Query(); [UseFirstOrDefault] [UseFiltering] public IExecutable GetPerson(IAsyncDocumentSession session) => session.Query().AsExecutable(); } ``` The Marten integration works very similarly. The main difference here is that you have to install a different package. Bash ``` dotnet install HotChocolate.Data.Marten ``` ## Azure Functions With version 12, we introduced the Azure Functions integration but only targeted in-process Azure Functions. Now, with Hot Chocolate 13, we have doubled down on Azure Functions and provided the ability to now run in the isolated process model, along with templates for both. 1. Install the HotChocolate Templates. Bash ``` dotnet new install HotChocolate.templates ``` 2. Chose your template to install or take a spin with both. Bash ``` dotnet new graphql-azf --output .\hc-graphql-azf dotnet new graphql-azf-ip --output .\hc-graphql-azf-ip ``` Or use Visual Studio. ![HotChocolate Azure Functions Project Templates](https://chillicream.com/images/blog/2023-02-08-new-in-hot-chocolate-13/az-func-templates-vs.png) ## Performance Performance is, in every release, a core concern that we have. For this release, we have looked at the memory consumption of the execution engine and were able to reduce consumption by 78% while at the same time improving execution performance by 24%. | Method | Mean | Gen0 | Gen1 | Allocated | | ------------------------------------------------------ | ---------- | ------- | ------ | --------- | | Hot Chocolate 12.17.0 / Introspection Query | 221.2 us | 13.6719 | 0.2441 | 84.13 KB | | Hot Chocolate 13.0.2 / Introspection Query | 167.9 us | 2.9297 | 0.4883 | 18.73 KB | | Hot Chocolate 12.17.0 / 5 parallel Introspection Query | 1,030.2 us | 68.3594 | \- | 420.63 KB | | Hot Chocolate 13.0.2 / 5 parallel Introspection Query | 835.8 us | 14.6484 | 2.9297 | 93.63 KB | As always, we micro-optimize Hot Chocolate to make more room for your own application logic. What these optimizations mean in your use case might be very different. ## Strawberry Shake While we did not have a strong focus on Strawberry Shake for this release, we wanted to address some user pain points. The first one was that it was too complex to set up and configure since you needed to fill in configuration and match it with the right packages. With Strawberry Shake 13, we wanted to improve the developer experience and simplify things. We now have three application profiles which translate to three meta-packages, Blazor, Maui, and Server. | Package | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | StrawberryShake.Blazor | For Blazor projects, use this package in your project, and we pre-configured it to generate Razor components automatically and use a client-side store for reactive web applications. | | StrawberryShake.Maui | For Maui projects, use this package in your project, and we pre-configured it to generate and use a client-side store for reactive mobile applications. | | StrawberryShake.Server | For consoles or backend-to-backend communication, we have the service profile, which does not have a client store but gives you a strongly typed client. | Here is a basic flow to initialize a project with a Strawberry Shake client. 1. Create your project. Bash ``` dotnet new blazorwasm ``` 2. Add client tooling to manage the GraphQL schema. Bash ``` dotnet new tool-manifest dotnet tool install StrawberryShake.Tools ``` 3. Register GraphQL service with your application. Bash ``` dotnet add package StrawberryShake.Blazor dotnet graphql init https://api-crypto-workshop.chillicream.com/graphql -n CryptoClient ``` With these three steps, you are good to go and can start creating clients for your project. We also made it simpler to opt-in to features like persisted queries. Configuration options are now neatly integrated into the project file. To export queries on deployment as persisted queries, you add one property, `GraphQLPersistedQueryOutput`, to your project file, and you are done. XML ``` net7.0 enable enable ../output ``` If you need a deep dive into the setup of persisted queries with Strawberry Shake, you can have a look at the following YouTube episode. ## Banana Cake Pop With version 13, we are also releasing Banana Cake Pop 4, which packs many new features. You can read all about this in the [Banana Cake Pop 4 announcement](https://chillicream.com/blog/2023-02-07-new-in-banana-cake-pop-4). ## Outlook There are many more features and fixes in Hot Chocolate 13; too many to go into each of them. Instead, let me give you a couple of numbers around this release. We had 81 contributors, including the core team working on Hot Chocolate 13, and more than 400 PRs went into this release. Not all of them were code; some were bits and pieces of documentation, unit tests, code fixes, or even complete features. The Marten database provider, for instance, was contributed to us by a single member of the community. When I saw this, I remembered the time it was just me. I remember when my brother Rafi and I started the slack channel, and there was this single other person in there asking me questions about Hot Chocolate. Now we are over 4400 on slack.chillicream.com. Since we were so many people working on this release, I do not want to mention one or two specific names here. It was all of us together who pushed this forward. You can have a look at the GitHub release for all the people who got their commits into main. [Release Version 13.0.0](https://github.com/ChilliCream/graphql-platform/releases/tag/13.0.0) The team will focus on tooling and Hot Chocolate Fusion for the next couple of months. We want to make distributed graphs much simpler and help you with great tooling to build and manage graphs at a massive scale. Further down the road, we will focus on AOT without compromise to enable much faster startup times. Also on our list is an overhaul of HotChocolate.Data, we want to make aggregations easier and also simplify creating custom providers. Let's take the next step and get even more people into our community. ## You might also like [View all](https://chillicream.com/blog) --- # New in Banana Cake Pop 4 > New document on paste cURL or fetch, schema reload enhancements, new ways of closing tabs, menu enhancements/standardization, and UI polishing. Canonical source: https://chillicream.com/blog/2023-02-07-new-in-banana-cake-pop-4 ![](https://chillicream.com/images/blog/2023-02-07-new-in-banana-cake-pop-4/new-in-banana-cake-pop-4.png) [![Rafael Staib's avatar](https://chillicream.com/_optimized/images/remote/47f10f3229fb2cfedca9993792f328fe15c7670ac3ed9d4e777b11374b3e4907.png)Rafael Staib](https://chillicream.com/authors/rafael-staib)2023-02-073 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2023-02-07-new-in-banana-cake-pop-4&text=New+in+Banana+Cake+Pop+4) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2023-02-07-new-in-banana-cake-pop-4) - [bananacakepop](https://chillicream.com/blog/tags/bananacakepop) - [graphql](https://chillicream.com/blog/tags/graphql) - [ide](https://chillicream.com/blog/tags/ide) - [cloud](https://chillicream.com/blog/tags/cloud) - [release](https://chillicream.com/blog/tags/release) In version 4, we mainly focused on polishing and fixing UI glitches to improve the overall user experience, but that doesn't mean there isn't any new feature. To download **Banana Cake Pop 4**, go to [bananacakepop.com](https://bananacakepop.com/). Let me walk you through the most important things we did in Version 4. ## Paste cURL and Fetch Almost any IDE or Tool nowadays allows copying HTTP requests as cURL or fetch. So why not make use of it? Yeah, that's what we thought too. With version 4, we support pasting `cURL` and `fetch` GraphQL requests into **Banana Cake Pop**. When pasting such a GraphQL request, a new document will be created with all its HTTP headers, GraphQL variables, GraphQL operation, and the GraphQL endpoint. This is very helpful in various scenarios, especially when working with the Chrome Developer Tools to identify GraphQL request issues. Just go to the Network tab, right-click on any HTTP request, and then _copy as cURL_. As long as the copied HTTP request is a GraphQL request, **Banana Cake Pop** will create a new document on paste. There are two ways of creating a new document for a copied `cURL` or `fetch` GraphQL request. First, use the shortcut. `CMD + OPT + V` on macOS and `CTRL + ALT + V` on windows. Second, click the three dots icon next to the save button for tabs, and then click _New document from clipboard_. Ta-da, that's it! ## Schema Reload Sometimes, reloading a schema takes just a couple of milliseconds, which makes it rather impossible to see the loading indicator spinning. In general, feedback is critical when clicking a button, so we know whether we clicked it. The same goes for the schema reload button. Without knowing whether we clicked it, we’ll click it again and again. In the end, we come to the conclusion that the schema reload does not work. In fact, this isn't true, but how should we know? Of course, we solved this issue by adding a decent pulse effect to the schema reload button, which keeps going for a couple of seconds. Additionally, we merged the schema reload button with the schema fetch status to keep things clear and compact. Furthermore, we improved the schema fetch status, including its tooltip in the status bar, which makes things more explicit. ## Close Multiple Document Tabs Finally, after quite some time, we've added a bunch of ways to close multiple document tabs simultaneously. Right-click on a document tab and choose between _close others_, _close to the right_, and _close all_. Enjoy! ## Menu Enhancements We've standardized and enhanced the menu component. We've fixed positioning issues and glitches. Also, we introduce navigation by key (arrow up and down). ## Schema Reference Default Values Default values for fields in the schema reference column and type view are now available. ## Become An Insider Hey you, we're looking for you to become an _Insider_ to help us shape the future of **Banana Cake Pop**. We're constantly pushing new _Insider_ builds, sometimes even daily. Get early access to new features, or help us find that last-minute show-stopper issue. Go to [bananacakepop.com](https://bananacakepop.com/) to get the latest version of the _Insider_ app or check out the online web version on [insider.bananacakepop.com](https://insider.bananacakepop.com/) instead. ## Subscribe To stay up to date, subscribe to our [ChilliCream YouTube Channel](https://www.youtube.com/c/ChilliCream) to get notified whenever we publish new videos. I'm Rafael Staib, and as soon as **Banana Cake Pop 5** is released, I'll be right here to tell you what's new in **Banana Cake Pop**! ## You might also like [View all](https://chillicream.com/blog) --- # New in Banana Cake Pop 3 > Team Workspaces, Express Middleware, Progressive Web Application (PWA) Support, Enterprise Single Sign-On (SSO), and many more features. Canonical source: https://chillicream.com/blog/2023-01-08-new-in-banana-cake-pop-3 ![](https://chillicream.com/images/blog/2023-01-08-new-in-banana-cake-pop-3/new-in-banana-cake-pop-3.png) [![Rafael Staib's avatar](https://chillicream.com/_optimized/images/remote/47f10f3229fb2cfedca9993792f328fe15c7670ac3ed9d4e777b11374b3e4907.png)Rafael Staib](https://chillicream.com/authors/rafael-staib)2023-01-082 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2023-01-08-new-in-banana-cake-pop-3&text=New+in+Banana+Cake+Pop+3) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2023-01-08-new-in-banana-cake-pop-3) - [bananacakepop](https://chillicream.com/blog/tags/bananacakepop) - [graphql](https://chillicream.com/blog/tags/graphql) - [ide](https://chillicream.com/blog/tags/ide) - [cloud](https://chillicream.com/blog/tags/cloud) - [release](https://chillicream.com/blog/tags/release) Version 3 comes with a couple of neat features, e.g. Team Workspaces, Express Middleware, PWA Support, Enterprise SSO, and more. If you would like to download **Banana Cake Pop 3**, go to [bananacakepop.com](https://bananacakepop.com/). Now lets see what’s new in detail. ## Team Workspaces In **Banana Cake Pop 1**, we've introduced _Personal Workspaces_ for individuals to keep documents safe across devices. _Personal Workspaces_ is a free tier feature and will stay forever free. However, this time we are introducing a new feature called _Team Workspaces_. _Team Workspaces_, as the name implies, allow teams to work together and share documents. To create _Team Workspaces_ an _Organization_ is required and can be created by anyone. _Organization_ is in preview and becomes a paid feature when the preview phase ends. We'll inform you as soon as the preview phase ends so that you can decide whether to use it. ## Express Middleware We launched our first NodeJS middleware on [NPM](https://www.npmjs.com/package/@chillicream/bananacakepop-express-middleware). You can plug **Banana Cake Pop** to your own GraphQL server with just 2 lines of code. Also, you can define per configuration whether to use our _cdn_ hosted version of the app or your own _self_ hosted version, and much more. Check out our [recipes](https://www.npmjs.com/package/@chillicream/bananacakepop-express-middleware#recipes) for [graphql-http](https://github.com/graphql/graphql-http), [graphql-yoga](https://the-guild.dev/graphql/yoga-server), and [express-graphql](https://github.com/graphql/express-graphql)! ## Progressive Web App (PWA) With version 3, **Banana Cake Pop** meets the PWA requirements, which allows the web version to be installed as an app. In Chrome on macOS, it looks like this. ![Banana Cake Pop PWA](https://chillicream.com/images/blog/2023-01-08-new-in-banana-cake-pop-3/banana-cake-pop-pwa.png) ## Enterprise Single Sign-On (SSO) We've added Enterprise Single Sign-On (SSO) to **Banana Cake Pop**, which lets companies bring their own identity service, so that their employees can use their company logins. Please reach out to us if you're interested. ## Enterprise Services For companies, we’re introducing **Banana Cake Pop** enterprise services, which can be deployed under their own Azure subscription. This gives companies control over their own data. Please reach out to us if you're interested. ## Insider Web Version We brought the _Insider_ version to the web. Get early access to new features, or even help us find that last-minute show stopper issue. Check it out here: [insider.bananacakepop.com](https://insider.bananacakepop.com/). ## Subscribe To stay up to date, subscribe to our [ChilliCream YouTube Channel](https://www.youtube.com/c/ChilliCream) to get notified whenever we publish new videos. I'm Rafael Staib, and as soon as **Banana Cake Pop 4** is released, I'll be right here to tell you what's new in **Banana Cake Pop**! ## You might also like [View all](https://chillicream.com/blog) --- # New in Banana Cake Pop 2 > Drag & drop support for documents, GraphQL defer/stream support, GraphQL operation extraction, and many further improvements. Canonical source: https://chillicream.com/blog/2022-10-05-new-in-banana-cake-pop-2 [![Rafael Staib's avatar](https://chillicream.com/_optimized/images/remote/47f10f3229fb2cfedca9993792f328fe15c7670ac3ed9d4e777b11374b3e4907.png)Rafael Staib](https://chillicream.com/authors/rafael-staib)2022-10-052 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2022-10-05-new-in-banana-cake-pop-2&text=New+in+Banana+Cake+Pop+2) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2022-10-05-new-in-banana-cake-pop-2) - [bananacakepop](https://chillicream.com/blog/tags/bananacakepop) - [graphql](https://chillicream.com/blog/tags/graphql) - [ide](https://chillicream.com/blog/tags/ide) - [cloud](https://chillicream.com/blog/tags/cloud) - [release](https://chillicream.com/blog/tags/release) ## Getting started Everything you need to get started with **Banana Cake Pop** you'll find on [bananacakepop.com](https://bananacakepop.com/) ## Drag & Drop We've added _Drag & Drop_ support to the document explorer. That makes moving documents, files and folders around so much easier. Moreover, we've added support for dropping files for _File Upload_ from outside. Just drag one or multiple files (e.g. a photo) from your computer and drop it on the document explorer root or a specific folder. ## Defer/Stream Spec We've updated to the latest Defer/Stream spec draft version, but with backward-compatibility in mind. It still works with previous versions of Hot Chocolate or other servers that implemented the prior spec version. ## Operation Extraction We've optimized how GraphQL operations are sent over the wire. Before we send an operation, we remove all the superfluous operations and fragments. For instance, if we have two queries in a GraphQL document, query `A` and `B`, we send only the query and its fragments we execute. Such a document could look like the following. GraphQL ``` query A { me { ...UserFragment } } query B { me { ...UserFragment friends { ...FriendFragment } } } fragment UserFragment on User { name image } fragment FriendFragment on User { ...User age } ``` If we execute query `A`, for example, the request would look like the following. GraphQL ``` query A { me { ...UserFragment } } fragment UserFragment on User { name image } ``` With this technique, we didn't only reduce the request overhead but were also able to send query `A` even though query `B` is not valid. ## Horizontal Scrolling for Tabs We've added horizontal scrolling on tabs for mice with a scroll wheel. Instead of scrolling up/down, we switched to scrolling left/right. Simply hover over tabs that contain partly visible tabs and use the scroll wheel of the mouse to move hidden tabs into the visible area. ## Insider Version We start now with insider versions for the Electron app, which will run side-by-side with the released app version. Follow this link [bananacakepop.com](https://bananacakepop.com/) to download the first insider build. ## Further Improvements A few more, worth mentioning, improvements are listed below. 1. Increased efficiency of workspace synchronization 2. Reduced Electron app size 3. Removed app leave warning prompt 4. Increased editor performance ## Subscribe To stay up to date, subscribe to our [ChilliCream YouTube Channel](https://www.youtube.com/c/ChilliCream) to get notified whenever we publish new videos. I'm Rafael Staib, and as soon as **Banana Cake Pop 3** is released, I'll be right here to tell you what's new in **Banana Cake Pop**! ## You might also like [View all](https://chillicream.com/blog) --- # New in Banana Cake Pop 1 > Subscription protocol auto-detection, Workspace auto synchronization, a Status Bar, and many further improvements. Canonical source: https://chillicream.com/blog/2022-09-01-new-in-banana-cake-pop-1 [![Rafael Staib's avatar](https://chillicream.com/_optimized/images/remote/47f10f3229fb2cfedca9993792f328fe15c7670ac3ed9d4e777b11374b3e4907.png)Rafael Staib](https://chillicream.com/authors/rafael-staib)2022-09-011 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2022-09-01-new-in-banana-cake-pop-1&text=New+in+Banana+Cake+Pop+1) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2022-09-01-new-in-banana-cake-pop-1) - [bananacakepop](https://chillicream.com/blog/tags/bananacakepop) - [graphql](https://chillicream.com/blog/tags/graphql) - [ide](https://chillicream.com/blog/tags/ide) - [cloud](https://chillicream.com/blog/tags/cloud) - [release](https://chillicream.com/blog/tags/release) ## Getting started Everything you need to get started with **Banana Cake Pop** you'll find on [bananacakepop.com](https://bananacakepop.com/) ## Subscription protocol auto-detection Auto-detection can save you from a headache when it comes to the following questions: - _Which subscription protocol is supported by server XYZ?_ - _Which subscription protocol prefers server XYZ?_ The new auto-detection is enabled by default. If you prefer a particular subscription protocol, you can change it in the **Connection Settings** dialog. ![Subscription protocol](https://chillicream.com/images/blog/2022-09-01-new-in-banana-cake-pop-1/subscription-protocol-auto-detection-1.png) **Banana Cake Pop** supports the following subscription protocols. ![Supported subscription protocols](https://chillicream.com/images/blog/2022-09-01-new-in-banana-cake-pop-1/subscription-protocol-auto-detection-2.png) ## Workspace auto synchronization Your local and remote workspace changes will be synchronized every 60 seconds automatically. No need for you to click the synchronize button anymore. The new synchronize button is now located in the status bar and is also an indicator, showing you whether a workspace is synchronizing. ![Workspace auto synchronization](https://chillicream.com/images/blog/2022-09-01-new-in-banana-cake-pop-1/workspace-auto-synchronization-1.png) ## Status bar The new decent status bar shows you a couple of information. For instance, is my browser connected to a network, or with what account am I signed in? ![Network status and username](https://chillicream.com/images/blog/2022-09-01-new-in-banana-cake-pop-1/status-bar-1.png) The status bar can also contain context-related information. For instance, is the schema of my current document up to date? ![Schema status](https://chillicream.com/images/blog/2022-09-01-new-in-banana-cake-pop-1/status-bar-2.png) Furthermore, we introduced a new button in the right corner that shows the current version of **Banana Cake Pop** when clicking it. ![Version info](https://chillicream.com/images/blog/2022-09-01-new-in-banana-cake-pop-1/status-bar-3.png) ## Hidden Tabs We introduced a new button that appears when a tab is at least partly hidden. This button, when clicked, will show a menu with partly hidden and completely hidden tabs. ![Version info](https://chillicream.com/images/blog/2022-09-01-new-in-banana-cake-pop-1/hidden-tabs-1.png) ## Subscribe To stay up to date, subscribe to our [ChilliCream YouTube Channel](https://www.youtube.com/c/ChilliCream) to get notified whenever we publish new videos. I'm Rafael Staib, and as soon as **Banana Cake Pop 2** is released, I'll be right here to tell you what's new in **Banana Cake Pop**! ## You might also like [View all](https://chillicream.com/blog) --- # Pushing ahead with Hot Chocolate 12.5 > Hot Chocolate 12.5 adds Banana Cake Pop themes, OpenTelemetry instrumentation, oneOf input objects, and client-controlled nullability. Canonical source: https://chillicream.com/blog/2022-01-13-hot-chocolate-12-5 ![](https://chillicream.com/images/blog/2022-01-13-hot-chocolate-12-5/hot-chocolate-12-5-banner.png) [![Michael Staib's avatar](https://chillicream.com/_optimized/images/remote/2bb0dd6e1b9347a1d3732300ea752f39f1ac4f1f76587433ca3a407245b31786.jpg)Michael Staib](https://chillicream.com/authors/michael-staib)2022-01-135 min read [Share this post on X](https://x.com/intent/tweet?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2022-01-13-hot-chocolate-12-5&text=Pushing+ahead+with+Hot+Chocolate+12.5) [Share this post on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fchillicream.com%2Fblog%2F2022-01-13-hot-chocolate-12-5) - [hotchocolate](https://chillicream.com/blog/tags/hotchocolate) - [graphql](https://chillicream.com/blog/tags/graphql) - [dotnet](https://chillicream.com/blog/tags/dotnet) - [aspnetcore](https://chillicream.com/blog/tags/aspnetcore) Today we have released Hot Chocolate 12.5, and this release is packed with new features. We put a focus on adding some early spec proposals into this release. We also have completely overhauled our GraphQL IDE Banana Cake Pop to include feedback from our community. Lastly, we picked up an issue created by Simon to support OpenTelemetry. ## Banana Cake Pop Let us start with the most visible change to Hot Chocolate. With Hot Chocolate 12.5, we have integrated Banana Cake Pop iteration 22, which introduces themes support. One of the top requests for BCP by users was a Dark mode. With the new version, you can now switch between our light and our dark theme. We will add more themes with one of the subsequent iterations. ![Banana Cake Pop Themes](https://chillicream.com/images/blog/2022-01-13-hot-chocolate-12-5/bcp1.png) We put another focus on discoverability. Many people getting into BCP had difficulty finding the schema explorer or other details regarding their operation document. With the new version, the IDE is much more organized and exposes clearly areas you can dig into. ![Banana Cake Pop Tabs](https://chillicream.com/images/blog/2022-01-13-hot-chocolate-12-5/bcp2.png) The new Banana Cake Pop version is now available online at [https://eat.bananacakepop.com](https://eat.bananacakepop.com/), as an application that you can download at [https://bananacakepop.com](https://bananacakepop.com/) or as a middleware in the new Hot Chocolate 12.5. ## Open Telemetry Hot Chocolate for a long time provides instrumentation events that can be used to add your logging solution. By doing this, we did not bind Hot Chocolate to a specific logging/tracing solution or a specific use-case. But it also meant that almost everyone had to come up with their own solution to instrument Hot Chocolate. With Hot Chocolate 12.5, we have added the `HotChocolate.Diagnostics` package, which uses the new `ActivitySource` API. To add OpenTelemetry to your GraphQL server, first add the activity instrumentation to your schema. C# ``` builder.Services .AddGraphQLServer() .AddQueryType() .AddInstrumentation(); ``` Next, we need to configure OpenTelemetry for our service. To quickly inspect our traces, we will use a Jaeger exported. C# ``` builder.Services.AddOpenTelemetryTracing( b => { b.AddHttpClientInstrumentation(); b.AddAspNetCoreInstrumentation(); b.AddHotChocolateInstrumentation(); b.AddJaegerExporter(); }); builder.Logging.AddOpenTelemetry( b => { b.IncludeFormattedMessage = true; b.IncludeScopes = true; b.ParseStateValues = true; }); ``` With all this in place, we can execute requests against our demo server and inspect the traces with the Jaeger UI. ![Banana Cake Pop Themes](https://chillicream.com/images/blog/2022-01-13-hot-chocolate-12-5/jaeger.png) The complete example can be found [here](https://github.com/ChilliCream/hotchocolate-examples/tree/master/misc/OpenTelemetry). Docs can be found [here](https://chillicream.com/docs/hotchocolate/server/instrumentation#opentelemetry). ## `OneOf` Input Objects One of the most asked-for features in GraphQL is input unions. The GraphQL working group has been discussing this feature for a long time, and we have explored multiple roads to achieve this. The most likely candidate has become the _`OneOf` Input Object_ representing a structural union. A structural union means that _`OneOf` Input Object_ is a special kind of input object where each field represents one choice. The _`OneOf` Input Object_ will only allow one field to be set, and the value can not be null. The type system enforces the rules for `OneOf` Input Objects\_. We support _`OneOf` Input Objects_ in all three schema-building approaches (annotation-based, code-first, and schema-first. In order to make an input object a _`OneOf` Input Object_ you simply need to annotate it with the `@oneOf` directive. **schema-first** SDL ``` input PetInput @oneOf { cat: CatInput dog: DogInput } ``` **code-first** C# ``` public class PetInputType : InputObjectType { protected override void Configure( IInputObjectTypeDescriptor descriptor) { descriptor.OneOf(); } } public class PetInput { public Dog? Dog { get; set; } public Cat? Cat { get; set; } } ``` **annotation-based** C# ``` [OneOf] public class PetInput { public Dog? Dog { get; set; } public Cat? Cat { get; set; } } ``` Next, you need to enable the RFC feature on the schema. C# ``` builder.Services .AddGraphQLServer() ... .ModifyOptions(o => o.EnableOneOf = true); ``` The complete example can be found [here](https://github.com/ChilliCream/hotchocolate-examples/tree/master/misc/OneOf). Docs can be found [here](https://chillicream.com/docs/hotchocolate/defining-a-schema/input-object-types#oneof-input-objects). The current GraphQL spec RFC can be found [here](https://github.com/graphql/graphql-spec/pull/825). ## Client-Controlled Nullability Client-Controlled nullability gives more power to the consumer of a GraphQL API. It allows us to specify error boundaries in GraphQL by defining if a field shall be nullable or required in our GraphQL request. To give this power to the user, the RFC introduces new query syntax to let the user override type nullability on fields and specify where error boundaries are in the GraphQL request. Let us, for instance, say we have a schema like the following: GraphQL ``` type Query { me: User } type User { name: String! bio: String! friends: [User!] } ``` We have a user object with a name, a bio, and friends in our schema. Let's now consider we have a simple query where we fetch the currently signed-in user and that user's friends. GraphQL ``` { me { name bio friends { name bio } } } ``` In our schema, the field `bio` is a non-null field, and whenever this field would become null due to a processing error or invalid data, the GraphQL non-null propagation rule would erase all friends. JSON ``` { "me": { "name": "Michael Staib", "bio": "Author of Hot Chocolate ...", "friends": null } } ``` With client-controlled nullability, the API consumer can now change this behavior by overriding the field type nullability. GraphQL ``` { me { name bio friends { name bio? } } } ``` We can tell the execution engine that we do not mind if this field becomes `null` by adding a question mark. But we could also approach this differently and say if the field `bio` does not deliver any data, I do not want a partial result where `friends` becomes `null`. I instead want to have no data at all and fail the complete request. GraphQL ``` { me! { name bio friends! { name bio } } } ``` So, in this case, I added the bang operator to the field `me` and the field `friends`. In GraphQL, a non-null violation will bubble up until it reaches a nullable field or until the complete result is deleted. Since we made the root non-null, the complete result, in this case, is deleted. Meaning either I get all the data I demanded or none. We could also produce null entries in our `friends` list for users that do not provide a value for the field `bio` with the new list nullability modifier `[?]`. GraphQL ``` { me! { name bio friends[?] { name bio } } } ``` At the moment, Banana Cake Pop is not updated for the new syntax yet. We will do that in the coming days. But you can write and execute the new syntax already since BCP will allow you to execute even with syntax errors. We hope to introduce an updated language server with the next iteration. The current GraphQL spec RFC can be found [here](https://github.com/graphql/graphql-spec/pull/895). ## Conclusion We have implemented a ton of other smaller additions and bug fixes. Hot Chocolate 12.5 pushes further ahead and allows you to opt into the newest GraphQL spec proposals and drafts. At the GraphQL working group, we are currently discussing great new additions to the GraphQL spec like fragment modularity and object identity. Together, stream/defer, OneOf, fragment modularity, object identity, and client-controlled nullability could make GraphQL so much better and help us solve fundamental problems in interacting with our data graphs. We have invested in these new features early and are iterating on these as the spec text matures. ## You might also like [View all](https://chillicream.com/blog)