From d58fa79757fa8806e270c8c62e75ef5f57bee75f Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:43:33 +0200 Subject: [PATCH 1/5] Fix heading levels --- .../2022-09-01-new-in-banana-cake-pop-1.md | 12 +++---- .../2022-10-05-new-in-banana-cake-pop-2.md | 16 ++++----- .../2023-01-08-new-in-banana-cake-pop-3.md | 14 ++++---- .../2023-02-07-new-in-banana-cake-pop-4.md | 14 ++++---- .../blog/2025-02-01-hot-chocolate-15.md | 18 +++++----- .../blog/2026-05-11-hot-chocolate-16.md | 26 +++++++------- .../content/blog/2026-05-15-fusion-16.md | 26 +++++++------- .../2026-05-28-mcp-hotchocolate-fusion.md | 34 +++++++++---------- .../blog/2026-06-05-introducing-skillz.md | 8 ++--- .../blog/2026-06-06-newsletter-may-2026.md | 10 +++--- ...6-11-open-your-graphql-api-for-the-rest.md | 8 ++--- 11 files changed, 93 insertions(+), 93 deletions(-) diff --git a/website-next/content/blog/2022-09-01-new-in-banana-cake-pop-1.md b/website-next/content/blog/2022-09-01-new-in-banana-cake-pop-1.md index 6b8070f96a8..15b1ab3a5ea 100644 --- a/website-next/content/blog/2022-09-01-new-in-banana-cake-pop-1.md +++ b/website-next/content/blog/2022-09-01-new-in-banana-cake-pop-1.md @@ -11,11 +11,11 @@ authorUrl: https://github.com/rstaib authorImageUrl: https://avatars0.githubusercontent.com/u/4325318?s=100&v=4 --- -## Getting started +# 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 +# Subscription protocol auto-detection Auto-detection can save you from a headache when it comes to the following questions: @@ -30,13 +30,13 @@ The new auto-detection is enabled by default. If you prefer a particular subscri ![Supported subscription protocols](../../public/images/blog/2022-09-01-new-in-banana-cake-pop-1/subscription-protocol-auto-detection-2.png) -## Workspace auto synchronization +# 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](../../public/images/blog/2022-09-01-new-in-banana-cake-pop-1/workspace-auto-synchronization-1.png) -## Status bar +# 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? @@ -50,13 +50,13 @@ Furthermore, we introduced a new button in the right corner that shows the curre ![Version info](../../public/images/blog/2022-09-01-new-in-banana-cake-pop-1/status-bar-3.png) -## Hidden Tabs +# 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](../../public/images/blog/2022-09-01-new-in-banana-cake-pop-1/hidden-tabs-1.png) -## Subscribe +# 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. diff --git a/website-next/content/blog/2022-10-05-new-in-banana-cake-pop-2.md b/website-next/content/blog/2022-10-05-new-in-banana-cake-pop-2.md index 8fa4f9eb547..fbf6cf5af1c 100644 --- a/website-next/content/blog/2022-10-05-new-in-banana-cake-pop-2.md +++ b/website-next/content/blog/2022-10-05-new-in-banana-cake-pop-2.md @@ -11,19 +11,19 @@ authorUrl: https://github.com/rstaib authorImageUrl: https://avatars0.githubusercontent.com/u/4325318?s=100&v=4 --- -## Getting started +# Getting started Everything you need to get started with **Banana Cake Pop** you'll find on [bananacakepop.com](https://bananacakepop.com) -## Drag & Drop +# 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 +# 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 +# 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. @@ -71,15 +71,15 @@ fragment UserFragment on User { 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 +# 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 +# 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 +# Further Improvements A few more, worth mentioning, improvements are listed below. @@ -88,7 +88,7 @@ A few more, worth mentioning, improvements are listed below. 1. Removed app leave warning prompt 1. Increased editor performance -## Subscribe +# 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. diff --git a/website-next/content/blog/2023-01-08-new-in-banana-cake-pop-3.md b/website-next/content/blog/2023-01-08-new-in-banana-cake-pop-3.md index 74063fa5d29..afbe3eaa673 100644 --- a/website-next/content/blog/2023-01-08-new-in-banana-cake-pop-3.md +++ b/website-next/content/blog/2023-01-08-new-in-banana-cake-pop-3.md @@ -12,13 +12,13 @@ authorImageUrl: https://avatars0.githubusercontent.com/u/4325318?s=100&v=4 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 +# 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 +# Express Middleware We launched our first NodeJS middleware on [NPM](https://www.npmjs.com/package/@chillicream/bananacakepop-express-middleware). @@ -26,27 +26,27 @@ You can plug **Banana Cake Pop** to your own GraphQL server with just 2 lines of 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) +# 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](../../public/images/blog/2023-01-08-new-in-banana-cake-pop-3/banana-cake-pop-pwa.png) -## Enterprise Single Sign-On (SSO) +# 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 +# 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 +# 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 +# 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. diff --git a/website-next/content/blog/2023-02-07-new-in-banana-cake-pop-4.md b/website-next/content/blog/2023-02-07-new-in-banana-cake-pop-4.md index 2085b3f305e..21f56a644ea 100644 --- a/website-next/content/blog/2023-02-07-new-in-banana-cake-pop-4.md +++ b/website-next/content/blog/2023-02-07-new-in-banana-cake-pop-4.md @@ -12,7 +12,7 @@ authorImageUrl: https://avatars0.githubusercontent.com/u/4325318?s=100&v=4 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 +# 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**. @@ -22,7 +22,7 @@ This is very helpful in various scenarios, especially when working with the Chro 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 +# 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? @@ -32,23 +32,23 @@ Additionally, we merged the schema reload button with the schema fetch status to Furthermore, we improved the schema fetch status, including its tooltip in the status bar, which makes things more explicit. -## Close Multiple Document Tabs +# 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 +# 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 +# Schema Reference Default Values Default values for fields in the schema reference column and type view are now available. -## Become An Insider +# 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 +# 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. diff --git a/website-next/content/blog/2025-02-01-hot-chocolate-15.md b/website-next/content/blog/2025-02-01-hot-chocolate-15.md index 028f9eda322..c97ba835678 100644 --- a/website-next/content/blog/2025-02-01-hot-chocolate-15.md +++ b/website-next/content/blog/2025-02-01-hot-chocolate-15.md @@ -11,7 +11,7 @@ authorImageUrl: https://avatars1.githubusercontent.com/u/9714350?s=100&v=4 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 +# 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. @@ -28,15 +28,15 @@ However, this change had broader implications, so we also introduced another ste 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 +# 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 +# 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 +# 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: @@ -64,7 +64,7 @@ public static IQueryable GetProducts(CatalogContext dbContext) 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 +# 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. @@ -153,7 +153,7 @@ public static async Task> GetBrandByIdAsync( So, the example above will lead to the DataLoader interface `IBrandByIdDataLoader`. -## Adding Projections, Filtering, and Sorting +# Adding Projections, Filtering, and Sorting How do we now introduce _client-driven_ projections, filtering, and sorting without exposing `IQueryable` from our business layer? @@ -244,7 +244,7 @@ public static async Task> GetBrandByIdAsync( 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 +# 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. @@ -405,7 +405,7 @@ The beauty of this approach is that the complexity in my business layer remains ![GreenDonut Packages By Layer](../../public/images/blog/2025-02-01-hot-chocolate-15/greendonut-2.png) -## DataLoader Branching +# 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. @@ -438,7 +438,7 @@ public async Task> Handle( 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 +# 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. diff --git a/website-next/content/blog/2026-05-11-hot-chocolate-16.md b/website-next/content/blog/2026-05-11-hot-chocolate-16.md index aac7caf7492..0e6cc2c2d0f 100644 --- a/website-next/content/blog/2026-05-11-hot-chocolate-16.md +++ b/website-next/content/blog/2026-05-11-hot-chocolate-16.md @@ -14,7 +14,7 @@ Hot Chocolate 16 is our first new major release of the platform in more than a y 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 +# 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. @@ -22,7 +22,7 @@ The idea behind Skimmed was to provide a modern API for Fusion composition and S 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 +# Scalars As part of this rewrite, we also tackled the scalar API. @@ -58,7 +58,7 @@ public sealed class PolicyType : ScalarType That is it. It is a breaking change, but the gain in clarity is well worth it. -## scalars.graphql.org +# 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. @@ -68,7 +68,7 @@ The result in v16 is fewer built-in scalars, but much better ones. The scalars w The full list, with diffs, lives in the [migration guide](../docs/hotchocolate/migrating/migrate-from-15-to-16.md). -## Date and time scalars +# 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. @@ -86,7 +86,7 @@ If you need to bind `System.TimeSpan` to `Duration`, or if you want custom preci 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 +# 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. @@ -137,7 +137,7 @@ So when we run a query like this: 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 +# 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. @@ -170,7 +170,7 @@ And here is a JSON Lines response stream: 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 +# 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. @@ -188,7 +188,7 @@ builder 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](./2026-04-22-semantic-introspection.md) 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 +# 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. @@ -219,7 +219,7 @@ 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 +# 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`. @@ -263,7 +263,7 @@ type Product { 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 +# 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. @@ -276,13 +276,13 @@ Most clients will not need to do anything. v0.2 is where the GraphQL ecosystem i 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 +# 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 +# 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: @@ -291,7 +291,7 @@ These deserve their own posts, so I will keep this short. v16 ships two new adap 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](../docs/hotchocolate/build/adapters/mcp.md) and the [OpenAPI adapter](../docs/hotchocolate/build/adapters/openapi.md). -## Wrapping up +# Wrapping up If you are upgrading, start with our [migration guide](../docs/hotchocolate/migrating/migrate-from-15-to-16.md). It has the full list of breaking changes, defaults, and migration notes. diff --git a/website-next/content/blog/2026-05-15-fusion-16.md b/website-next/content/blog/2026-05-15-fusion-16.md index f2d400f9553..3c921c87527 100644 --- a/website-next/content/blog/2026-05-15-fusion-16.md +++ b/website-next/content/blog/2026-05-15-fusion-16.md @@ -18,7 +18,7 @@ Around this time, we noticed other gateway vendors rewriting their solutions in > Upgrading from Fusion 15? Head straight to the [migration guide](../docs/fusion/migration/migrate-from-15-to-16.md) for the full list of breaking changes and upgrade notes. -## ASP.NET Core +# 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. @@ -102,7 +102,7 @@ builder.Services For a full walkthrough, see the [Getting Started](../docs/fusion/getting-started.md) guide. -## Performance +# 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. @@ -154,11 +154,11 @@ Fusion delivers an exceptionally fast gateway that ranks near the top in benchma For tuning knobs like HTTP/2 multiplexing, connection pooling, and concurrency limits, see the [Performance Tuning](../docs/fusion/performance-tuning.md) guide. -## AOT +# 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 +# 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. @@ -176,7 +176,7 @@ In the details you find a button to test the operations which will create a new ![Query plan testing view](../../public/images/blog/2026-05-15-fusion-16/query-plan-testing.png) -## Aspire +# 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. @@ -195,7 +195,7 @@ Aspire is excellent for the inner development loop. Being able to compose on the The [Aspire Integration](../docs/fusion/aspire-integration.md) guide covers schema endpoint annotations, composition settings, and Nitro-backed remote subgraph composition in detail. -## CI/CD +# 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. @@ -205,7 +205,7 @@ We also now provide native integration with both GitHub Actions and Azure DevOps See the [Deployment and CI/CD](../docs/fusion/deployment-and-ci-cd.md) guide and the [Nitro CLI reference](../docs/fusion/cli.md) for the full command set. -## Incremental Delivery +# 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. @@ -217,7 +217,7 @@ builder .ModifyOptions(o => o.EnableDefer = true); ``` -## Semantic Introspection +# 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. @@ -235,7 +235,7 @@ builder 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](./2026-04-22-semantic-introspection.md) 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 Adapters and Connectors are major additions in Fusion 16, each deserving a deep dive of their own. Here’s a quick overview: @@ -276,7 +276,7 @@ flowchart LR end ``` -### Adapters +## 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. @@ -306,13 +306,13 @@ mutation CreateUser($user: UserInput! @body) @http(method: POST, route: "/users" We currently ship Adapters for OpenAPI and MCP, with a gRPC Adapter landing in one of the next dot releases. The [MCP Adapter](../docs/fusion/adapters/mcp.md) docs are up now; the OpenAPI Adapter docs follow with its dedicated post. -### Connectors +## 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 +# 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. @@ -320,7 +320,7 @@ Along the way, we've gotten a shared vocabulary, a reference test suite, and imp 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 +# 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. diff --git a/website-next/content/blog/2026-05-28-mcp-hotchocolate-fusion.md b/website-next/content/blog/2026-05-28-mcp-hotchocolate-fusion.md index 7061ceb668e..4cfa48838ad 100644 --- a/website-next/content/blog/2026-05-28-mcp-hotchocolate-fusion.md +++ b/website-next/content/blog/2026-05-28-mcp-hotchocolate-fusion.md @@ -14,7 +14,7 @@ Agents are becoming first-class consumers of our APIs. Alongside the web and mob 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 +# 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. @@ -25,7 +25,7 @@ An MCP server exposes two main things: 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 +# The pieces There are three moving parts: @@ -35,7 +35,7 @@ There are three moving parts: 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 +# 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: @@ -77,7 +77,7 @@ app.Run(); 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 +# Enable MCP on a Fusion gateway The Fusion story is the same shape. Different packages, same two calls: @@ -113,7 +113,7 @@ 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 +# 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: @@ -131,7 +131,7 @@ mcp/ 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 +## 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. @@ -152,7 +152,7 @@ query SearchProducts($text: String!, $first: Int! = 10) { With just that file, `SearchProducts` is already a working MCP tool. -### Optional settings +## Optional settings Add a sibling `.json` file for a custom title, icons, or behavior hints: @@ -170,7 +170,7 @@ Add a sibling `.json` file for a custom title, icons, or behavior hints: 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 +## 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. @@ -280,7 +280,7 @@ In hosts that support MCP Apps, the chat will render this list inline. In plain- -### A prompt is templated JSON +## A prompt is templated JSON `mcp/prompts/SearchProducts/SearchProducts.json`: @@ -309,11 +309,11 @@ In hosts that support MCP Apps, the chat will render this list inline. In plain- `{searchQuery}` is interpolated from the matching argument. Whatever you declare in `arguments` is available as `{name}` inside `messages`. -## Publish with the Nitro CLI +# 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 +## 1. Create the feature collection A collection is a named container for a set of tools and prompts. Create one for your API: @@ -325,7 +325,7 @@ nitro mcp create \ `` comes from `nitro api list` or the Nitro UI. The command prints the new collection ID. Save it. -### 2. Upload a tagged version +## 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): @@ -339,7 +339,7 @@ nitro mcp upload \ 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 +## 3. Publish to a stage Uploading does not expose anything to clients. Publishing makes a tagged version live on a stage: @@ -352,7 +352,7 @@ nitro mcp publish \ 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 +## 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: @@ -362,7 +362,7 @@ 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 +# 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: @@ -395,7 +395,7 @@ When the model decides to call `SearchProducts`, the host invokes the tool, your ![SearchProducts result with Apps view](../../public/images/blog/2026-05-28-mcp-hotchocolate-fusion/chatgpt-tool-result.webp) -## What you get from Nitro +# What you get from Nitro Once tools are flowing, the management surface is where Nitro pays off: @@ -406,7 +406,7 @@ Once tools are flowing, the management surface is where Nitro pays off: 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 +# 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. diff --git a/website-next/content/blog/2026-06-05-introducing-skillz.md b/website-next/content/blog/2026-06-05-introducing-skillz.md index 4860e05df67..ec22c562129 100644 --- a/website-next/content/blog/2026-06-05-introducing-skillz.md +++ b/website-next/content/blog/2026-06-05-introducing-skillz.md @@ -66,7 +66,7 @@ or add the .NET-specific ones: dnx skillz add dotnet/skills ``` -## Installing +# Installing There are two ways to run `skillz`: @@ -81,7 +81,7 @@ 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 +# 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. @@ -112,7 +112,7 @@ dnx skillz add --copy # copy files instead of symlinkin dnx skillz list --json # machine-readable output ``` -## Authoring your own skills +# Authoring your own skills To package your team's conventions as a skill, start with: @@ -122,7 +122,7 @@ 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 +# graphql-schema-design The first ChilliCream skill we're shipping is `graphql-schema-design`. diff --git a/website-next/content/blog/2026-06-06-newsletter-may-2026.md b/website-next/content/blog/2026-06-06-newsletter-may-2026.md index a243fadea3b..ed8f3104395 100644 --- a/website-next/content/blog/2026-06-06-newsletter-may-2026.md +++ b/website-next/content/blog/2026-06-06-newsletter-may-2026.md @@ -14,7 +14,7 @@ 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 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 +# Fusion 16 Fusion 16 is a major step for our distributed GraphQL work. It is no longer built as an extension on top of Hot Chocolate; instead, it now has its own architecture while staying fully aligned with ASP.NET Core and the .NET ecosystem. @@ -22,7 +22,7 @@ That gives Fusion a cleaner foundation for gateway execution, planning, and comp Read the full post: [What's new in Fusion 16](./2026-05-15-fusion-16.md). -## Hot Chocolate 16 +# Hot Chocolate 16 Hot Chocolate 16 is our first major 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 proposals, and made the defaults safer. @@ -30,7 +30,7 @@ The new type system is the foundation for a lot of what comes next. It gives Hot Read the full post: [What's new for Hot Chocolate 16](./2026-05-11-hot-chocolate-16.md). -## skillz +# 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. @@ -42,7 +42,7 @@ dnx skillz add ChilliCream/agent-skills --skill graphql-schema-design Read the announcement: [Introducing skillz](./2026-06-05-introducing-skillz.md). -## From the community +# 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. @@ -50,7 +50,7 @@ It was also great to see so many of you in person at GraphQLConf 2026. Putting f 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 +# 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. diff --git a/website-next/content/blog/2026-06-11-open-your-graphql-api-for-the-rest.md b/website-next/content/blog/2026-06-11-open-your-graphql-api-for-the-rest.md index dd16dcf567c..5615ee22b24 100644 --- a/website-next/content/blog/2026-06-11-open-your-graphql-api-for-the-rest.md +++ b/website-next/content/blog/2026-06-11-open-your-graphql-api-for-the-rest.md @@ -22,7 +22,7 @@ Sooner or later, that endpoint comes back to haunt you. Its traffic never shows 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 +# 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. @@ -82,7 +82,7 @@ fragment Product on Product { } ``` -## Wiring it up +# 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: @@ -133,7 +133,7 @@ app.UseSwaggerUI(options => This renders the Swagger UI explorer at `/swagger/index.html`, where consumers can discover and try out your REST endpoints. -## Publishing your first endpoint +# 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. @@ -159,7 +159,7 @@ There is also a dedicated dashboard that shows all of your adapter endpoints and ![Nitro dashboard listing the REST endpoints generated by the OpenAPI adapter together with their telemetry](../../public/images/blog/2026-06-11-open-your-graphql-api-for-the-rest/nitro-endpoints-dashboard.png) -## Wrap up +# 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. From 5c0feedd2b59a4e2d918b0adf5766d77eadc997f Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:53:54 +0200 Subject: [PATCH 2/5] Fix more headings and image sizes --- website-next/app/blog/[...slug]/page.tsx | 5 +- .../content/docs/fusion/adapters/mcp.md | 32 ++--- .../content/docs/fusion/adding-a-subgraph.md | 38 +++--- .../content/docs/fusion/aspire-integration.md | 14 +-- .../authentication-and-authorization.md | 22 ++-- .../content/docs/fusion/composition.md | 38 +++--- .../fusion/data-requirements-and-mapping.md | 38 +++--- .../docs/fusion/entities-and-lookups.md | 20 ++-- .../fusion/field-ownership-and-sharing.md | 18 +-- .../content/docs/fusion/getting-started.md | 112 +++++++++--------- .../coming-from-apollo-federation.md | 80 ++++++------- .../content/docs/fusion/performance-tuning.md | 28 ++--- .../content/docs/fusion/request-limits.md | 30 ++--- .../fusion/schema-exposure-and-evolution.md | 30 ++--- .../docs/fusion/source-schema-extensions.md | 24 ++-- .../docs/hotchocolate/build/adapters/mcp.md | 32 ++--- .../content/docs/nitro/apis/deployments.md | 2 +- .../docs/nitro/explore-the-ui/explorer.md | 6 +- .../content/docs/nitro/nitro-services.md | 2 +- .../docs/nitro/open-telemetry/logging.md | 12 +- .../open-telemetry/service-monitoring.md | 12 +- .../strawberryshake/get-started/console.md | 10 +- website-next/scripts/optimize-images.mjs | 5 +- .../src/image-optimization/generate.mjs | 12 +- .../src/image-optimization/remote.mjs | 29 ++++- 25 files changed, 342 insertions(+), 309 deletions(-) diff --git a/website-next/app/blog/[...slug]/page.tsx b/website-next/app/blog/[...slug]/page.tsx index edbe3f0af14..7bc91197ac2 100644 --- a/website-next/app/blog/[...slug]/page.tsx +++ b/website-next/app/blog/[...slug]/page.tsx @@ -199,7 +199,10 @@ export default async function BlogSlugPage({ params }: PageProps) { src={featuredImage} alt="" priority - sizes="(max-width: 1024px) 100vw, 1024px" + // Mirrors the layout: a max-w-5xl (1024px) column inside px-5 + // (sm:px-12) page padding, so the browser picks the smallest + // sufficient variant instead of rounding the slot up to 100vw. + sizes="(max-width: 639px) calc(100vw - 2.5rem), (max-width: 1119px) calc(100vw - 6rem), 1024px" className="mb-6 aspect-video w-full rounded-lg object-cover" /> ) : null} diff --git a/website-next/content/docs/fusion/adapters/mcp.md b/website-next/content/docs/fusion/adapters/mcp.md index 50e155e35c6..61d0947e987 100644 --- a/website-next/content/docs/fusion/adapters/mcp.md +++ b/website-next/content/docs/fusion/adapters/mcp.md @@ -9,7 +9,7 @@ You wire MCP onto an existing Fusion gateway with two calls: `AddMcp()` during s This page covers wiring and configuration. For authoring tools and prompts, see the [Nitro MCP](../../nitro/adapters/mcp.md) section. -## Prerequisites +# Prerequisites You need an existing Fusion gateway. If you do not have one yet, follow the [Getting Started](../getting-started.md) tutorial first. @@ -19,7 +19,7 @@ Add the adapter package to the gateway project: dotnet add package HotChocolate.Fusion.Adapters.Mcp ``` -## Enabling MCP on the Gateway +# Enabling MCP on the Gateway Two adapter calls turn a Fusion gateway 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. @@ -36,7 +36,7 @@ app.MapGraphQLMcp(); The gateway 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 +# 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: @@ -62,7 +62,7 @@ Three overloads cover the common registration patterns: `IMcpStorage` returns `OperationToolDefinition` and `PromptDefinition` collections, and exposes `IObservable` streams so the gateway 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 +# 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): @@ -82,7 +82,7 @@ builder Tools registered through `configureServer` appear alongside the GraphQL-derived tools, so you can mix native MCP tools with operation tools in the same gateway. -## Mapping the MCP Endpoint +# Mapping the MCP Endpoint `MapGraphQLMcp()` accepts two optional arguments: @@ -100,11 +100,11 @@ 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 +# 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 gateway 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 +## Install the Nitro packages ```bash dotnet add package ChilliCream.Nitro @@ -113,7 +113,7 @@ dotnet add package ChilliCream.Nitro.Fusion `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.Fusion` referenced, `AddDefaults()` calls `AddFusion()` for you. -### Wire Nitro into the gateway +## Wire Nitro into the gateway Call `AddNitro().AddDefaults()` (or the explicit `AddNitro().AddFusion()`) before configuring the gateway. `AddNitro()` configures the shared connection options (`ApiId`, `ApiKey`, `Stage`) on `NitroServiceOptions`. `ModifyNitroOptions()` on the gateway builder configures gateway-specific options (MCP, OpenAPI, persisted operations, metrics, and so on): @@ -149,7 +149,7 @@ If you prefer environment variables over inline configuration, set `NITRO_API_ID > Order matters: `AddNitro().AddDefaults()` (or `AddFusion()`) must run before the gateway builder calls so that the Fusion gateway pipeline picks up the Nitro contributions during registration. -### What you get +## What you get With Nitro and MCP both enabled: @@ -161,32 +161,32 @@ With Nitro and MCP both enabled: For authoring tools and prompts, publishing feature collection versions, and managing stages, see the [Nitro MCP](../../nitro/adapters/mcp.md) section. -## Troubleshooting +# Troubleshooting -### `InvalidOperationException: Call AddMcp() when configuring the GraphQL server.` +## `InvalidOperationException: Call AddMcp() when configuring the GraphQL server.` `MapGraphQLMcp()` was called but `AddMcp()` was not registered on the gateway builder. Add `.AddMcp()` to the chain that starts with `AddGraphQLGateway()`. -### MCP endpoint returns 404 Not Found +## 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 ''.` +## `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.Fusion` and call `AddNitro().AddDefaults()` (or `AddNitro().AddFusion()`), 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 gateway registered through `AddGraphQLGateway(name)` plus `AddMcp()`. With multiple named gateways, pass the matching name. With a single unnamed gateway, omit `schemaName` and the adapter resolves it automatically. -### Tools and prompts list is empty +## 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.` +## 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 +# Next Steps - Author tools and prompts and publish them to a stage in the [Nitro MCP](../../nitro/adapters/mcp.md) section. - Deploy your gateway across stages with [Deployment and CI/CD](../deployment-and-ci-cd.md). diff --git a/website-next/content/docs/fusion/adding-a-subgraph.md b/website-next/content/docs/fusion/adding-a-subgraph.md index b97885835aa..5b13a832de4 100644 --- a/website-next/content/docs/fusion/adding-a-subgraph.md +++ b/website-next/content/docs/fusion/adding-a-subgraph.md @@ -7,7 +7,7 @@ You have an existing Fusion project with a gateway, one or more subgraphs, and a This page walks you through adding a Shipping subgraph to an existing project that already has Products and Reviews subgraphs. If you have not set up a Fusion project yet, start with the [Getting Started](./getting-started.md) tutorial first. -## Prerequisites +# Prerequisites Before you begin, you need: @@ -17,7 +17,7 @@ Before you begin, you need: You should be able to compose and run your existing project successfully. If composition is currently broken, fix that first. -## Subgraph Project Structure +# Subgraph Project Structure Every Fusion subgraph follows the same layout: @@ -39,7 +39,7 @@ src/ Your folder structure may differ, but the Fusion components are always the same: a GraphQL server, your type definitions, an exported schema, and a `schema-settings.json`. -## Create the Subgraph Project +# Create the Subgraph Project Create a new GraphQL server project from the template: @@ -76,7 +76,7 @@ Your `Shipping/Shipping.csproj` should look like this: ``` -### Configure the Port +## Configure the Port The `dotnet new graphql` template already defines a default port in `launchSettings.json`. Change it so the Shipping subgraph runs on port 5003. Edit `Shipping/Properties/launchSettings.json` and set `launchUrl` and `applicationUrl` under the `http` profile to: @@ -87,11 +87,11 @@ The `dotnet new graphql` template already defines a default port in `launchSetti This ensures the subgraph runs on port 5003, which matches what you will configure in `schema-settings.json` later. -## Define Your Types +# Define Your Types The Shipping subgraph owns shipment data and contributes a `shipments` field to the existing `Product` type. It does not own Product. The Products subgraph does. The Shipping subgraph extends it with shipping information. -### Define the Shipment Type +## Define the Shipment Type Create `Shipment.cs` in the Shipping project: @@ -114,7 +114,7 @@ public class Shipment Each shipment has a `ProductId` that references a product from the Products subgraph. -### Add In-Memory Data +## Add In-Memory Data Create `ShipmentRepository.cs`: @@ -140,7 +140,7 @@ public static class ShipmentRepository } ``` -### Create the Entity Stub +## Create the Entity Stub An entity stub is a lightweight declaration that says "I know this type exists in the graph and I want to add fields to it." Create `Types/Product.cs`: @@ -158,7 +158,7 @@ public sealed record Product(int Id) This is not a duplicate of the Product type from the Products subgraph. It is an entity stub. The Shipping subgraph does not define `name`, `price`, or any other Product field. It only contributes the `shipments` field. When the gateway composes the schema, it merges this stub with the full `Product` type from the Products subgraph. Clients see one `Product` type with all fields from all subgraphs. -### Add Query Resolvers +## Add Query Resolvers Create `Types/ShippingQueries.cs` with a public lookup for `Shipment` and an internal lookup for `Product`: @@ -185,7 +185,7 @@ public static partial class ShippingQueries For more on public vs. internal lookups and when to use each, see [Entities and Lookups](./entities-and-lookups.md). -### Replace Foreign Keys with Entity References +## Replace Foreign Keys with Entity References The `Shipment` type currently exposes a raw `ProductId`. To expose `shipment.product` instead, add a type extension. Create `Types/ShipmentNode.cs`: @@ -207,7 +207,7 @@ public static partial class ShipmentNode `[BindMember(nameof(Shipment.ProductId))]` tells Hot Chocolate to replace the `productId` field on `Shipment` with the `product` field returned by this resolver. In the exported schema, clients see `shipment.product` (returning a full `Product`) instead of `shipment.productId` (a raw integer). The gateway resolves the full Product from whichever subgraph owns it. -## Configure the Server +# Configure the Server Set `Program.cs` to: @@ -226,7 +226,7 @@ app.RunWithGraphQLCommands(args); `AddGraphQL("Shipping")` sets the subgraph name used during schema export. `AddTypes()` is source-generated and registers all types discovered by the analyzer. `RunWithGraphQLCommands(args)` enables CLI commands like schema export. -## Export the Schema +# Export the Schema From the project root, export the schema: @@ -254,7 +254,7 @@ Because `Program.cs` uses `AddGraphQL("Shipping")`, the generated `schema-settin The `name` field identifies this subgraph within the composite schema and must be unique. The `url` is where the gateway sends requests to this subgraph at runtime. -## Compose +# Compose Run composition with all subgraph schemas, including your new one: @@ -280,7 +280,7 @@ nitro fusion compose \ -a gateway.far ``` -## Test Cross-Subgraph Queries +# Test Cross-Subgraph Queries Start all services and the gateway. With the Shipping subgraph added, you can now query shipment data that crosses subgraph boundaries: @@ -321,23 +321,23 @@ query { Here the gateway resolves the shipment from the Shipping subgraph, then uses the Products subgraph to fetch `name` and `price` for the referenced product. -## Troubleshooting Composition Errors +# Troubleshooting Composition Errors If composition fails after adding your new subgraph, the error messages point to specific issues. -### Duplicate field without sharing +## Duplicate field without sharing **"Field X is defined in multiple subgraphs"**. Your new subgraph defines a field that already exists in another subgraph. Key fields (like `id`) are automatically shareable, but all other duplicated fields need `@shareable` on every definition. See [Field Ownership](./field-ownership-and-sharing.md) for details. -### Missing lookup +## Missing lookup **"No lookup found for entity X"**. Your subgraph references an entity type but no subgraph provides a lookup for it. Add a lookup resolver (public or internal) for that entity. See [Entities and Lookups](./entities-and-lookups.md). -### Incompatible field types +## Incompatible field types **"Incompatible field types for X"**. Two subgraphs define the same field with different types. The types must be compatible according to the composition merging rules. -## Next Steps +# Next Steps - **Need cross-subgraph field dependencies?** See [Data Requirements](./data-requirements-and-mapping.md) for the full range of `@require` patterns and FieldSelectionMap syntax. - **Composition failed?** See [Composition](./composition.md) for the full merging rules, common errors, and fixes. diff --git a/website-next/content/docs/fusion/aspire-integration.md b/website-next/content/docs/fusion/aspire-integration.md index 613293d029d..957f0992cf0 100644 --- a/website-next/content/docs/fusion/aspire-integration.md +++ b/website-next/content/docs/fusion/aspire-integration.md @@ -7,7 +7,7 @@ Nitro gives you full control over composition, but during active development you The `HotChocolate.Fusion.Aspire` package integrates composition into the .NET Aspire AppHost. When you build the AppHost, the orchestrator starts your subgraphs, extracts their source schemas, composes them into a Fusion archive, and writes it to the gateway project directory. One build step replaces the manual export-compose-restart cycle. You can also mix live subgraphs with pre-exported schema files, letting you develop against a full composite schema even when you only run a subset of services locally. -## Prerequisites +# Prerequisites You need a .NET Aspire AppHost project. If you do not have one yet, create it with: @@ -24,7 +24,7 @@ dotnet add package HotChocolate.Fusion.Aspire Your subgraph projects need the `HotChocolate.AspNetCore.CommandLine` package so the orchestrator can extract their schemas. If you followed the [Getting Started](./getting-started.md) tutorial, your subgraphs already have this. -## Setting Up the AppHost +# Setting Up the AppHost The AppHost wires together your subgraphs and gateway. Three extension methods configure the composition pipeline. @@ -63,7 +63,7 @@ Four things to notice: When you build and run the AppHost, the orchestrator handles the entire composition pipeline automatically. No manual `nitro fusion compose` step needed. -## Live Schema Extraction +# Live Schema Extraction By default, `WithGraphQLSchemaEndpoint()` fetches the source schema from `/graphql/schema.graphql` on each subgraph. Hot Chocolate subgraphs expose this endpoint automatically when they include the `HotChocolate.AspNetCore.CommandLine` package. @@ -82,7 +82,7 @@ var productsApi = builder All three parameters have sensible defaults. The `sourceSchemaName` defaults to the resource name (`"products-api"` in this example). Override it when you want the source schema name to differ from the Aspire resource name. -## Working with Partial Graphs +# Working with Partial Graphs You do not need to run every subgraph locally. When your system has many subgraphs but you only develop on a few, use `WithGraphQLSchemaFile()` to include pre-exported schema files for the subgraphs you are not running. @@ -136,7 +136,7 @@ var shippingApi = builder sourceSchemaName: "Shipping"); ``` -## Composition Settings +# Composition Settings `WithGraphQLSchemaComposition()` accepts a settings parameter that controls composition behavior. @@ -166,7 +166,7 @@ builder .WithReference(reviewsApi); ``` -## How Composition Fits the Dev Loop +# How Composition Fits the Dev Loop With Aspire, your inner dev loop looks like this: @@ -178,7 +178,7 @@ With Aspire, your inner dev loop looks like this: If composition fails (for example, a field conflict or a missing lookup), the orchestrator logs the error and stops the AppHost. Fix the issue and rebuild. You get the same composition validation as the Nitro CLI, integrated into your build step. -## Next Steps +# Next Steps - **Need to compose without Aspire?** See the Nitro CLI composition workflow in [Adding a Subgraph](./adding-a-subgraph.md). - **Need entity resolution patterns?** See [Entities and Lookups](./entities-and-lookups.md) for public vs. internal lookups, composite keys, and the node pattern. diff --git a/website-next/content/docs/fusion/authentication-and-authorization.md b/website-next/content/docs/fusion/authentication-and-authorization.md index 22d83b0e6b4..e356139022c 100644 --- a/website-next/content/docs/fusion/authentication-and-authorization.md +++ b/website-next/content/docs/fusion/authentication-and-authorization.md @@ -7,7 +7,7 @@ The Fusion gateway builds on ASP.NET Core authentication and authorization. You Fusion does not add its own authentication or authorization mechanisms. It relies on ASP.NET Core, so you keep the same configuration model, extension points, and security-hardened framework you already use. -## Authenticating clients at the gateway +# Authenticating clients at the gateway The gateway authenticates incoming requests with the same APIs as any ASP.NET Core service. There is no Fusion-specific configuration; use `AddAuthentication(...)` and `UseAuthentication()` as usual. @@ -44,7 +44,7 @@ Everything in the `AddJwtBearer` callback is standard ASP.NET Core. For details The example above configures your service to validate JWT access tokens at the gateway. It does not reject requests when the token is missing or invalid. To require authentication, add authorization to the GraphQL endpoint. -### Requiring authentication on the GraphQL endpoint +## Requiring authentication on the GraphQL endpoint To reject anonymous requests at the gateway before they reach the GraphQL pipeline, attach an authorization policy to the endpoint: @@ -67,17 +67,17 @@ app.MapGraphQL().RequireAuthorization("graphql:read"); Without `RequireAuthorization`, anonymous requests reach the GraphQL pipeline, and any field not protected by the subgraphs is accessible without credentials. Whether this is acceptable depends on your API. For example, a public catalog with an authenticated checkout flow may leave the endpoint open and let subgraphs protect sensitive fields. An internal back-office gateway typically requires authentication at the edge. -### Cookies Authentication & Browser Clients +## Cookies Authentication & Browser Clients You can also use cookie authentication at the gateway and serve UI assets directly from it. For guidance, see [Cookie Authentication in ASP.NET Core](https://learn.microsoft.com/aspnet/core/security/authentication/cookie). If you are building a browser-based client, review [UI and SPA considerations](https://learn.microsoft.com/en-us/aspnet/core/client-side/spa/intro?view=aspnetcore-10.0). In general, we **do NOT recommend** serving SPA applications directly from the gateway. Instead, put a Backend for Frontend (BFF) in front of the gateway. You can build your own BFF or [use Duende BFF](https://duendesoftware.com/products/bff). The BFF handles browser-specific authentication and session management, then calls the gateway with a token-based approach. This keeps the gateway focused on GraphQL traffic while the BFF manages browser authentication and session concerns. -## Forwarding identity to subgraphs +# Forwarding identity to subgraphs By default, the gateway does not forward headers to subgraphs. This means subgraphs do not receive the caller's access token or other identity information. Because authorization is usually handled by the subgraph, you typically need to pass the access token or relevant identity data so the subgraph can authorize requests correctly. -### Header propagation +## Header propagation Most often, you want to forward the `Authorization` header containing the access token to the subgraph. The subgraph can then validate the JWT and run authorization checks. @@ -123,7 +123,7 @@ app.UseHeaderPropagation(); app.MapGraphQL(); ``` -### Authentication termination +## Authentication termination In many enterprise scenarios, you may not want to forward the access token through the entire stack. With Fusion, you have full control over outgoing requests, so you can adapt the gateway to your authentication model and support custom authentication flows. @@ -168,21 +168,21 @@ builder.Services .AddHttpMessageHandler(); ``` -## Client-to-gateway trust (mTLS) +# Client-to-gateway trust (mTLS) In some scenarios, only trusted clients should be able to call your gateway. Mutual TLS (mTLS) supports this by requiring the client to present a certificate. The gateway validates that certificate against a trusted certificate authority (CA). Kestrel supports mTLS directly, and you can configure it with the `ConfigureKestrel` API. For details, see the [Microsoft Learn documentation](https://learn.microsoft.com/aspnet/core/security/authentication/certauth). -## Gateway-to-subgraph trust +# Gateway-to-subgraph trust -### Keep subgraphs private +## Keep subgraphs private Subgraphs should not be public endpoints. GraphQL federation features like `@internal` assume subgraphs are not publicly accessible. Place subgraphs on a private network, such as a VPC, Kubernetes cluster, or Azure VNet, that only the gateway can reach. This ensures all subgraph requests pass through the gateway and are subject to the gateway's composition, authentication, and authorization policies. -### mTLS to subgraphs +## mTLS to subgraphs To ensure that only the gateway can call the subgraphs, use mutual TLS (mTLS) for gateway-to-subgraph communication. @@ -191,7 +191,7 @@ Useful links: - [Certificate authentication](https://learn.microsoft.com/aspnet/core/security/authentication/certauth) - [Kestrel client certificates](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints#client-certificates) -## Authorization +# Authorization Authorization is a cross-cutting concern that involves both the gateway and the subgraphs. In internal or back-office scenarios, you usually want to reject unauthorized requests as early as possible at the gateway. In public scenarios, you may allow unauthorized requests to reach the subgraph and handle rejection there. diff --git a/website-next/content/docs/fusion/composition.md b/website-next/content/docs/fusion/composition.md index af9fe1562bd..e70e0667c2c 100644 --- a/website-next/content/docs/fusion/composition.md +++ b/website-next/content/docs/fusion/composition.md @@ -3,7 +3,7 @@ title: "Composition" description: "How Fusion composition validates and merges source schemas into an execution schema, with the full pipeline phases and diagnostic log codes explained." --- -## What Is Composition? +# What Is Composition? Composition is the build-time process that takes one or more **source schemas** (the schemas exported by your subgraphs) and produces a single, validated, distributable **execution schema** that the gateway uses to plan and execute distributed queries. @@ -17,7 +17,7 @@ Composition is the safety net of a Fusion deployment. It catches schema conflict If you are still onboarding, start with [Getting Started](./getting-started.md). The pages on [Entities and Lookups](./entities-and-lookups.md) and [Field Ownership](./field-ownership-and-sharing.md) cover the directives most often referenced by the log codes below. -## The Composition Pipeline +# The Composition Pipeline Composition runs as an ordered set of phases. Each phase reads the output of the previous one, runs its rules, and either passes the schema to the next phase or aborts with diagnostic errors. Composition halts at the first phase that produces errors. Within a single phase, several errors may be reported together before the pipeline stops at the phase boundary. @@ -42,39 +42,39 @@ flowchart TD I -.fail.-> X ``` -### 1. Parse Source Schemas +## 1. Parse Source Schemas Parses each source schema into an internal representation. This phase catches syntactically invalid SDL (`INVALID_GRAPHQL`). -### 2. Preprocess Source Schemas +## 2. Preprocess Source Schemas Normalizes each source schema for composition. This includes applying tag-based exclusions and any version-specific or interop transformations needed before the validation phases run. -### 3. Enrich Source Schemas +## 3. Enrich Source Schemas Decorates each schema with metadata extracted from directives, such as key fields, shareability, accessibility flags, and lookup information. The metadata is attached to types and fields so later phases can reason about them efficiently without re-parsing directive arguments. -### 4. Validate Source Schemas +## 4. Validate Source Schemas Runs per-schema validation rules against each source schema independently. Each source schema is checked on its own at this stage, before any cross-schema reasoning. Examples of issues caught here include `@external` placed on an interface field, malformed `@key` selection sets, root types with non-default names, and `@override` referencing the same source schema. -### 5. Pre-Merge Validation +## 5. Pre-Merge Validation Validates compatibility _across_ source schemas. This is where the pipeline checks that two source schemas which both define the same type agree on its shape: enum values match, output field types are mergeable, external argument types align, and input types declare the same required fields. Most "two source schemas disagree" diagnostics are surfaced here. -### 6. Merge Source Schemas +## 6. Merge Source Schemas Combines all validated source schemas into a single merged schema. Same-named types are unified, Fusion metadata is applied, and lookup information is recorded so the gateway knows which subgraph owns which field. -### 7. Post-Merge Validation +## 7. Post-Merge Validation Validates the merged schema as a whole. The rules here treat the composed schema as if it were a single GraphQL schema and check global invariants: no merged type is empty, all interfaces are implemented, no field references an inaccessible or internal type, the root query type exposes accessible fields, and default values do not point at inaccessible enum values. -### 8. Validate Satisfiability +## 8. Validate Satisfiability Performs reachability analysis. Starting from the root types, the pipeline walks every reachable field in the merged schema and confirms it can be resolved by at least one subgraph given the available `@lookup` and `@key` paths. If a field is reachable from a query but no subgraph can produce it, satisfiability fails with `UNSATISFIABLE_QUERY_PATH`. This is the last line of defense against shapes that look valid statically but cannot actually be served at runtime. -## Common Scenarios +# Common Scenarios A few rules account for most composition failures. Knowing the shape of the error helps you spot the cause quickly. @@ -108,13 +108,13 @@ If you do not want to mark shared fields manually, you can set the per-source-sc **`@key` references a field that does not exist on the type.** A `@key(fields: "sku")` on a type that has no `sku` field fails with `KEY_INVALID_FIELDS`. The fix is to either add the missing field to the type or correct the selection set in the `@key` directive. See [Entities and Lookups](./entities-and-lookups.md) for the rules around key selection sets. -## Log Codes Reference +# Log Codes Reference This is the complete list of diagnostic codes the composition pipeline emits, grouped by severity. Codes are stable identifiers and are safe to match on in CI scripts. The placeholders `{0}`, `{1}`, etc. in the message column are replaced with the relevant type, field, or schema name when the diagnostic is emitted. -### Errors +## Errors | Code | Message | How to resolve | | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -185,20 +185,20 @@ The placeholders `{0}`, `{1}`, etc. in the message column are replaced with the | [TYPE_KIND_MISMATCH](https://graphql.github.io/composite-schemas-spec/draft/#sec-Type-Kind-Mismatch) | The type '\{0\}' has a different kind in schema '\{1\}' (\{2\}) than it does in schema '\{3\}' (\{4\}). | The same type name was used for different kinds (for example, an object in one source schema and an interface in another). Decide which kind is correct and update the other source schema, or rename one of the types so they no longer collide. | | [UNSATISFIABLE_QUERY_PATH](https://graphql.github.io/composite-schemas-spec/draft/#sec-Unsatisfiable-Query-Path) | (Message varies. Includes the unreachable field, the path the validator tried, and the lookups it considered.) | Some reachable field cannot be resolved through the available `@lookup` and `@key` paths. See [Diagnosing UNSATISFIABLE_QUERY_PATH Errors](#diagnosing-unsatisfiable_query_path-errors) for how to read the message and fix the underlying gap. | -### Warnings +## Warnings | Code | Message | How to resolve | | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [LOOKUP_RETURNS_NON_NULLABLE_TYPE](https://graphql.github.io/composite-schemas-spec/draft/#sec-Lookup-Returns-Non-Nullable-Type) | The lookup field '\{0\}' in schema '\{1\}' should return a nullable type. | Lookups should return nullable entities so the gateway can represent missing keys without throwing. Change the return type of the lookup field to nullable. See [Entities and Lookups](./entities-and-lookups.md). | | [SPECIFIED_BY_URL_MISMATCH](https://graphql.github.io/composite-schemas-spec/draft/#sec-SpecifiedBy-URL-Mismatch) | The scalar type '\{0\}' has a different specified-by URL in schema '\{1\}' (\{2\}) than it does in schema '\{3\}' (\{4\}). | The same scalar declares conflicting `@specifiedBy(url: ...)` URLs in different source schemas. Align the URL across all source schemas that define the scalar so the composed schema points to a single specification. | -### Diagnosing UNSATISFIABLE_QUERY_PATH Errors +## Diagnosing UNSATISFIABLE_QUERY_PATH Errors Most composition errors point at a single source schema or definition. `UNSATISFIABLE_QUERY_PATH` is different. It is raised by the satisfiability validator (phase 8) and tells you that the merged schema as a whole has at least one reachable field that no source schema can serve through the available `@lookup` and `@key` paths. Because the validator weighs every option for reaching a field across every source schema, the diagnostic carries a tree of nested errors that explain why each candidate option failed. Reading that tree from the bottom up is the fastest way to find the gap. -#### Anatomy of the message +### Anatomy of the message The top-level message names the unreachable field: @@ -218,7 +218,7 @@ Underneath it, indented with two spaces per level, are the nested errors that th Indentation is meaningful: a nested message is the reason its parent option failed. Start at the leaves and work up. -#### Showing the path the validator took +### Showing the path the validator took By default, the top-level message names the failed field but not the path the validator walked to reach it. Enable `IncludeSatisfiabilityPaths` to extend the message with the path: @@ -236,7 +236,7 @@ nitro fusion compose --include-satisfiability-paths Paths add noise to short outputs and pay off on any non-trivial graph. Turn them on whenever an `UNSATISFIABLE_QUERY_PATH` error is hard to triage. -#### Common causes and fixes +### Common causes and fixes - **Missing `@lookup` for an entity in the target source schema.** A field on schema A returns an entity that is also defined in schema B, but B has no lookup that takes the entity's key. Add a lookup on B (typically `(id: ID!): `) annotated with `@lookup`. See [Entities and Lookups](./entities-and-lookups.md). - **Mismatched `@key` selections across source schemas.** The shared entity has different `@key` selection sets in different source schemas, so the validator cannot find a key both sides recognize. Align the key selection sets, or add additional `@key` directives so every key the producer uses is also a key the consumer recognizes. @@ -245,7 +245,7 @@ Paths add noise to short outputs and pay off on any non-trivial graph. Turn them When you fix the root cause, both the top-level `UNSATISFIABLE_QUERY_PATH` error and its nested children disappear together. -#### Temporarily ignoring a non-accessible field +### Temporarily ignoring a non-accessible field When a field is intentionally non-accessible at the moment, for example during an incremental rollout, you can ask the validator to skip it instead of failing composition. Add the qualified field name and the path to skip under the source schema's `satisfiability.ignoredNonAccessibleFields` in its `-settings.json` file: diff --git a/website-next/content/docs/fusion/data-requirements-and-mapping.md b/website-next/content/docs/fusion/data-requirements-and-mapping.md index be5dc5d1c15..6b075d570fa 100644 --- a/website-next/content/docs/fusion/data-requirements-and-mapping.md +++ b/website-next/content/docs/fusion/data-requirements-and-mapping.md @@ -11,7 +11,7 @@ This shifts cross-service data dependencies from hidden runtime failures to visi This chapter covers the directives and attributes that make this work: `@require` for declaring data requirements and `@provides` for declaring fields that a subgraph can resolve locally alongside an entity reference. You will learn the full range of patterns and the syntax behind each directive. -## Declaring Data Dependencies +# Declaring Data Dependencies Use `@require` on a resolver argument when that argument's value must come from fields owned by other subgraphs. Instead of calling another service yourself, you declare what data you need and the gateway fetches it for you. @@ -69,7 +69,7 @@ type Product { The `weight` argument is gone. Clients pass only `zip`. The gateway handles the resolution of `weight` transparently. -### How the Gateway Resolves Requirements +## How the Gateway Resolves Requirements When a resolver declares a data requirement with `@require`, three things happen during composition and execution: @@ -81,7 +81,7 @@ The resolver receives the data as if it were a normal argument. It does not know ![The gateway fetches required data from the Products subgraph first, then passes it to the Shipping subgraph as resolved @require arguments](../../../public/images/fusion-docs/data-requirements-require-flow.png) -### Complex Requirements with Input Objects +## Complex Requirements with Input Objects When a resolver needs multiple fields, you can gather them into a single input object using FieldSelectionMap syntax. This is a clean approach as it puts all the requirements for a resolver into a single argument. @@ -165,7 +165,7 @@ type Product { The `dimension` requirement argument is hidden from clients. Clients see only `zip`; the gateway resolves the nested fields (`weight`, `length`, `width`, `height`) automatically. -### Multiple Scalar Requirements +## Multiple Scalar Requirements You can annotate multiple arguments with `@require` on the same field. Each one declares an independent data dependency. So, while you can aggregate them into a single input object like explained above you also can spread them across arguments. @@ -200,7 +200,7 @@ public static partial class ProductNode } ``` -### Nested Field Paths +## Nested Field Paths `@require` can reach into nested objects using dot notation. @@ -218,7 +218,7 @@ type Product { The gateway traverses `seller.address.countryCode` on the entity and passes the resolved value as the `countryCode` argument. -### List Aggregation Paths +## List Aggregation Paths When an entity field is a list, you can use bracket notation to select a field from each element. The gateway collects the selected values into a flat list. @@ -236,11 +236,11 @@ type Product { The path `seller.addresses[countryCode]` means: navigate to `seller.addresses` (a list), then select `countryCode` from each element. If the seller has three addresses with country codes `"US"`, `"DE"`, and `"US"`, the resolver receives `["US", "DE", "US"]` as the `countryCodes` argument. -## Declaring Contextually Available Fields +# Declaring Contextually Available Fields Use `@provides` on a field that returns an entity to tell the gateway that certain subfields of that entity are available when resolved through this specific field. The subgraph does not own those fields globally, but it can provide them in this context. -### When Contextual Availability Helps +## When Contextual Availability Helps Consider a Reviews subgraph where the `author` field returns a `User` entity. The `User` type and its `username` field are owned by the Accounts subgraph. Normally the gateway would need to call the Accounts subgraph to fetch `username`. But the Reviews subgraph already has the author's username available when resolving `Review.author`. By annotating the `author` field with `@provides(fields: "username")`, the subgraph tells the gateway: "When you resolve `author` through the `Review` entity on my subgraph, I can also give you `username`." @@ -283,7 +283,7 @@ public static partial class ReviewNode } ``` -### Providing Multiple and Nested Fields +## Providing Multiple and Nested Fields `@provides` accepts a field selection set, so you can declare multiple fields or nested fields: @@ -299,7 +299,7 @@ type Review { } ``` -### When to Provide Fields Contextually +## When to Provide Fields Contextually Use `@provides` when: @@ -309,7 +309,7 @@ Use `@provides` when: Do not use `@provides` as a substitute for proper entity ownership. If your subgraph is the authoritative source for a field, that field should be defined as a regular field, not as `@external` with `@provides`. Similarly, if your subgraph can resolve a field on every path, use `@shareable` instead of marking it `@external` and adding `@provides` to each field that returns the entity. -## FieldSelectionMap Syntax Reference +# FieldSelectionMap Syntax Reference `@require` uses the FieldSelectionMap scalar for its `field` argument. This is a mini-language for describing how to map entity fields to argument shapes. @@ -323,7 +323,7 @@ Do not use `@provides` as a substitute for proper entity ownership. If your subg | List aggregation | `"addresses[countryCode]"` | Selects `countryCode` from each element in the `addresses` list | | List projection | `"dimensions[{ weight, height }]"` | Selects multiple fields from each list element into an object | -### When to Use Which Syntax +## When to Use Which Syntax **Simple path.** Use when `@require` maps one argument to one field. @@ -357,9 +357,9 @@ dims: [ProductDimensionInput!]! @require(field: "dimensions[{ weight, height }]" > For the full FieldSelectionMap grammar, see the [Composite Schemas specification](https://graphql.github.io/composite-schemas-spec/draft/#sec-Appendix-A-Specification-of-FieldSelectionMap-Scalar). -## Troubleshooting +# Troubleshooting -### `REQUIRE_INVALID_FIELDS`: Referenced field does not exist +## `REQUIRE_INVALID_FIELDS`: Referenced field does not exist ```text Error: The @require directive on argument "weight" references field "weight" @@ -368,26 +368,26 @@ which does not exist on type "Product". The field path in `@require(field: "...")` points to a field that does not exist on the entity type after composition. Check that the field name matches exactly (GraphQL field names, not C# property names) and that the owning subgraph is included in composition. -### `PROVIDES_INVALID_FIELDS`: Invalid field selection in `@provides` +## `PROVIDES_INVALID_FIELDS`: Invalid field selection in `@provides` The `@provides(fields: "...")` selection references one or more fields that do not exist on the returned entity type. Verify each field path (including nested fields) against the GraphQL schema. -### `PROVIDES_FIELDS_MISSING_EXTERNAL`: Provided field must be marked `@external` +## `PROVIDES_FIELDS_MISSING_EXTERNAL`: Provided field must be marked `@external` A field referenced by `@provides(fields: "...")` must be declared as `@external` in the same subgraph. Mark the provided field (and nested fields, when applicable) as `@external`, or remove it from `@provides` if this subgraph owns it globally. -### `EXTERNAL_UNUSED`: External field is not referenced +## `EXTERNAL_UNUSED`: External field is not referenced Every `@external` field must be referenced by a `@provides` directive. Remove unused `@external` declarations or add the corresponding `@provides` selection. -### Required argument still visible in composite schema +## Required argument still visible in composite schema If a `@require` argument appears in the composite schema when it should not, check that: - The `@require` directive is on the argument, not on the field - The `field` value is a valid FieldSelectionMap (invalid syntax triggers a `REQUIRE_INVALID_SYNTAX` composition error) -## Next Steps +# Next Steps - **Need entity identity and lookup patterns?** See [Entities and Lookups](./entities-and-lookups.md) for the full guide to keys, public vs. internal lookups, and composite keys. - **Need field ownership contracts?** See [Field Ownership](./field-ownership-and-sharing.md). diff --git a/website-next/content/docs/fusion/entities-and-lookups.md b/website-next/content/docs/fusion/entities-and-lookups.md index 6ec85403c00..ee17b77ac64 100644 --- a/website-next/content/docs/fusion/entities-and-lookups.md +++ b/website-next/content/docs/fusion/entities-and-lookups.md @@ -7,7 +7,7 @@ Entities are the mechanism that makes distributed GraphQL work. They are types w This page explains entity resolution in more detail: how entities are defined and how lookups resolve them across subgraphs. If you completed the [Getting Started](./getting-started.md) tutorial, you already used these concepts. Here, you will focus on the mechanics and patterns behind them. -## What Makes a Type an Entity +# What Makes a Type an Entity A type is not an entity because it appears in multiple subgraphs. It is an entity because it has stable key-based identity. An entity is a type with one or more key fields that uniquely identify an instance across multiple subgraphs. Those key fields form the contract between subgraphs: one subgraph can return an entity reference by key, and another subgraph can use that key to resolve additional fields for the same instance. @@ -44,11 +44,11 @@ type Query { In these examples, `id` is the key and `@lookup` defines how `Product` is resolved by that key. The Reviews lookup is internal, so clients cannot call it directly, but the gateway can use it to enter the Reviews subgraph and resolve `reviews`. -## Lookups +# Lookups A lookup is a query field that resolves an entity by its key. The gateway uses lookups to fetch additional fields for entities. Depending on the requested fields and available routes, it can use any subgraph that provides those fields and a compatible lookup path. Without a lookup, the gateway has no way to enter a subgraph and resolve an entity. -### Public Lookups +## Public Lookups A public lookup serves two purposes: clients can call it directly as a query field, and the gateway uses it for entity resolution behind the scenes. @@ -104,7 +104,7 @@ public static partial class ProductQueries } ``` -### Internal Lookups +## Internal Lookups An internal lookup is hidden from the composite schema. Clients cannot call it directly. It exists only for the gateway to use during entity resolution. @@ -165,7 +165,7 @@ public partial class InternalLookups In this pattern, clients cannot access `internalLookups` from the composite schema, but the gateway can still use nested `@lookup` fields for internal transitions. -### When to Use Internal vs. Public Lookups +## When to Use Internal vs. Public Lookups ![Public vs internal lookup visibility: clients can call public lookups, only the gateway can call internal lookups](../../../public/images/fusion-docs/entities-public-vs-internal-lookup.png) @@ -183,7 +183,7 @@ Use an **internal lookup** when: For cross-subgraph resolution to work, a subgraph that contributes fields to an entity must expose a compatible lookup path for that entity. This can be a direct lookup field or a nested lookup under an internal root object. If no lookup path exists, transitions into that subgraph are unsatisfiable. -### Multiple Lookups Per Entity +## Multiple Lookups Per Entity An entity can have multiple lookups, even in the same subgraph. This is useful when an entity can be identified by different keys. It is especially helpful when different subgraphs reference the same entity through different keys, for example `User.id` in one place and `User.username` in another. By providing both lookups, the gateway can transition into the target subgraph from either reference shape. @@ -235,7 +235,7 @@ input UserByInput @oneOf { In this case we use the `@is` directive with the choice operator `|` to signal to Fusion that it can use this lookup either with the `id` or the `username` as a key. -### Composite Keys +## Composite Keys Some entities are identified by a combination of fields instead of a single field. In that case, the lookup arguments together form the key. @@ -322,7 +322,7 @@ Both variants describe the same composite key. The first maps each argument expl > The FieldSelectionMap syntax from the Composite Schemas specification supports more advanced argument-to-field mappings for lookups. For the full grammar and examples, see the [Composite Schemas specification](https://graphql.github.io/composite-schemas-spec/draft/#sec-Appendix-A-Specification-of-FieldSelectionMap-Scalar). -## Explicit Key Declaration +# Explicit Key Declaration In most cases, you do not need to declare entity keys explicitly. The composition engine infers keys from your lookup fields. @@ -378,7 +378,7 @@ type Tenant { } ``` -## GraphQL Global Object Identification +# GraphQL Global Object Identification If your subgraphs implement GraphQL Global Object Identification, with a `node` field on `Query` and a `Node` interface, you already have a strong entity identity contract. You can build on this by using `node` as a lookup and treating types that implement `Node` as entities. @@ -406,7 +406,7 @@ builder > If GraphQL Global Object Identification is enabled at the gateway level, every entity resolvable through the `node` field becomes a public entry point. Use explicit internal lookups for entities you do not want exposed as public entry points. -## Next Steps +# Next Steps - **Need field ownership contracts?** See [Field Ownership](./field-ownership-and-sharing.md). - **Need argument mapping and cross-subgraph dependencies?** See [Data Requirements](./data-requirements-and-mapping.md) for `@is`, `@require`, and FieldSelectionMap patterns. diff --git a/website-next/content/docs/fusion/field-ownership-and-sharing.md b/website-next/content/docs/fusion/field-ownership-and-sharing.md index d87555b7970..910e54cb0cb 100644 --- a/website-next/content/docs/fusion/field-ownership-and-sharing.md +++ b/website-next/content/docs/fusion/field-ownership-and-sharing.md @@ -7,7 +7,7 @@ Field ownership defines which subgraph is responsible for each field in the comp This chapter explains Fusion's ownership model, when to use `@shareable`, and how `@external` and `@provides` fit into ownership contracts. -## Default Ownership Rules +# Default Ownership Rules For non-key fields, Fusion expects a single owner. @@ -16,7 +16,7 @@ For non-key fields, Fusion expects a single owner. Key fields are special. Fields used for entity identity and lookup mapping can appear in multiple subgraphs as part of entity resolution. -## Shared Fields +# Shared Fields Use `@shareable` when the same field is intentionally defined in multiple subgraphs and has the same meaning and value semantics in each definition. @@ -57,7 +57,7 @@ public static partial class UserNode } ``` -### When Not to Share +## When Not to Share Do not use `@shareable` when fields are only superficially similar. @@ -67,7 +67,7 @@ Do not use `@shareable` when fields are only superficially similar. If the meaning differs, use different field names and keep a single owner per field. -## Contextual Field Availability +# Contextual Field Availability `@provides` declares that a field returning an entity can also provide selected subfields of that entity in that specific path. This is a contextual optimization, not a transfer of global ownership. @@ -95,11 +95,11 @@ In this example: For detailed `@provides` patterns and FieldSelectionMap syntax, see [Data Requirements and Mapping](./data-requirements-and-mapping.md). -## Common Ownership Failures +# Common Ownership Failures These are the most frequent ownership mistakes that cause composition to fail or produce unexpected behavior. -### Duplicate Non-Key Field Without Sharing +## Duplicate Non-Key Field Without Sharing If two subgraphs define the same non-key field without `@shareable`, composition fails. @@ -108,11 +108,11 @@ Typical fix: 1. Remove the duplicate field from one subgraph, or 2. Mark all definitions with `@shareable` and align semantics. -### Misusing Provides as Ownership +## Misusing Provides as Ownership `@provides` is a load optimization for partial availability. Use it when only some paths in a subgraph can supply a field. If your subgraph can always provide a field, use `@shareable` instead. Marking it `@external` and adding `@provides` to every path that returns the entity adds complexity for no benefit. -## Ownership Checklist +# Ownership Checklist Before composition, verify: @@ -122,7 +122,7 @@ Before composition, verify: 4. `@external` is used only for fields owned elsewhere. 5. `@provides` is used for contextual availability, not to hide ownership ambiguity. -## Next Steps +# Next Steps - **Need identity and lookup routing?** See [Entities and Lookups](./entities-and-lookups.md). - **Need dependency and mapping syntax?** See [Data Requirements and Mapping](./data-requirements-and-mapping.md). diff --git a/website-next/content/docs/fusion/getting-started.md b/website-next/content/docs/fusion/getting-started.md index 23122d29100..9b81339e5a8 100644 --- a/website-next/content/docs/fusion/getting-started.md +++ b/website-next/content/docs/fusion/getting-started.md @@ -14,7 +14,7 @@ By the end, you will have: If you want the broader conceptual background first, read [Overview](./index.md). -## What Is Fusion and Why Use It? +# What Is Fusion and Why Use It? Hot Chocolate Fusion lets you split a GraphQL API across multiple independent services, called **subgraphs**, and present them to clients as a single, unified schema (called a **composite schema** in the spec). Each subgraph contributes types and fields to the overall API, runs in its own process, and can be developed, deployed, and scaled independently. @@ -24,17 +24,17 @@ Instead of building one monolithic GraphQL server that knows about every domain **When should you use Fusion?** Fusion becomes valuable when your API spans multiple domains (products, reviews, accounts, shipping), when different teams own different parts of the API, or when you need to deploy and scale parts of your API independently. If you find yourself wanting to break a growing monolith into smaller, team-owned services without forcing clients to stitch together calls to multiple endpoints, Fusion is the tool for the job. -## Thinking in Composite Schemas +# Thinking in Composite Schemas When you first encounter distributed GraphQL, there is a natural assumption about how subgraphs interact that turns out to be wrong. Getting the right mental model now will save you from the most common design mistakes. -### One GraphQL Schema, Many Contributors +## One GraphQL Schema, Many Contributors In Fusion, all subgraphs contribute to **one shared GraphQL schema**. There is no "Products Schema" and "Reviews Schema" that exist as separate GraphQL schemas. There is one graph that describes your entire domain, and different subgraphs are responsible for different parts of it. This means a single type can have fields contributed by multiple subgraphs. For example, the `Product` type might get its `name` and `price` fields from the Products subgraph, its `reviews` field from the Reviews subgraph, and its `deliveryEstimate` field from the Shipping subgraph. From the client's perspective, `Product` is one type with all those fields. The gateway figures out where each field lives and fetches it from the right place. -### Subgraphs Don't Call Each Other +## Subgraphs Don't Call Each Other When you split an API across multiple services, it is tempting to think each subgraph needs to call other subgraphs directly. Consider two subgraphs: one that manages Tenants, and one that manages Users. A `User` belongs to a `Tenant`. @@ -50,7 +50,7 @@ This seems reasonable, but it creates a problem: your subgraphs become coupled. In Fusion, your subgraph never calls another subgraph. Instead, it says: "this field returns a Tenant with this ID" and trusts the gateway to figure out the rest. The gateway knows which subgraph can resolve a Tenant by ID, using a **lookup**, which is a query field that resolves an entity by its key. The Tenants subgraph provides this lookup resolver, and the gateway calls it when it needs to turn a tenant ID into a full Tenant object. The Users subgraph never talks to the Tenants subgraph directly. The gateway handles all coordination. -### Why This Matters +## Why This Matters This design has practical consequences: @@ -58,7 +58,7 @@ This design has practical consequences: - **Adding new subgraphs is safe.** If a new Billing subgraph wants to contribute a `billingPlan` field to the `Tenant` type, it can do so without modifying the Tenants or Users subgraphs. - **The gateway is the coordinator.** Cross-subgraph data fetching is the gateway's job, not yours. Your subgraph only needs to know how to resolve the fields it owns and how to look up its own entities by key. When you build your subgraphs in the following sections, keep this model in mind: each subgraph contributes to one API, declares its entities and lookups, and trusts the gateway to wire everything together. -## Key Concepts +# Key Concepts Before diving into code, here are the core terms you will encounter throughout this guide. Each builds on the mental model from the previous section. @@ -98,11 +98,11 @@ type User { } ``` -## Prerequisites +# Prerequisites To follow this tutorial, you need the following installed on your machine. -### .NET SDK +## .NET SDK Install the [.NET 10 SDK](https://dotnet.microsoft.com/download) or later. Verify your installation: @@ -112,7 +112,7 @@ dotnet --version You should see `10.0.100` or higher. -### Nitro CLI +## Nitro CLI The Nitro CLI is a .NET tool that handles schema composition. Install it globally: @@ -126,7 +126,7 @@ Verify the installation: nitro version ``` -### Hot Chocolate Templates +## Hot Chocolate Templates Install the Hot Chocolate templates: @@ -134,11 +134,11 @@ Install the Hot Chocolate templates: dotnet new install HotChocolate.Templates ``` -## Create Your First Subgraph (Products) +# Create Your First Subgraph (Products) Time to write code. You will create a Products subgraph that exposes a few products through a GraphQL API. This subgraph will later become part of your composed API. -### Project Setup +## Project Setup Create a new GraphQL server project from the template: @@ -179,7 +179,7 @@ Your `Products/Products.csproj` should now look like this: ``` -### Configure the Port +## Configure the Port The `dotnet new graphql` template already defines a default port in `launchSettings.json`. For this tutorial, change it so the Products subgraph runs on port 5001. Edit `Products/Properties/launchSettings.json` and set `launchUrl` and `applicationUrl` under the `http` profile to: @@ -190,7 +190,7 @@ The `dotnet new graphql` template already defines a default port in `launchSetti This ensures the subgraph runs on port 5001, which matches what we will configure for composition later. -### Define the Product Type +## Define the Product Type Create a file called `Product.cs` in the Products project: @@ -209,7 +209,7 @@ public class Product This is a plain C# class with nothing GraphQL-specific yet. -### Add In-Memory Data +## Add In-Memory Data Create a file called `ProductRepository.cs`: @@ -235,7 +235,7 @@ public static class ProductRepository > In production, use **DataLoader** or **batch resolvers** in your subgraphs to batch backend lookups and reduce N+1 query patterns. This is especially important for entity lookups across subgraphs. See the [Fusion demo project](https://github.com/ChilliCream/fusion-demo) for a more complete example. -### Expose Products Through GraphQL +## Expose Products Through GraphQL Create a file called `ProductQueries.cs`. This class defines the Query root fields for the Products subgraph: @@ -259,7 +259,7 @@ Two attributes to notice: - **`[QueryType]`** tells Hot Chocolate that this class contributes fields to the `Query` root type. The source generator (from `HotChocolate.Types.Analyzers`) automatically registers these fields. - **`[Lookup]`** marks `GetProductById` as a lookup resolver. This is how the gateway resolves a `Product` when another subgraph references it. Without this attribute, the gateway would have no way to fetch a Product by its ID from this subgraph. Because this lookup is **public**, it also appears in the composite schema as a query field clients can call directly. Lookups must return nullable entity types (`Product?`) so unresolved keys can return `null` and avoid cascading failures when one or more subgraphs cannot provide requested fields for an entity. -### Configure the Server +## Configure the Server Set `Program.cs` to: @@ -278,7 +278,7 @@ app.RunWithGraphQLCommands(args); This is a minimal Hot Chocolate server. `AddGraphQL("Products")` sets the subgraph name used during schema export. `AddTypes()` registers all discovered types, including `ProductQueries` and `Product`. `RunWithGraphQLCommands(args)` enables CLI commands, including schema export. -### Test the Subgraph +## Test the Subgraph Run the Products subgraph: @@ -327,7 +327,7 @@ query { Stop the server with `Ctrl+C` before continuing. -### Export the Schema +## Export the Schema The gateway needs each subgraph's source schema for composition. Hot Chocolate can export source schema files automatically. From the `fusion-getting-started` directory, run: @@ -355,7 +355,7 @@ Because `Program.cs` uses `AddGraphQL("Products")`, `Products/schema-settings.js The `name` field identifies this subgraph within the composite schema and must be unique. The `url` is where the gateway sends requests to this subgraph at runtime. -### What You Built +## What You Built Your Products subgraph now: @@ -366,11 +366,11 @@ Your Products subgraph now: In the next section, you will create a second subgraph (Reviews) that references Products. This is where the power of Fusion starts to show. -## Create a Second Subgraph (Reviews) +# Create a Second Subgraph (Reviews) Now you will create a Reviews subgraph that adds review data to the API. The key part is that the Reviews subgraph will add a `reviews` field to the `Product` type that lives in the Products subgraph. This is Fusion's core capability: multiple subgraphs contributing fields to the same type. -### Project Setup +## Project Setup From the `fusion-getting-started` directory: @@ -380,7 +380,7 @@ dotnet new graphql -n Reviews The template also creates sample files in `Reviews/Types` (`Author.cs`, `Book.cs`, `Query.cs`). Delete those files before continuing. -### Configure the Port +## Configure the Port The `dotnet new graphql` template already defines a default port in `launchSettings.json`. For this tutorial, change it so the Reviews subgraph runs on port 5002. Edit `Reviews/Properties/launchSettings.json` and set `launchUrl` and `applicationUrl` under the `http` profile to: @@ -391,7 +391,7 @@ The `dotnet new graphql` template already defines a default port in `launchSetti The Reviews subgraph runs on port 5002, while the Products subgraph runs on 5001. -### Define the Review Type +## Define the Review Type Create `Review.cs` in the Reviews project: @@ -412,7 +412,7 @@ public class Review Each review has a `ProductId` that references a product from the Products subgraph. -### Add In-Memory Data +## Add In-Memory Data Create `ReviewRepository.cs`: @@ -442,7 +442,7 @@ public static class ReviewRepository Notice that reviews reference products by `ProductId`. Reviews 1 and 2 belong to Product 1 (Table), Review 3 belongs to Product 2 (Couch), and Review 4 belongs to Product 3 (Chair). -### Define the Product Stub +## Define the Product Stub This is the most important file in this section. Create `Product.cs`: @@ -465,7 +465,7 @@ The Reviews subgraph does not define `name`, `price`, or any other Product field All fields appear on a single unified `Product` type in the composite schema. -### Add Query Resolvers +## Add Query Resolvers Create `ReviewQueries.cs`: @@ -507,7 +507,7 @@ Why is the internal lookup needed? When a client queries `review.product.reviews Notice that this lookup returns `Product?` (nullable), even though it is internal. Lookups should be nullable so unresolved keys can return `null` and avoid cascading failures across subgraphs. In this sample, the resolver still constructs a stub with `new(id)` because the key is already known. -### Configure the Server +## Configure the Server Set `Program.cs` to: @@ -526,7 +526,7 @@ app.RunWithGraphQLCommands(args); This follows the same pattern as the Products subgraph, but sets the subgraph name to `Reviews`. -### Add the ObjectType Extension +## Add the ObjectType Extension There is one more piece needed. The `Review` type currently exposes a raw `ProductId`. To expose `review.product` instead, add a type extension for `Review`. Create `ReviewNode.cs`: @@ -550,7 +550,7 @@ public static partial class ReviewNode This transformation happens in the GraphQL schema, not your C# classes. Your `Review` class still has a `ProductId` field internally, but in the exported schema that field is replaced with `product: Product`. When a client queries `review.product`, Hot Chocolate calls `GetProduct()`, reads the `ProductId` from the parent Review, and returns a `Product` stub with just that ID. The gateway then uses the Product lookup to fetch the full product data from whichever subgraph owns it. -### Test the Subgraph +## Test the Subgraph Run the Reviews subgraph: @@ -607,7 +607,7 @@ You should see each review with its product reference, and each product with its Stop the server with `Ctrl+C`. -### Export the Schema +## Export the Schema Export the Reviews subgraph's source schema. From the `fusion-getting-started` directory: @@ -635,7 +635,7 @@ Because `Program.cs` uses `AddGraphQL("Reviews")`, `Reviews/schema-settings.json The `name` field identifies this subgraph within the composite schema and must be unique. The `url` is where the gateway sends requests to this subgraph at runtime. -### What You Built +## What You Built Your Reviews subgraph now: @@ -647,13 +647,13 @@ Your Reviews subgraph now: You now have two independent subgraphs that contribute to one shared API. In the next section, you will compose them into a single composite schema. -## Compose the Schemas with Nitro CLI +# Compose the Schemas with Nitro CLI You now have two subgraphs, each with an exported schema and a settings file. The next step is **composition**: merging these source schemas into a single composite schema that the gateway will serve to clients. Composition is handled by the Nitro CLI. It reads each subgraph's `.graphqls` schema file and its companion `-settings.json` file, validates that the schemas are compatible, and produces a **Fusion archive** (`.far` file) that contains everything the gateway needs: the composite schema, the execution schema with routing metadata, and the transport configuration for each subgraph. -### Verify Your File Structure +## Verify Your File Structure Before running composition, make sure your project looks like this: @@ -682,7 +682,7 @@ fusion-getting-started/ Each subgraph directory has a `schema.graphqls` and a `schema-settings.json`. The Nitro CLI matches these files by their shared prefix (`schema`). -### Run Composition +## Run Composition From the `fusion-getting-started` directory, run: @@ -701,7 +701,7 @@ Fusion archive created: gateway.far The exact output may vary by Nitro CLI version. Confirm that `Gateway/gateway.far` was created. This file contains the composed gateway configuration, and the gateway loads it directly. -### What Happens During Composition +## What Happens During Composition The Nitro CLI performs four steps: @@ -710,11 +710,11 @@ The Nitro CLI performs four steps: 3. **Check satisfiability**: verify that cross-subgraph selections can actually be fulfilled with the available keys and lookups. 4. **Produce** output: create the composite schema plus routing metadata for the gateway. -## Run the Fusion Gateway +# Run the Fusion Gateway The gateway is the service that clients connect to. It loads the composed configuration from the `.far` file, exposes the composite schema, and routes queries to the appropriate subgraphs at runtime. -### Create the Gateway Project +## Create the Gateway Project From the `fusion-getting-started` directory: @@ -744,7 +744,7 @@ Your `Gateway/Gateway.csproj` should look like this: ``` -### Configure the Port +## Configure the Port Edit `Gateway/Properties/launchSettings.json` and set `launchUrl` and `applicationUrl` under the `http` profile to: @@ -755,7 +755,7 @@ Edit `Gateway/Properties/launchSettings.json` and set `launchUrl` and `applicati The gateway runs on port 5000. The subgraphs run on ports 5001 (Products) and 5002 (Reviews). -### Verify the Fusion Archive +## Verify the Fusion Archive The composition step already wrote `gateway.far` into the `Gateway` directory (`-f Gateway/gateway.far`). Verify that the file exists before continuing: @@ -763,7 +763,7 @@ The composition step already wrote `gateway.far` into the `Gateway` directory (` ls Gateway/gateway.far ``` -### Configure the Gateway +## Configure the Gateway The template generates `Gateway/Program.cs`. For this tutorial, enable operation-plan telemetry so Nitro can show execution metrics in the Operation Plan view: @@ -791,7 +791,7 @@ Three things to notice: - **`AddFileSystemConfiguration("./gateway.far")`** tells the gateway to load its composed configuration from a local file. In production, you would typically use `.AddNitro()` to download the configuration from the Nitro cloud, but for local development the file system approach is simpler. - **`ModifyRequestOptions(o => o.CollectOperationPlanTelemetry = true)`** enables operation-plan telemetry. This is off by default. -### Start Everything +## Start Everything You need all three services running at the same time: both subgraphs and the gateway. Open three terminal windows and start the subgraphs first. @@ -830,7 +830,7 @@ info: Microsoft.Hosting.Lifetime[0] The port number will differ for each service (5001, 5002, and 5000). If you see errors like "Address already in use," another process is using that port. Either stop it or choose a different port in `launchSettings.json`. -### Verify the Gateway +## Verify the Gateway Open your browser to `http://localhost:5000/graphql/` to access the Nitro GraphQL IDE on the gateway. Try a simple query to verify the gateway is working: @@ -846,7 +846,7 @@ query { You should see the same product data as when you queried the Products subgraph directly. The difference is that this query went through the gateway, which routed it to the Products subgraph behind the scenes. If you look at the terminal running the Products subgraph (Terminal 1), you should see a log entry showing it received and processed the request. This confirms the gateway successfully routed the query. -### What You Built +## What You Built Your gateway now: @@ -857,11 +857,11 @@ Your gateway now: In the next section, you will run queries that demonstrate the gateway coordinating data across both subgraphs in a single request. -## Query Across Subgraphs +# Query Across Subgraphs This is the moment everything comes together. With all three services running (Products on 5001, Reviews on 5002, Gateway on 5000), you can now run a single query that fetches data from both subgraphs. -### The Cross-Subgraph Query +## The Cross-Subgraph Query Open Nitro at `http://localhost:5000/graphql/` and run this query: @@ -917,7 +917,7 @@ You should see: Look at what happened: `name` and `price` came from the Products subgraph, while `reviews` came from the Reviews subgraph. The client sent one query to one endpoint, and the gateway coordinated the rest. -### What the Gateway Did +## What the Gateway Did Behind the scenes, the gateway executed a query plan with multiple steps: @@ -935,7 +935,7 @@ With `CollectOperationPlanTelemetry` enabled on the gateway, the plan includes t The client never knew that two separate services were involved. This is the core promise of Fusion: multiple subgraphs, one unified API. -### Try Another Query +## Try Another Query You can also query in the other direction: start from reviews and reach into product data. @@ -985,13 +985,13 @@ You should see: This time, `body` and `stars` came from the Reviews subgraph, while `product.name` and `product.price` came from the Products subgraph. The gateway resolved the product references by calling the Products subgraph's `productById` lookup with each review's product ID. -### Compare: Before and After Composition +## Compare: Before and After Composition Remember in the previous section, when you queried `review.product` on the Reviews subgraph directly? You could see `product.id` and `product.reviews`, but `product.name` and `product.price` were missing because those fields did not exist in the Reviews subgraph. Now, through the gateway, `product.name` and `product.price` are available. Composition merged the Product type from both subgraphs, and the gateway resolves each field from the subgraph that owns it. This is what it means for subgraphs to contribute to one shared API. -### Lookup a Single Product +## Lookup a Single Product You can also look up a single product by ID: @@ -1010,7 +1010,7 @@ query { This uses the `productById` lookup from the Products subgraph, the public one that appears in the composite schema. The gateway then fetches reviews from the Reviews subgraph using its internal lookup. Public lookups serve as both client-facing query fields and gateway entity resolution entry points, while internal lookups are only used by the gateway behind the scenes. -### What You Accomplished +## What You Accomplished You proved that Fusion works: @@ -1022,7 +1022,7 @@ You proved that Fusion works: The Products and Reviews subgraphs are completely independent. They do not import each other's code, do not call each other directly, and can be deployed and scaled separately. Yet clients can query across both as if they were one unified schema. -## Sharing Fields Across Subgraphs +# Sharing Fields Across Subgraphs In the tutorial so far, the Products and Reviews subgraphs each contribute **different** fields to the `Product` type. Products owns `name` and `price`, while Reviews owns `reviews`. There is no overlap, so composition works without any special annotations. @@ -1030,7 +1030,7 @@ But what happens when two subgraphs need to define the **same** field on the sam The `[Shareable]` attribute solves this. When you mark a field as shareable in both subgraphs, you are telling Fusion: "these definitions are intentional and return the same data. Use whichever is most efficient." -### When You Need It +## When You Need It A common scenario: your Reviews subgraph needs to display the product name alongside each review. You could always fetch the name from the Products subgraph via the gateway, but if performance matters, you might want the Reviews subgraph to cache product names locally. In that case, both subgraphs would define `Product.name`: @@ -1063,17 +1063,17 @@ public sealed record Product(int Id) Both subgraphs define `Product.name` and mark it `[Shareable]`. Composition succeeds, and the gateway can resolve `name` from either subgraph depending on what else the query needs. If a query only asks for `product.name` and `product.reviews`, the gateway will fetch everything from the Reviews subgraph in a single call instead of making a separate trip to the Products subgraph. -### The Rule +## The Rule Without `[Shareable]`, a non-key field must exist in exactly one subgraph. Key fields (like `id`) are automatically shareable, so you do not need the attribute on them. For any other field that appears in multiple subgraphs, add `[Shareable]` to **all** definitions of that field. If even one subgraph forgets to mark it, composition fails. -### When Not to Use It +## When Not to Use It Do not mark a field as shareable unless the subgraphs genuinely return the same data. If two subgraphs define `Product.name` but one returns the display name and the other returns an internal code name, marking them shareable would give clients inconsistent results depending on which subgraph the gateway happens to call. If the fields return **different** data, they should have **different** names (e.g., `displayName` vs `codeName`) and each lives in its own subgraph without `[Shareable]`. -## What's Next +# What's Next You now have a working Fusion setup: two subgraphs contributing to one composed API, served through a single gateway. Here are some directions to explore next based on what you need: diff --git a/website-next/content/docs/fusion/migration/coming-from-apollo-federation.md b/website-next/content/docs/fusion/migration/coming-from-apollo-federation.md index f07ea29bc9d..c85d0db0e99 100644 --- a/website-next/content/docs/fusion/migration/coming-from-apollo-federation.md +++ b/website-next/content/docs/fusion/migration/coming-from-apollo-federation.md @@ -7,7 +7,7 @@ If you have experience with Apollo Federation, you already understand the core i This guide maps Apollo Federation concepts to their Fusion equivalents, explains behavioral differences, and walks you through migrating subgraphs, the gateway, and your CI/CD pipeline. It is self-contained: you can complete a migration by following this guide alone. Links to other Fusion docs pages are provided for deeper dives, not as prerequisites. -## Concept Mapping +# Concept Mapping The table below maps Apollo Federation concepts to their Fusion equivalents. Some are straightforward renames; others involve meaningful behavioral changes. The "Key Difference" column flags which is which. @@ -29,7 +29,7 @@ The table below maps Apollo Federation concepts to their Fusion equivalents. Som | Federation subgraph library (`@apollo/subgraph`) | No equivalent needed | Subgraphs are standard HotChocolate servers. No federation library. | | `_service { sdl }` introspection | `dotnet run -- schema export` | Schema export is a CLI command, not a runtime introspection field. | -## What Fusion Does Not Need +# What Fusion Does Not Need Several things from Apollo's model have no Fusion equivalent because the architecture handles them differently. @@ -43,11 +43,11 @@ Several things from Apollo's model have no Fusion equivalent because the archite **No `@key` directive.** In Apollo, `@key(fields: "id")` tells the gateway which fields identify an entity. In Fusion, the gateway infers entity keys from the arguments of your `[Lookup]` fields. If your lookup is `GetProductById(int id)`, the gateway knows that `id` is the key for `Product`. You can use `[EntityKey("id")]` for explicit key declaration when needed, but it is rarely necessary. -## Behavioral Differences in Depth +# Behavioral Differences in Depth Beyond naming, several concepts work fundamentally differently in Fusion. Understanding these differences is important for a successful migration. -### Entity Resolution: Lookups vs. the Entities Query +## Entity Resolution: Lookups vs. the Entities Query This is the most significant architectural difference between Apollo Federation and Fusion. @@ -134,7 +134,7 @@ The `[Internal]` attribute hides this field from the composite schema. Only the For more on lookups and entity resolution patterns, see [Entities and Lookups](../entities-and-lookups.md). -### Requirements Operate on Arguments, Not Fields +## Requirements Operate on Arguments, Not Fields In Apollo Federation, `@requires` is a field-level directive. It declares that a field depends on data from another subgraph: @@ -180,7 +180,7 @@ This changes how you design resolvers. In Apollo, the required data is available Cross-subgraph data dependencies with `[Require]` are also covered in the [Adding a Subgraph](../adding-a-subgraph.md) page. A dedicated deep-dive will be available in future documentation. -### Entity Stubs: How Subgraphs Reference Foreign Entities +## Entity Stubs: How Subgraphs Reference Foreign Entities In Apollo, when a subgraph extends an entity from another subgraph, it uses `extend type` with `@key`: @@ -212,7 +212,7 @@ public sealed record Product([property: ID] int Id) The stub is not a copy of the full Product type. It only declares the key (`Id`) and the fields this subgraph contributes (`reviews`). The gateway merges it with the full `Product` type from the Products subgraph during composition. -### Composition: Build Step, Not Cloud Operation +## Composition: Build Step, Not Cloud Operation In Apollo, composition typically happens in GraphOS cloud when you run `rover subgraph publish`. The router downloads the composed supergraph schema from GraphOS at startup. @@ -231,7 +231,7 @@ You can also use Nitro cloud for managed composition (similar to Apollo's GraphO For more on composition rules and error resolution, see [Composition](../composition.md). -### The Gateway Is Code, Not a Separate Binary +## The Gateway Is Code, Not a Separate Binary Apollo Router is a standalone binary (written in Rust) that you configure via YAML. Fusion's gateway is an ASP.NET Core application that you write and control: @@ -249,13 +249,13 @@ app.Run(); Because it is a standard ASP.NET Core app, you get full access to the middleware pipeline, dependency injection, authentication, header propagation, and everything else in the .NET ecosystem. There is no separate binary to deploy or configure. -## Step-by-Step Migration +# Step-by-Step Migration -### Phase 1: Migrate Subgraphs +## Phase 1: Migrate Subgraphs For each Apollo Federation subgraph, follow these steps. -#### Step 1: Replace Apollo Packages with HotChocolate +### Step 1: Replace Apollo Packages with HotChocolate Remove the Apollo subgraph library and add HotChocolate packages. @@ -303,7 +303,7 @@ app.RunWithGraphQLCommands(args); The call to `RunWithGraphQLCommands(args)` enables `dotnet run -- schema export`, which is how Fusion extracts the subgraph schema for composition. -#### Step 2: Convert Entity Resolution to Lookups +### Step 2: Convert Entity Resolution to Lookups This is the core conversion. For every entity type that has a `@key` directive and a `__resolveReference` resolver, create a `[Lookup]` query field. @@ -362,7 +362,7 @@ public static Product GetProductById(int id) => new(id); **Key point:** You do not need a `@key` directive. The gateway infers the entity key from the lookup's arguments. If your lookup takes `int id`, the gateway knows `id` is the key. -#### Step 3: Convert Field Requirements +### Step 3: Convert Field Requirements Replace field-level `@requires` with argument-level `[Require]`. @@ -422,7 +422,7 @@ public int GetDeliveryEstimate( } ``` -#### Step 4: Convert External Fields and Provides +### Step 4: Convert External Fields and Provides **`@external`** has a direct equivalent in `[External]`, but it is less frequently needed. In Apollo, you must mark any field referenced by `@requires` as `@external`. In Fusion, the `[Require]` selection syntax references fields from the composed graph directly -- no `@external` annotation is needed on the entity type. @@ -453,7 +453,7 @@ public static partial class ReviewNode } ``` -#### Step 5: Handle Shared Fields +### Step 5: Handle Shared Fields `[Shareable]` works the same in both systems. If multiple subgraphs define the same field on the same type, each definition must be marked as shareable. @@ -480,7 +480,7 @@ public static partial class UserNode One difference: in Fusion, key fields (like `id`) are automatically shareable. You do not need to annotate them. -#### Step 6: Create Schema Settings +### Step 6: Create Schema Settings Every Fusion subgraph needs a `schema-settings.json` file that tells composition where the subgraph lives and how to connect to it: @@ -506,7 +506,7 @@ Every Fusion subgraph needs a `schema-settings.json` file that tells composition Place this file next to your project. The `name` field must be unique across all subgraphs. The `clientName` field (`"fusion"`) must match the named HTTP client configured in the gateway. -#### Step 7: Export the Schema +### Step 7: Export the Schema Run the schema export command: @@ -516,11 +516,11 @@ dotnet run -- schema export This generates a `.graphqls` file containing your subgraph's schema with Fusion-specific directives. This file, together with `schema-settings.json`, is what composition reads. -### Phase 2: Migrate the Gateway +## Phase 2: Migrate the Gateway Replace Apollo Router with a Fusion gateway ASP.NET Core project. -#### Step 1: Create the Gateway Project +### Step 1: Create the Gateway Project ```bash dotnet new web -n Gateway @@ -528,7 +528,7 @@ cd Gateway dotnet add package HotChocolate.Fusion.AspNetCore ``` -#### Step 2: Configure the Gateway +### Step 2: Configure the Gateway **Minimal gateway (`Program.cs`):** @@ -553,7 +553,7 @@ builder .AddNitro(); ``` -#### Step 3: Set Up Header Propagation +### Step 3: Set Up Header Propagation If your Apollo Router forwards headers (like `Authorization`) to subgraphs, configure the same in Fusion: @@ -593,7 +593,7 @@ app.MapGraphQL(); app.Run(); ``` -#### Step 4: Compose and Run +### Step 4: Compose and Run Compose your subgraph schemas into a gateway archive: @@ -613,7 +613,7 @@ dotnet run Navigate to `http://localhost:5000/graphql` to open the Nitro IDE and run cross-subgraph queries. -### Phase 3: Migrate CI/CD +## Phase 3: Migrate CI/CD Replace Apollo's `rover` commands with Nitro CLI equivalents. @@ -623,7 +623,7 @@ Replace Apollo's `rover` commands with Nitro CLI equivalents. | `rover subgraph publish` | `nitro fusion upload` + `nitro fusion publish` | | `rover supergraph compose` | `nitro fusion compose` | -#### Schema Upload (Replaces Rover Subgraph Publish) +### Schema Upload (Replaces Rover Subgraph Publish) In Apollo, publishing a subgraph triggers server-side composition. In Fusion, this is a two-step process: upload the schema, then publish to trigger composition. @@ -655,7 +655,7 @@ nitro fusion publish \ --api-key $NITRO_API_KEY ``` -#### Schema Validation (Replaces Rover Subgraph Check) +### Schema Validation (Replaces Rover Subgraph Check) **Apollo:** @@ -675,7 +675,7 @@ nitro fusion validate \ --api-key $NITRO_API_KEY ``` -#### Local Composition (Replaces Rover Supergraph Compose) +### Local Composition (Replaces Rover Supergraph Compose) **Apollo:** @@ -692,7 +692,7 @@ nitro fusion compose \ --archive gateway.far ``` -#### Example GitHub Actions Workflow +### Example GitHub Actions Workflow ```yaml name: Deploy Subgraph @@ -740,19 +740,19 @@ jobs: For more on deployment workflows, see [Deployment and CI/CD](../deployment-and-ci-cd.md). -## Mindset Shifts +# Mindset Shifts If you have spent significant time with Apollo Federation, some habits need adjusting. These are not just naming differences -- they change how you think about your graph. -### Entity Resolution Is Explicit and Testable +## Entity Resolution Is Explicit and Testable In Apollo, entity resolution happens through a hidden protocol (`_entities` + `__resolveReference`). You cannot easily call `_entities` from a GraphQL client to debug resolution issues. In Fusion, entity resolution is a regular query field. You can open your IDE, call `productById(id: 1)`, and see exactly what your lookup returns. This makes debugging straightforward. -### You Don't Need to Think About Entity Ownership the Same Way +## You Don't Need to Think About Entity Ownership the Same Way Apollo Federation has a strong concept of entity "ownership" -- one subgraph is the "defining" subgraph for an entity, and others "extend" it. In Fusion, all subgraphs contribute fields to shared entity types. The gateway uses lookups to resolve entities wherever they need to be fetched. The question is not "who owns this entity?" but "which subgraphs provide lookups for it?" -### Composition Is a Build Step, Not a Cloud Operation +## Composition Is a Build Step, Not a Cloud Operation In Apollo, composition typically happens in GraphOS when you publish a subgraph. In Fusion, composition is a command you run locally or in CI: @@ -762,7 +762,7 @@ nitro fusion compose --archive gateway.far You can run this on your machine, see the output, inspect errors, and fix them before pushing. There is no cloud service in the loop unless you choose to use Nitro cloud. -### Requirements Operate on Arguments, Not Fields +## Requirements Operate on Arguments, Not Fields This changes resolver design. In Apollo, required data appears on the entity object. In Fusion, it arrives as a method parameter: @@ -776,11 +776,11 @@ public int GetShippingEstimate([Require("weight")] int weight) This makes dependencies explicit in the method signature. You can see exactly what data a resolver needs by looking at its parameters. -### The Gateway Is Your Code +## The Gateway Is Your Code Apollo Router is a pre-built binary you configure externally. Fusion's gateway is an ASP.NET Core application you control. You write `Program.cs`, configure middleware, add authentication, and deploy it like any other .NET service. This means you have full control but also full responsibility for the gateway's behavior. -## What Gets Simpler +# What Gets Simpler Migrating to Fusion resolves several common pain points from Apollo Federation. @@ -796,25 +796,25 @@ Migrating to Fusion resolves several common pain points from Apollo Federation. **Simpler subgraph setup.** Without a federation library, there are fewer moving parts. A minimal Fusion subgraph is just a HotChocolate server with `[Lookup]` on its entity query fields and a `schema-settings.json` file. That is the entire federation surface area. -## FAQ +# FAQ -### Can I migrate incrementally -- some subgraphs on Apollo, some on Fusion? +## Can I migrate incrementally -- some subgraphs on Apollo, some on Fusion? No. Apollo Federation and Fusion use different gateway protocols and composition models. You cannot run a mixed fleet where some subgraphs speak Apollo's federation protocol and others speak Fusion's. You need to migrate all subgraphs and the gateway together. However, you can migrate one subgraph at a time by converting and testing each one before switching the gateway over. -### Do I need Nitro cloud? +## Do I need Nitro cloud? No. Nitro cloud provides managed composition and gateway configuration delivery (similar to Apollo's GraphOS), but everything works without it. You can compose schemas locally with `nitro fusion compose`, load the `.far` file from disk with `AddFileSystemConfiguration()`, and never touch a cloud service. Nitro cloud is optional for teams that want managed schema delivery. -### What About Apollo Federation Auth Directives? +## What About Apollo Federation Auth Directives? Fusion uses standard ASP.NET Core authentication and authorization. You configure JWT/cookie authentication in the gateway's middleware pipeline and use HotChocolate's `[Authorize]` attribute on fields and types in your subgraphs. There is no Fusion-specific auth directive -- you use the same patterns you already know from ASP.NET Core. -### Can Fusion handle subscriptions? +## Can Fusion handle subscriptions? Yes. Fusion supports real-time subscriptions through the gateway via SSE (Server-Sent Events) and WebSocket transports. Subgraphs can use any HotChocolate subscription provider, including Postgres-backed subscriptions for multi-instance scenarios. -### Where do I go from here? +## Where do I go from here? After migrating, these pages provide deeper coverage of specific topics: diff --git a/website-next/content/docs/fusion/performance-tuning.md b/website-next/content/docs/fusion/performance-tuning.md index f6a0bbb9566..05f1e7aa606 100644 --- a/website-next/content/docs/fusion/performance-tuning.md +++ b/website-next/content/docs/fusion/performance-tuning.md @@ -12,7 +12,7 @@ This page covers: - Deduplicating identical in-flight requests - Limiting concurrent request processing to maximize throughput -## Configure the HTTP Transport +# Configure the HTTP Transport Fusion uses a **named `HttpClient`** to communicate with subgraphs. The default client name is `"fusion"`, and you configure it through the standard `IHttpClientFactory` pattern. This gives you full control over connection behavior, timeouts, and message handlers. @@ -37,11 +37,11 @@ app.Run(); 1. **Named HTTP client `"fusion"`**: the client the gateway uses to call subgraphs. Any handler configuration you add here applies to all subgraph requests. 2. **Gateway registration**: wires up the Fusion execution engine and loads the composed schema. -## HTTP/2 +# HTTP/2 HTTP/2 multiplexes multiple requests over a single TCP connection, which reduces connection overhead when the gateway sends many concurrent requests to a subgraph. This is especially beneficial when subgraphs are behind a load balancer that supports HTTP/2. -### With TLS +## With TLS When your subgraphs use TLS (HTTPS), HTTP/2 is negotiated automatically via ALPN. Enable `EnableMultipleHttp2Connections` to allow the gateway to open additional HTTP/2 connections when a single connection's stream limit is reached: @@ -56,7 +56,7 @@ builder.Services No additional version configuration is needed. .NET negotiates HTTP/2 over TLS by default. -### Without TLS +## Without TLS In many Kubernetes deployments, services communicate over plaintext HTTP inside the cluster. HTTP/2 cleartext (h2c) requires explicit opt-in because .NET defaults to HTTP/1.1 for unencrypted connections. @@ -89,11 +89,11 @@ builder.WebHost.ConfigureKestrel(options => If you are unsure whether your infrastructure supports HTTP/2 cleartext end-to-end, **HTTP/1.1 works well** for most internal deployments. Switch to HTTP/2 only when you have confirmed support on both the gateway and all subgraphs. -## Request Deduplication +# Request Deduplication When multiple identical query requests are in flight to the same subgraph at the same time, **request deduplication** ensures only one HTTP request is actually sent. The first request becomes the "leader" and executes normally. Subsequent identical requests become "followers" that wait for the leader's response. Each caller receives an independent copy of the result. -### When It Helps +## When It Helps Deduplication is most effective when: @@ -101,7 +101,7 @@ Deduplication is most effective when: - **Public APIs** serve unauthenticated traffic where many users send the same queries. - **The same user** sends identical concurrent requests (e.g., a UI that fires duplicate fetches). -### Security Model +## Security Model The deduplication hash includes the request body, URL, and the values of configurable **hash headers**. By default, `Authorization` and `Cookie` headers are included in the hash. This means: @@ -109,7 +109,7 @@ The deduplication hash includes the request body, URL, and the values of configu - **Same user, same query, concurrent**: identical tokens produce the same hash and are deduplicated. - **Different users, same query**: different tokens produce different hashes. Never deduplicated. One user's response is never shared with another. -### How to Enable +## How to Enable Add the request deduplication message handler with `.AddRequestDeduplication()` to the named HTTP client builder: @@ -119,7 +119,7 @@ builder.Services .AddRequestDeduplication(); ``` -### Customizing Hash Headers +## Customizing Hash Headers By default, the `Authorization` and `Cookie` headers are included in the deduplication hash, which covers most setups. If you need additional headers to be part of the hash, for instance a tenant identifier in a multi-tenant application, add them to `HashHeaders`: @@ -139,7 +139,7 @@ For **service-to-service communication** where the gateway does not receive cook }); ``` -### What Gets Deduplicated +## What Gets Deduplicated Only **query operations** are deduplicated. The following are **not** deduplicated: @@ -147,7 +147,7 @@ Only **query operations** are deduplicated. The following are **not** deduplicat - **Subscriptions**: long-lived connections that are inherently unique. - **File uploads**: multipart requests bypass deduplication. -## Concurrent Execution Limiting +# Concurrent Execution Limiting The gateway limits the number of simultaneous **executions** it processes using a **concurrency gate**. An execution is the work of running a single GraphQL operation end-to-end. Each query or mutation counts as one execution, and each event a subscription emits counts as one execution while its selection set runs. Capping concurrency keeps the gateway operating in its optimal throughput range. Too much work competing for the same resources (thread pool, memory, connections) can reduce overall throughput rather than increase it. @@ -177,14 +177,14 @@ app.MapGraphQLHttp() }); ``` -### Tuning Guidance +## Tuning Guidance - **Too low**: executions queue behind the concurrency gate, adding latency even when the gateway and subgraphs have spare capacity. Subscriptions with high event rates feel this first. - **Too high**: the gateway runs more work than it can efficiently process, leading to thread pool starvation and increased latency across queries, mutations, and subscription events. Start with the default of 64 and adjust based on your workload. If you expect many long-lived subscriptions firing frequent events, factor those into your sizing. They now contend for the same slots as queries and mutations. Set to `null` to disable the limit entirely. -### Execution Cancellation +## Execution Cancellation 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. @@ -198,7 +198,7 @@ builder If executions routinely time out at the gate, that is a signal to scale out or raise `MaxConcurrentExecutions`. Increasing `ExecutionTimeout` only defers the problem. -## Next Steps +# Next Steps - **"I need CDN and HTTP response caching behavior"**: [Cache Control](./cache-control.md) covers `@cacheControl`, composition merge behavior, and gateway response headers. - **"I need to secure my gateway"**: [Authentication and Authorization](./authentication-and-authorization.md) covers JWT validation, header propagation, and subgraph-level authorization. diff --git a/website-next/content/docs/fusion/request-limits.md b/website-next/content/docs/fusion/request-limits.md index c7bbad6e997..7a4b6c1414b 100644 --- a/website-next/content/docs/fusion/request-limits.md +++ b/website-next/content/docs/fusion/request-limits.md @@ -12,7 +12,7 @@ This page covers: - Planner guardrails that prevent expensive query plan generation - Execution limits that bound time, request size, and transport features -## Parser Limits +# Parser Limits Parser limits stop malicious payloads before the AST is fully constructed. They are the first line of defense because they run before any semantic analysis. @@ -34,11 +34,11 @@ builder | `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 +# Validation Limits After parsing, the validation pipeline enforces semantic limits on the query structure. These limits protect against queries that are syntactically valid but computationally expensive to execute. -### Execution Depth +## Execution Depth Caps how deeply nested a query can be. A depth of 10 covers most real-world queries while blocking deeply nested attacks: @@ -46,15 +46,15 @@ Caps how deeply nested a query can be. A depth of 10 covers most real-world quer builder.AddMaxExecutionDepthRule(10); ``` -### Fragment Visits +## 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. The total number of fragment visits per operation is capped at **1,000** by default. -### Field Merge Comparisons +## Field Merge Comparisons The overlapping-fields-can-be-merged rule caps comparison work at 100,000 by default. No configuration is needed for most gateways. The default protects against fragment expansion bombs. -### Field Coordinate Cycles +## 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. @@ -98,7 +98,7 @@ builder.AddMaxAllowedFieldCycleDepthRule( 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 +## Validation Errors Caps the total number of validation errors reported per request. The default is 5. When the limit is reached, validation stops early instead of continuing to accumulate errors. @@ -106,7 +106,7 @@ Caps the total number of validation errors reported per request. The default is builder.SetMaxAllowedValidationErrors(5); ``` -### Introspection Depth +## 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. @@ -116,7 +116,7 @@ builder.SetIntrospectionAllowedDepth( maxAllowedListRecursiveDepth: 1); ``` -### Disable Introspection +## Disable Introspection Introspection is disabled in non-development environments by default as part of the default security policy. This prevents clients from running expensive introspection queries against production systems. To disable it unconditionally (including in development): @@ -124,7 +124,7 @@ Introspection is disabled in non-development environments by default as part of builder.DisableIntrospection(); ``` -## Operation Planner Guardrails +# Operation Planner Guardrails Before execution, the gateway plans how to distribute the query across subgraphs. Complex queries can cause expensive planning. These guardrails prevent planning from consuming excessive resources. @@ -145,9 +145,9 @@ builder | `MaxQueueSize` | Disabled | Maximum size of the planner's internal work queue. | | `MaxGeneratedOptionsPerWorkItem` | Disabled | Maximum number of options the planner generates per work item. | -## Execution Limits +# Execution Limits -### Timeout +## Timeout Requests are aborted after 30 seconds by default. The timeout covers the entire request including all subgraph calls. It is not enforced when a debugger is attached. @@ -159,7 +159,7 @@ builder }); ``` -### HTTP Request Size +## HTTP Request Size The maximum HTTP request body size defaults to approximately 20 MB: @@ -167,7 +167,7 @@ The maximum HTTP request body size defaults to approximately 20 MB: builder.AddGraphQLGateway(maxAllowedRequestSize: 5 * 1000 * 1024); // 5 MB ``` -### Server Options +## Server Options Control which HTTP methods and features are available: @@ -179,7 +179,7 @@ builder }); ``` -## Next Steps +# Next Steps - **"I need to secure my gateway."** [Authentication and Authorization](./authentication-and-authorization.md) covers JWT validation, header propagation, and subgraph-level authorization. - **"I need to tune transport performance."** [Performance Tuning](./performance-tuning.md) covers HTTP/2, request deduplication, and concurrency limiting. diff --git a/website-next/content/docs/fusion/schema-exposure-and-evolution.md b/website-next/content/docs/fusion/schema-exposure-and-evolution.md index 186f46b38f0..405f4d99b72 100644 --- a/website-next/content/docs/fusion/schema-exposure-and-evolution.md +++ b/website-next/content/docs/fusion/schema-exposure-and-evolution.md @@ -7,11 +7,11 @@ Not everything in your source schema should be visible to clients, and not every This page covers five directives that handle exposure and evolution. `@inaccessible` and `@internal` control visibility in the composite schema. `@deprecated` and `@requiresOptIn` manage the lifecycle of fields and values. `@override` migrates field ownership between subgraphs. If you have completed the [Getting Started](./getting-started.md) tutorial and worked through [Entities and Lookups](./entities-and-lookups.md), you already used `@internal` on lookup fields. Here, you will see the full picture of visibility control and schema evolution. -## Controlling Client Visibility +# Controlling Client Visibility Your source schemas contain fields and types that serve different audiences. Some are for clients, some carry internal data shared between subgraphs, and some are infrastructure that only the gateway uses. Fusion provides two directives for hiding schema elements from the composite schema. They differ in how they interact with composition merging. -### Hidden Fields +## Hidden Fields Mark a field or type `@inaccessible` to hide it from the public client-facing composite schema while keeping it available for internal. The element still participates in composition merging and can be referenced by `@require` dependencies in other subgraphs. @@ -66,7 +66,7 @@ The `CANCELLED` value does not appear in the composite schema. Subgraphs can sti **Constraint:** You cannot mark a required input field as `@inaccessible`. If a client must provide a value, they need to see the field. Composition fails if you try. -### Internal Lookups +## Internal Lookups The `@internal` directive is designed for lookups. An internal lookup is a query field that the gateway uses for entity resolution but that clients cannot call. Internal lookups do not participate in composition merging, which means multiple subgraphs can define lookups with the same field name and different argument shapes without causing a conflict. This gives each subgraph the flexibility to resolve an entity in whatever way makes sense for its data, without coordinating field signatures across teams. @@ -129,7 +129,7 @@ public partial class InternalLookups For a deeper look at internal vs. public lookups, composite keys, and the node pattern, see [Entities and Lookups](./entities-and-lookups.md). -### Choosing Between Hidden and Internal +## Choosing Between Hidden and Internal These directives serve different purposes. `@inaccessible` hides data from clients while keeping it available across subgraphs. `@internal` keeps lookups local to one subgraph so they can vary freely without merge conflicts. @@ -143,7 +143,7 @@ These directives serve different purposes. `@inaccessible` hides data from clien Use `@inaccessible` when the field carries data that other subgraphs need but clients should not see. Use `@internal` on lookups that exist only for gateway entity resolution. -## Deprecating Fields and Values +# Deprecating Fields and Values The `@deprecated` directive signals that a field, argument, or enum value is being phased out. Clients see the deprecation reason in introspection, and GraphQL tooling (IDEs, linters, code generators) can warn consumers to migrate away. The field continues to work. Deprecation is a soft signal, not a hard removal. @@ -206,13 +206,13 @@ enum SortOrder { **Constraint:** You cannot deprecate a non-null argument or input field without a default value. If clients must provide a value, they cannot stop using the field. -### Deprecation Across Subgraphs +## Deprecation Across Subgraphs If a shareable field is deprecated in at least one subgraph, it is deprecated in the composite schema. You do not need to deprecate it in every subgraph that defines it. With shared ownership comes the power for any owner to deprecate the field for all clients. If you only want to remove a shared field from one subgraph, you do not need to deprecate it. Remove the field from that subgraph and the gateway will resolve it from the remaining subgraphs that still provide it. -## Experimental and Preview Features +# Experimental and Preview Features The `@requiresOptIn` directive is the counterpart to `@deprecated`. Where `@deprecated` signals that a field is going away, `@requiresOptIn` signals that a field is not yet stable. Fields marked with `@requiresOptIn` are hidden from introspection by default. Clients must explicitly opt in to discover and use them. @@ -257,7 +257,7 @@ The directive is repeatable. A single field can be part of multiple features. public decimal? DynamicPrice { get; set; } ``` -### Enabling Opt-In Support +## Enabling Opt-In Support Opt-in features are disabled by default. Enable them in your schema configuration. @@ -272,7 +272,7 @@ builder .ModifyOptions(o => o.EnableOptInFeatures = true); ``` -### Discovering Opt-In Fields +## Discovering Opt-In Fields Clients pass the `includeOptIn` argument in introspection queries to discover opt-in fields. @@ -299,7 +299,7 @@ To discover which opt-in features exist in the schema: } ``` -### Feature Stability Levels +## Feature Stability Levels You can declare the stability level of each opt-in feature at the schema level. This lets consumers know whether a feature is experimental, preview, or any other stability level you define. @@ -328,15 +328,15 @@ Consumers can query feature stability through introspection: } ``` -### Constraints +## Constraints Like `@deprecated`, you cannot apply `@requiresOptIn` to non-null arguments or input fields without a default value. Hiding a required field would break queries that need to provide it. -### Opt-In Across Subgraphs +## Opt-In Across Subgraphs If a shareable field is marked `@requiresOptIn` in at least one subgraph, it requires opt-in in the composite schema. To make the field generally available again, every subgraph that defines it must remove the `@requiresOptIn` directive. This is the inverse of `@deprecated`, where a single subgraph can deprecate a field for all clients. With `@requiresOptIn`, a single subgraph can gate a shared field behind opt-in, and it stays gated until all owners agree to remove the restriction. -## Migrating Field Ownership Between Subgraphs +# Migrating Field Ownership Between Subgraphs As your system evolves, you may need to move a field from one subgraph to another. A team might split a subgraph, or a field might belong more naturally in a different domain. The `@override` directive migrates field ownership without breaking existing queries. @@ -386,7 +386,7 @@ type Product { The `from` argument is the subgraph name (from `schema-settings.json`) that originally owned the field. -### Migration Workflow +## Migration Workflow 1. Add the field to the new subgraph with `[Override(from: "old-subgraph")]`. 2. Export schemas and compose. Composition validates that the override is valid. @@ -395,7 +395,7 @@ The `from` argument is the subgraph name (from `schema-settings.json`) that orig The old resolver stays in place during the transition. Both subgraphs can define the field simultaneously because `[Override]` tells composition which one wins. This avoids duplicate-field errors without requiring `[Shareable]`. -## Next Steps +# Next Steps - **Need entity resolution patterns?** See [Entities and Lookups](./entities-and-lookups.md) for public vs. internal lookups, composite keys, and the node pattern. - **Need cross-subgraph field dependencies?** See [Data Requirements](./data-requirements-and-mapping.md) for `@require`, `@is`, and FieldSelectionMap patterns. diff --git a/website-next/content/docs/fusion/source-schema-extensions.md b/website-next/content/docs/fusion/source-schema-extensions.md index ae9a27efdd0..fbc18f3c583 100644 --- a/website-next/content/docs/fusion/source-schema-extensions.md +++ b/website-next/content/docs/fusion/source-schema-extensions.md @@ -5,7 +5,7 @@ description: "Add Fusion directives like @lookup and @inaccessible to schemas yo Some source schemas are not yours to edit, or you want to keep them free of Fusion-specific annotations. Fusion still needs [directives](./directives-reference.md) like `@lookup`, `@internal`, and `@inaccessible` on those schemas to compose your graph correctly. A **source schema extensions document** solves this. It is a separate SDL file you author yourself, layered over the base schema at parse time, that adds Fusion-specific annotations without modifying a single character of the base. Your original schema stays clean, and the directives the gateway needs to plan distributed queries land in a file you fully control. -## How It Works +# How It Works Both documents (the base schema and its extensions) are parsed sequentially into the same schema model. Extensions written with `extend type X` merge into the canonical `type X` definition during parsing. By the time composition runs, your extensions are indistinguishable from directives written directly on the base schema. Composition never sees the `extend` syntax: the parser has already folded everything into one source schema. @@ -18,11 +18,11 @@ flowchart LR C --> E[Execution schema] ``` -## A Realistic Example +# A Realistic Example Suppose you compose a federated graph from several services. One of them is a Products service that exposes `products.graphqls` as its source schema, and you want to keep that file free of Fusion-specific annotations. You need to mark `productById` as a lookup, expose a second lookup that Reviews uses for hydration without making it part of the public surface, hide an internal `warehouseLocationCode` field, and add a derived `stockStatus` field that depends on the warehouse code. -### The Base Schema +## The Base Schema ```graphql type Query { @@ -47,7 +47,7 @@ type Money { There is not a single Fusion annotation in this file. It can change without forcing edits to your Fusion-specific configuration. Every Fusion-specific decision lives in the next document. -### The Extensions Document +## The Extensions Document ```graphql extend type Query { @@ -77,7 +77,7 @@ What each block does: - `extend type Product { stockStatus(warehouseLocationCode: ... @require(...)): StockStatus! }` adds a brand new field with a hidden resolver argument. `@require(field: "warehouseLocationCode")` tells the gateway to populate the argument from the existing `warehouseLocationCode` field, and the argument is removed from the public surface so clients see `stockStatus: StockStatus!` with no arguments. Adding a field via extensions still requires that field to be resolvable at runtime by the underlying subgraph implementation: the extensions document only declares the field, it does not provide the resolver. See [Data Requirements](./data-requirements-and-mapping.md) for `@require` semantics. - `enum StockStatus { ... }` introduces a new type. Extensions can declare types the base schema does not, because extensions are valid SDL documents in their own right. -### The Merged Result +## The Merged Result After parsing, the source schema looks like this from composition's perspective: @@ -108,7 +108,7 @@ enum StockStatus { This is the same shape you would have if you had hand-edited the base schema. The difference is purely in how you got there: two files, parsed in order, with the second contributing only the deltas. -## What You Can Do with an Extensions Document +# What You Can Do with an Extensions Document **Apply directives to existing fields.** This is the most common use. Directives commonly applied this way include `@lookup`, `@internal`, `@inaccessible`, `@shareable`, `@external`, `@provides`, `@override`, `@deprecated`, and `@requiresOptIn`. For the semantics of each, see [Entities and Lookups](./entities-and-lookups.md), [Field Ownership](./field-ownership-and-sharing.md), and [Schema Exposure and Evolution](./schema-exposure-and-evolution.md). @@ -150,9 +150,9 @@ enum StockStatus { } ``` -## Using Extensions +# Using Extensions -### File Convention +## File Convention Place the extensions file alongside the base schema using the `{name}-extensions.graphqls` naming convention. The Fusion CLI loads it automatically. There is no extra flag and no configuration entry to add. @@ -166,13 +166,13 @@ schemas/ When `nitro fusion compose` runs, each base schema and its matching extensions document are parsed together into a single source schema before composition starts. -### Inside the Fusion Archive +## Inside the Fusion Archive The archive preserves both files separately. The base schema is stored at `source-schemas/{name}/schema.graphqls`, and the extensions document is stored at `source-schemas/{name}/schema-extensions.graphqls`. The base schema is stored exactly as supplied. Fusion does not fold the extensions into it. This matters because recomposition (when a subgraph's schema changes and the archive is rebuilt) needs the extensions document to reapply your annotations on top of the new base. Without it, the directive layer would be lost on every regeneration. Keeping the two files distinct in the archive is what makes "the base schema stays untouched" hold end-to-end, not only in your repo but in every downstream consumer. -## Behavior Reference +# Behavior Reference | Situation | Behavior | | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | @@ -182,7 +182,7 @@ The base schema is stored exactly as supplied. Fusion does not fold the extensio | Extension applies a directive the schema does not define | Implicit at parse time. Fusion's built-ins compose as-is; other custom directives must be declared. | | Extension re-applies a non-repeatable directive to the target | Parsing fails when the extension's directives are merged onto the target. | -## Troubleshooting +# Troubleshooting **My directive did not apply.** Check the field name. If it does not exactly match a field on the base type, the parser adds a new field rather than extending the existing one, and the directive lands on that new field. Return type or repeated argument mismatches are different and cause parsing to fail rather than silently producing a new field. @@ -190,7 +190,7 @@ The base schema is stored exactly as supplied. Fusion does not fold the extensio **I want to apply a directive that the schema does not import.** Fusion's built-in directives compose without further declaration. For any other custom directive, declare it in the schema (or in the extensions document) before applying it; otherwise composition will fail validation even though the parser accepts the unknown directive. -## Next Steps +# Next Steps - [Composition](./composition.md): what happens after extensions are folded in. - [Adding a Subgraph](./adding-a-subgraph.md): the typical entry point that may use extensions. diff --git a/website-next/content/docs/hotchocolate/build/adapters/mcp.md b/website-next/content/docs/hotchocolate/build/adapters/mcp.md index 5ea401f6c6d..bf14d4530d5 100644 --- a/website-next/content/docs/hotchocolate/build/adapters/mcp.md +++ b/website-next/content/docs/hotchocolate/build/adapters/mcp.md @@ -9,7 +9,7 @@ You wire MCP onto an existing Hot Chocolate server with two calls: `AddMcp()` du This page covers wiring and configuration. For authoring tools and prompts, see the [Nitro MCP](../../../nitro/adapters/mcp.md) section. -## Prerequisites +# Prerequisites You need an existing Hot Chocolate GraphQL server. If you do not have one yet, follow the [Get started with Hot Chocolate](../../get-started-with-graphql-in-net-core.md) tutorial first. @@ -19,7 +19,7 @@ Add the adapter package to the server project: dotnet add package HotChocolate.Adapters.Mcp ``` -## Enabling MCP on the Server +# 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. @@ -36,7 +36,7 @@ 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 +# 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: @@ -62,7 +62,7 @@ Three overloads cover the common registration patterns: `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 +# 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): @@ -82,7 +82,7 @@ builder 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 +# Mapping the MCP Endpoint `MapGraphQLMcp()` accepts two optional arguments: @@ -100,11 +100,11 @@ 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 +# 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 +## Install the Nitro packages ```bash dotnet add package ChilliCream.Nitro @@ -113,7 +113,7 @@ 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 +## 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): @@ -149,7 +149,7 @@ If you prefer environment variables over inline configuration, set `NITRO_API_ID > 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 +## What you get With Nitro and MCP both enabled: @@ -161,32 +161,32 @@ With Nitro and MCP both enabled: For authoring tools and prompts, publishing feature collection versions, and managing stages, see the [Nitro MCP](../../../nitro/adapters/mcp.md) section. -## Troubleshooting +# Troubleshooting -### `InvalidOperationException: Call AddMcp() when configuring the GraphQL server.` +## `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 +## 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 ''.` +## `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 +## 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.` +## 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 +# Next Steps - Author tools and prompts and publish them with Nitro in the [Nitro MCP](../../../nitro/adapters/mcp.md) section. - Customize how GraphQL errors surface in MCP tool results with [Errors](../../resolvers/errors.md). diff --git a/website-next/content/docs/nitro/apis/deployments.md b/website-next/content/docs/nitro/apis/deployments.md index 01f21d0abea..59dcf48778d 100644 --- a/website-next/content/docs/nitro/apis/deployments.md +++ b/website-next/content/docs/nitro/apis/deployments.md @@ -9,7 +9,7 @@ Deploying a service typically involves publishing a client, schema, or fusion co 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 +# 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. diff --git a/website-next/content/docs/nitro/explore-the-ui/explorer.md b/website-next/content/docs/nitro/explore-the-ui/explorer.md index 94a6f1eeb33..f00d286db93 100644 --- a/website-next/content/docs/nitro/explore-the-ui/explorer.md +++ b/website-next/content/docs/nitro/explore-the-ui/explorer.md @@ -7,14 +7,14 @@ The Explorer is a versatile component utilized in various parts of the applicati ![Explorer](../../../../public/images/nitro-docs/explore-the-ui/explorer-0.webp) -## 1. Quick Actions +# 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 +# 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 +# 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. diff --git a/website-next/content/docs/nitro/nitro-services.md b/website-next/content/docs/nitro/nitro-services.md index 01af323db4b..2ff9a8d7e25 100644 --- a/website-next/content/docs/nitro/nitro-services.md +++ b/website-next/content/docs/nitro/nitro-services.md @@ -5,7 +5,7 @@ description: "Connect your Hot Chocolate server to Nitro to fetch persisted oper 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](./apis/client-registry.md). -## Getting Started +# Getting Started To get started, follow these steps: diff --git a/website-next/content/docs/nitro/open-telemetry/logging.md b/website-next/content/docs/nitro/open-telemetry/logging.md index 095697a3007..b7d8ba7d509 100644 --- a/website-next/content/docs/nitro/open-telemetry/logging.md +++ b/website-next/content/docs/nitro/open-telemetry/logging.md @@ -6,32 +6,32 @@ description: "Collect and analyze OpenTelemetry logs in Nitro: browse the Logs t 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 ![Api Logs](../../../../public/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 +## Detailed Log Inspection ![API Logs - Expanded](../../../../public/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 ![Trace Logs](../../../../public/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 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 +# Connect your service All the logging is done on a per API basis. An api represents one of your deployments. @@ -111,6 +111,6 @@ public void ConfigureServices(IServiceCollection services) > > In this setup, the API key, ID, and stage are set through environment variables. -## Full Example +# Full Example For a complete implementation example, visit the [example repository](https://link.chillicream.com/docs/logging-example). diff --git a/website-next/content/docs/nitro/open-telemetry/service-monitoring.md b/website-next/content/docs/nitro/open-telemetry/service-monitoring.md index 0e322efad45..6fe81a64b9d 100644 --- a/website-next/content/docs/nitro/open-telemetry/service-monitoring.md +++ b/website-next/content/docs/nitro/open-telemetry/service-monitoring.md @@ -8,15 +8,15 @@ Nitro’s OpenTelemetry support extends beyond GraphQL, allowing you to gather a > [!NOTE] > If you are looking specifically for GraphQL telemetry, please refer to the [Operation Monitoring](./operation-monitoring.md#connect-your-service-to-the-telemetry-system) section. -## Prerequisites +# 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 +# Quick Start -### 1. Install Required Packages +## 1. Install Required Packages In your .NET project, install the following NuGet packages if they are not already present: @@ -27,7 +27,7 @@ dotnet add package OpenTelemetry --version dotnet add package OpenTelemetry.Extensions.Hosting --version ``` -### 2. Register Nitro and OpenTelemetry Exporters +## 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`: @@ -42,7 +42,7 @@ services .AddOpenTelemetry(); ``` -### 3. Add Additional Instrumentation +## 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. @@ -58,7 +58,7 @@ services.ConfigureOpenTelemetryTracerProvider(x => }); ``` -### 4. View Your Traces +## 4. View Your Traces Once your service is running, head over to the **Nitro dashboard**. diff --git a/website-next/content/docs/strawberryshake/get-started/console.md b/website-next/content/docs/strawberryshake/get-started/console.md index 24150b90971..e5a403127cf 100644 --- a/website-next/content/docs/strawberryshake/get-started/console.md +++ b/website-next/content/docs/strawberryshake/get-started/console.md @@ -14,7 +14,7 @@ In this tutorial, we will teach you: - How to use the generated client in a classical or reactive way. - How to disable state management for ASP.NET core use-cases. -## Step 1: Add the Strawberry Shake CLI tools +# Step 1: Add the Strawberry Shake CLI tools The Strawberry Shake tool will help you to set up your project to create a GraphQL client. @@ -32,7 +32,7 @@ dotnet new tool-manifest dotnet tool install StrawberryShake.Tools --local ``` -## Step 2: Create a console project +# Step 2: Create a console project Next, we will create our console project so that we have a little playground. @@ -54,7 +54,7 @@ dotnet new console --name Demo dotnet sln add ./Demo ``` -## Step 3: Install the required packages +# Step 3: Install the required packages Strawberry Shake supports multiple GraphQL transport protocols. In this example we will use the standard GraphQL over HTTP protocol to interact with our GraphQL server. @@ -64,7 +64,7 @@ Strawberry Shake supports multiple GraphQL transport protocols. In this example dotnet add Demo package StrawberryShake.Server ``` -## Step 4: Add a GraphQL client to your project using the CLI tools +# Step 4: Add a GraphQL client to your project using the CLI tools To add a client to your project, you need to run `dotnet graphql init {{ServerUrl}} --clientName {{ClientName}}`. @@ -156,7 +156,7 @@ var services = serviceCollection.BuildServiceProvider(); var client = services.GetRequiredService(); ``` -## Step 5: Use the ConferenceClient to perform a simple fetch +# Step 5: Use the ConferenceClient to perform a simple fetch In this section we will perform a simple fetch with our `ConferenceClient` and output the result to the console. diff --git a/website-next/scripts/optimize-images.mjs b/website-next/scripts/optimize-images.mjs index d68f5ff4750..47f6dc972f1 100755 --- a/website-next/scripts/optimize-images.mjs +++ b/website-next/scripts/optimize-images.mjs @@ -12,7 +12,10 @@ import optimizeImages from "../src/image-optimization/generate.mjs"; const config = { quality: 90, - widths: [640, 1080, 1920], + // 828 fills the 640->1080 gap: content columns top out around 1024px CSS, + // so ~700-828px slots (tablet 1x, phone 2x) would otherwise be forced up to + // the 1080 variant. + widths: [640, 828, 1080, 1920], formats: ["avif", "webp"], sourceDir: "public/images", outputDir: "public/_optimized/images", diff --git a/website-next/src/image-optimization/generate.mjs b/website-next/src/image-optimization/generate.mjs index bc1c01dad38..d75980ecd67 100644 --- a/website-next/src/image-optimization/generate.mjs +++ b/website-next/src/image-optimization/generate.mjs @@ -168,12 +168,16 @@ async function run(config) { let remoteFetched = 0; let remoteCached = 0; - const remoteTasks = remotes.map(({ key, url, fallbackUrl }) => async () => { + const remoteTasks = remotes.map((remote) => async () => { + const { key, url, fallbackUrl } = remote; + // Per-entry ladder override (e.g. avatars only need tiny DPR variants). + const ladder = remote.widths ?? widths; try { const sha = sha256(key); // URL-based cache key: the remote content is assumed stable, so we do not - // re-download on every build. - const hash = sha256(key + settingsJson); + // re-download on every build. The fetch URL and ladder are part of the + // hash so changing either re-encodes the entry. + const hash = sha256(key + url + settingsJson + JSON.stringify(ladder)); const existing = manifest[key]; if ( @@ -206,7 +210,7 @@ async function run(config) { const meta = await sharp(bytes).metadata(); const intrinsicW = meta.width ?? 0; const intrinsicH = meta.height ?? 0; - const targetWidths = computeTargetWidths(widths, intrinsicW); + const targetWidths = computeTargetWidths(ladder, intrinsicW); const formatsEntry = {}; for (const format of formats) { diff --git a/website-next/src/image-optimization/remote.mjs b/website-next/src/image-optimization/remote.mjs index 711ab023762..eeadac47e9f 100644 --- a/website-next/src/image-optimization/remote.mjs +++ b/website-next/src/image-optimization/remote.mjs @@ -4,6 +4,14 @@ import matter from "gray-matter"; const MD_RE = /\.(md|mdx)$/i; +// Author avatars render at 30px (see BlogMetadata), so the srcset ladder only +// needs 1x/2x/3x DPR variants instead of the global content-image ladder. +const AVATAR_WIDTHS = [30, 60, 90]; + +// Ask GitHub for a pre-scaled avatar (covers the largest ladder width at 2x) +// instead of downloading the full-size original. +const AVATAR_FETCH_SIZE = 180; + // Matches an 11-char YouTube id in any of the supported URL forms or in a //