# Migrate Hot Chocolate Fusion from 15 to 16 - Fusion

> Migration guide for Hot Chocolate Fusion v15 to v16: publish dual archive formats per subgraph, then cut the gateway over to the new `.far` configuration.

Canonical source: https://chillicream.com/docs/fusion/migration/migrate-from-15-to-16

Note

While directives and behavior largely mirror v15, v16 is a complete re-implementation of Fusion that not only affects the gateway itself, but also the archive format and composition process. Therefore, you can't simply bump the package versions in the gateway and be done with the update. You'll need a coordinated strategy to incrementally adopt Fusion v2 in Subgraphs and their deployment process, before you can switch the gateway to v16.

## Migration at a glance

The migration happens in three stages, and the gateway keeps serving traffic the whole time:

1. **[Migrate the subgraph repositories](#per-subgraph-migration).** Update each subgraph so its pipeline publishes both archive formats side by side. Subgraphs can be migrated independently and in parallel. This stage does not require migrating the subgraph to Hot Chocolate v16.
2. **[Upgrade and cut over the gateway](#upgrade-the-gateway).** Once every subgraph publishes both archives, upgrade the gateway to the v16 packages and switch it to consume the `.far`. Until the subgraphs stop publishing the `.fgp` format, this is reversible.
3. **[Clean up](#cleanup).** Remove the v1 compose and publishing step from each subgraph so it only publishes the `.far`, and optionally [migrate the subgraphs to Hot Chocolate v16](#migrate-subgraph-to-v16).

## Per subgraph migration

### Migrate subgraph-config.json

For each subgraph in your repository, the existing `subgraph-config.json` file needs to be migrated to the new `schema-settings.json` format.

You can run the following command in the root of your repository and it will find all `subgraph-config.json` files and automatically convert them into `schema-settings.json` files:

Bash

```
dnx ChilliCream.Nitro.CommandLine fusion migrate subgraph-config
```

Note

If you can't use .NET 10 / `dnx` you can also install `ChilliCream.Nitro.CommandLine` via `dotnet tool install` and then invoke it via `dotnet nitro ...`.

If you need to do this conversion manually: Create a `schema-settings.json` file next to each `subgraph-config.json` with the following changes:

Diff

```
 {
-  "subgraph": "products",
-  "http": {
-    "baseAddress": "http://products/graphql"
-  }
+  "version": "1.0.0",
+  "name": "products",
+  "transports": {
+    "http": {
+      "url": "http://products/graphql"
+    }
+  }
 }
```

Note

By default the Fusion v2 composition assumes your subgraph is compatible with the latest features. By adding `"version": "1.0.0"` we tell the composition that this is a legacy (Fusion v1) subgraph, which relaxes certain validations like `@shareable` and re-creates inferences that were present in Fusion v1, like fields ending in `ById` being inferred as `@lookup`.

If your subgraph is using a version older than the latest HotChocolate v15 or your subgraph uses an entirely different technology, you also need to disable variable batching in `schema-settings.json`.

Diff

```
 {
   "version": "1.0.0",
   "name": "products",
   "transports": {
     "http": {
-      "url": "http://products/graphql"
+      "url": "http://products/graphql",
+      "capabilities": {
+        "batching": {
+          "variableBatching": false
+        }
+      }
     }
   }
 }
```

#### Environment-specific configuration

Fusion v1 let you set environment-specific values from the pipeline with `fusion subgraph config set`:

Bash

```
dotnet fusion subgraph config set http \
    --url https://dev.example.com/graphql \
    -c subgraph.fsp
```

Fusion v2 moves these values into `schema-settings.json`. Replace anything that varies between environments with a `{{PLACEHOLDER}}` token, then list the per-environment values under a top-level `environments` section:

Diff

```
{
  "version": "1.0.0",
  "name": "products",
  "transports": {
    "http": {
-      "url": "https://example.com/graphql"
+      "url": "{{URL}}"
    }
-  }
+  },
+  "environments": {
+    "dev": {
+      "URL": "https://dev.example.com/graphql"
+    },
+    "prod": {
+      "URL": "https://prod.example.com/graphql"
+    }
+  }
}
```

Composition resolves the placeholders against a chosen environment. Pass `--environment <environment>` to `nitro fusion compose` to select one explicitly, or rely on `nitro fusion publish`, which derives the environment from its `--stage` value. When publishing through Nitro, the keys under `environments` therefore need to match the stage names defined in Nitro.

### Update subgraph

#### Batch resolvers

The concept of batch resolvers like `productByIds(ids: [ID!]!)` no longer exists in Fusion v2\. Batching is done on the transport level through [variable and request batching](https://github.com/graphql/graphql-over-http/blob/fb404ac12dde473f3d9f5a1b1026574c7475e1e4/spec/Appendix%20B%20--%20Variable%20Batching.md). This means singular fields like `Query.productById(id: ID!): Product` are invoked with a list of IDs instead of a plural `Query.productsById(ids: [ID!]!): [Product!]` field. Check out [this GitHub issue](https://github.com/graphql/composite-schemas-spec/issues/25#issue-2173900758) for details on this decision.

Since you don't want multiple invocations of the `Query.productById` field during a single request to hit the database multiple times, you need to ensure your `Query` root fields and `[NodeResolver]` implementations (powering the `Query.node(id: ID!): Node` field) are using [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/batching/dataloader). This is a best practice and ensures the performance of your server does not degrade in comparison to the previous batching fields.

If an entity currently only has batch `Query` root fields in your subgraph, you'll also have to add a singular field:

Diff

```
 type Query {
   productsById(ids: [ID!]!): [Product!] @lookup @internal
+  productById(id: ID!): Product @lookup @internal
 }
```

#### Transport batching

Variable and request batching aren't enabled by default, so you also need to update your `Program.cs` to enable it:

Diff

```
- app.MapGraphQL();
+ app.MapGraphQL().WithOptions(new GraphQLServerOptions { EnableBatching = true });
```

#### GraphQL errors

The v16 gateway only forwards subgraph errors when the associated field is missing or `null` ([spec](https://spec.graphql.org/draft/#sec-Errors.Execution-Errors)). Reporting an error via `IResolverContext.ReportError()` while also returning a non-null value no longer surfaces the error at the gateway.

Throw a `GraphQLException` instead, so the field resolves to `null` and the error is forwarded.

---

If you want to, you can also now [migrate the subgraph to Hot Chocolate v16](#migrate-subgraph-to-v16), but it's not required at this point.

### Migrate pipelines

Migrate one subgraph repository at a time. Throughout this stage your gateway stays on v15 and keeps serving traffic; you [cut it over to v16](#upgrade-the-gateway) only after every subgraph publishes a `.far` archive.

Note

In Fusion v15 a subgraph pipeline composes a Fusion gateway package (`.fgp`) and publishes it to Nitro as the latest archive. In Fusion v16 the equivalent artifact is the Fusion archive (`.far`).

The change to each subgraph pipeline is small: keep the v15 compose step that produces the `.fgp`, but instead of publishing the `.fgp` directly, publish a `.far` with that `.fgp` embedded via the legacy archive option. This keeps the `.fgp` fresh for the running v15 gateway and makes the `.far` available for the v16 cut-over.

Two pipelines in a typical subgraph repository need this change:

- the **deployment pipeline** that publishes the subgraph's archive to Nitro, and
- the **PR validation pipeline** that checks the composed schema for breaking changes.

In both, the existing v15 download and compose steps stay in place; only the final Nitro command changes.

#### Deployment pipeline

In practice this means three changes to your existing deployment pipeline:

1. **Add** a step in the build job that uploads the source schema to Nitro, so the v16 publish can reference it.
2. **Keep** the v15 compose step in the deploy job. It is still responsible for producing an up-to-date `.fgp`.
3. **Replace** the final `dotnet nitro fusion-configuration publish commit` with `dotnet nitro fusion publish` and pass the freshly composed `.fgp` via `--legacy-v1-archive`.

Below is the existing v15 pipeline for reference:

Bash

```
# BUILD JOB
dotnet run --project ./src/SubgraphA -- schema export --output schema.graphql
dotnet fusion subgraph pack -w ./src/SubgraphA

# DEPLOY JOB
dotnet fusion subgraph config set http \
  --url <subgraph-url> \
  -c ${{ github.workspace }}/subgraph/subgraph-a.fsp
dotnet nitro fusion-configuration publish begin \
  --tag <tag> \
  --api-id <api-id> \
  --subgraph-name subgraph-a \
  --stage <stage> \
  --api-key <api-key>
dotnet nitro fusion-configuration publish start \
  --api-key <api-key>
dotnet nitro fusion-configuration download \
  --api-id <api-id> \
  --stage <stage> \
  --output-file ./gateway.fgp \
  --api-key <api-key>
dotnet fusion compose \
  -p ./gateway.fgp \
  --enable-nodes \
  -s ${{ github.workspace }}/subgraph
dotnet nitro fusion-configuration publish commit \
  --configuration ./gateway.fgp \
  --api-key <api-key>
```

##### Upload the source schema in the build job

Add a step to the build job that uploads the exported source schema to Nitro. The `tag` is later used by the publish step to find the matching upload.

YAML

```
- uses: ChilliCream/nitro-fusion-upload@v16
  with:
    tag: <tag>
    api-id: <api-id>
    api-key: <api-key>
    source-schema-files: |
      ./src/SubgraphA/schema.graphql
```

Note

The `dotnet fusion subgraph pack` step is still required while the v15 compose step runs in the deploy job, since v15 composition consumes the `.fsp` archive. It can be removed once the subgraph is migrated to v16 and the v15 compose step is dropped (see [Cleanup](#cleanup)).

##### Replace `publish commit` with `nitro fusion publish` in the deploy job

In the deploy job, leave the existing v15 commands that download the latest `.fgp` and run v15 composition untouched. Only the trailing `dotnet nitro fusion-configuration publish commit` is removed:

Diff

```
- dotnet nitro fusion-configuration publish commit \
-   --configuration ./gateway.fgp \
-   --api-key <api-key>
```

Replace it with `dotnet nitro fusion publish`, passing the freshly composed `gateway.fgp` via `--legacy-v1-archive`. This composes a new `.far`, embeds the `.fgp` inside it, and uploads the result as the latest archive.

YAML

```
- uses: ChilliCream/nitro-fusion-publish@v16
  with:
    tag: <tag>
    stage: <stage>
    api-id: <api-id>
    api-key: <api-key>
    legacy-v1-archive: ./gateway.fgp
    source-schemas: |
      subgraph-a
```

Note

`dotnet nitro fusion publish` should run **after** the subgraph application has been deployed. Once it succeeds, the new archive becomes the latest in Nitro and the gateway will start routing traffic against the new schema, so the subgraph must already be reachable at that URL.

Note

The legacy archive is only required during the transition. Once every subgraph has been migrated to v16 and the gateway has been cut over to consume `.far` directly, the v15 compose step and the legacy archive option can be removed (see [Cleanup](#cleanup)).

#### PR validation pipeline

In addition to the deployment pipeline, most subgraph repositories have a PR validation pipeline that downloads the latest archive, runs composition with the proposed change, and verifies that the composed schema introduces no breaking changes. Below are the relevant v15 steps for reference:

Bash

```
dotnet run --project ./src/SubgraphA -- schema export --output schema.graphql
dotnet fusion subgraph pack -w ./src/SubgraphA
dotnet nitro fusion-configuration download \
  --api-id <api-id> \
  --stage <stage> \
  --output-file ./gateway.fgp \
  --api-key <api-key>
dotnet fusion compose \
  --package-file ./gateway.fgp \
  --enable-nodes \
  --subgraph-package-file ./src/SubgraphA/subgraph-a.fsp
dotnet nitro fusion-configuration validate \
  --stage <stage> \
  --api-id <api-id> \
  --configuration ./gateway.fgp \
  --api-key <api-key>
```

As with the deployment pipeline, the v15 download and compose steps stay in place during the transition so the v15 composition path keeps being validated. Only the final `dotnet nitro fusion-configuration validate` is replaced by `dotnet nitro fusion validate`. Pass the freshly composed `gateway.fgp` via `--legacy-v1-archive` so the validation also covers the embedded v15 archive:

Diff

```
- dotnet nitro fusion-configuration validate \
-   --stage <stage> \
-   --api-id <api-id> \
-   --configuration ./gateway.fgp \
-   --api-key <api-key>
```

YAML

```
- uses: ChilliCream/nitro-fusion-validate@v16
  with:
    stage: <stage>
    api-id: <api-id>
    api-key: <api-key>
    legacy-v1-archive: ./gateway.fgp
    source-schema-files: |
      ./src/SubgraphA/schema.graphql
```

### Migrate subgraph to v16

Once a subgraph is publishing a `.far`, you can migrate the subgraph project itself to Hot Chocolate v16 at any time. This is optional and independent of the gateway cut-over, but it lets you drop the legacy compatibility mode and use the full Fusion v2 feature set.

Start by working through the [Hot Chocolate 15 to 16 migration guide](https://chillicream.com/docs/hotchocolate/migrating/migrate-from-15-to-16) for the subgraph project. Once that is done, apply the steps below.

#### Apply the source schema defaults

Call `AddSourceSchemaDefaults()` when configuring the GraphQL server:

Diff

```
builder.Services
    .AddGraphQLServer()
+   .AddSourceSchemaDefaults()
    // ...
```

This registers the schema as a Fusion source schema, which:

- enables variable and request batching automatically, so you can remove the manual batching change from the [Update subgraph](#update-subgraph) step,
- applies `@shareable` to framework-provided types such as Relay connections, the `PageInfo` type, and the `Query.node` field,
- applies `@serializeAs` to the built-in scalars.

#### Remove the legacy compatibility mode

Remove `"version": "1.0.0"` from `schema-settings.json`:

Diff

```
{
-  "version": "1.0.0",
   "name": "products",
   // ...
}
```

Without `"version": "1.0.0"` the composition treats the subgraph as a Fusion v2 subgraph: the full validations are enforced and the Fusion v1 inferences (such as fields ending in `ById` being treated as `@lookup`) are no longer applied. As a result, a few things that used to be inferred now have to be explicit.

Annotate `By<Field>` lookup fields like `productById(id: ID!): Product` with `[Lookup]` (`@lookup`):

Diff

```
[QueryType]
public static class Query
{
+    [Lookup]
     public static Product GetProductById(int id)
         => // ...
}
```

Missing `@lookup` annotations will usually manifest as `UNSATISFIABLE_QUERY_PATH` errors in the composition. See [Entities and lookups](https://chillicream.com/docs/fusion/entities-and-lookups) for details.

If your graph has overlapping fields, i.e. multiple subgraphs providing the same field, you now also have to explicitly mark those fields as shareable from both sides.

Diff

```
[ObjectType]
public class Product
{
+    [Shareable]
     public string Name { get; set; }
}
```

Missing `@shareable` annotations will usually manifest as `INVALID_FIELD_SHARING` errors in the composition. See [Field ownership and sharing](https://chillicream.com/docs/fusion/field-ownership-and-sharing) for details.

See [Composition](https://chillicream.com/docs/fusion/composition) for guidance on composition behavior and an explanation of any other errors you might encounter, including what they mean and how to fix them.

## Upgrade the gateway

Start by installing the latest `16.x.x` version of **all** of the `HotChocolate.Fusion.*` packages referenced by your project. The gateway runtime now ships in a single ASP.NET Core meta-package, `HotChocolate.Fusion.AspNetCore`, which includes the execution engine, the type system, and the ASP.NET Core integration. This means you can replace your existing references to `HotChocolate.AspNetCore` and `HotChocolate.Fusion` with a single reference to `HotChocolate.Fusion.AspNetCore`:

Diff

```
-<PackageReference Include="HotChocolate.AspNetCore" Version="15.x.x" />
-<PackageReference Include="HotChocolate.Fusion" Version="15.x.x" />
+<PackageReference Include="HotChocolate.Fusion.AspNetCore" Version="16.x.x" />
```

### Breaking changes

Things that have been removed or had a change in behavior that may cause your code not to compile or lead to unexpected behavior at runtime if not addressed.

#### AddFusionGatewayServer renamed to AddGraphQLGatewayServer

The entry point that adds a Fusion gateway to the service collection has been renamed and now lives in the `Microsoft.Extensions.DependencyInjection` namespace.

Diff

```
-builder.Services.AddFusionGatewayServer();
+builder.Services.AddGraphQLGatewayServer();
```

The builder type returned by `AddGraphQLGatewayServer` is now `IFusionGatewayBuilder` instead of the concrete `FusionGatewayBuilder`. All of the configuration extension methods now hang off this interface.

#### CoreBuilder is gone — methods now hang off IFusionGatewayBuilder directly

In v15, the Fusion gateway builder exposed a `CoreBuilder` property of type `IRequestExecutorBuilder` that you used to reach Hot Chocolate's core configuration APIs (validation rules, error filters, etc.).

In v16 there is no separate underlying request executor builder. The Fusion gateway is configured exclusively via `IFusionGatewayBuilder`, and all relevant Hot Chocolate APIs (such as `DisableIntrospection`, `AddErrorFilter`, `AddSha256DocumentHashProvider`, etc.) are exposed directly on `IFusionGatewayBuilder` as Fusion-specific extension methods.

Diff

```
-gatewayBuilder.CoreBuilder.DisableIntrospection();
+gatewayBuilder.DisableIntrospection();
```

#### ModifyFusionOptions split into ModifyOptions, ModifyRequestOptions and ModifyPlannerOptions

`FusionOptions` and `RequestExecutorOptions` no longer exist as Fusion configuration surfaces. The settings have been split across three dedicated option types and three matching `Modify*` methods on `IFusionGatewayBuilder`:

| v15 surface          | v16 surface                                    |
| -------------------- | ---------------------------------------------- |
| ModifyFusionOptions  | ModifyOptions (cache sizes, error handling, …) |
| ModifyRequestOptions | ModifyRequestOptions (per-request settings)    |
| —                    | ModifyPlannerOptions (planner guardrails)      |

Diff

```
-gatewayBuilder
-    .ModifyFusionOptions(o =>
-    {
-        o.AllowQueryPlan = true;
-        o.IncludeDebugInfo = true;
-    })
-    .ModifyRequestOptions(o =>
-    {
-        o.ExecutionTimeout = TimeSpan.FromSeconds(30);
-        o.PersistedOperations.OnlyAllowPersistedDocuments = false;
-        o.IncludeExceptionDetails = true;
-    });
+gatewayBuilder
+    .ModifyOptions(o =>
+    {
+        o.OperationDocumentCacheSize = 200;
+        o.OperationExecutionPlanCacheSize = 100;
+    })
+    .ModifyRequestOptions(o =>
+    {
+        o.ExecutionTimeout = TimeSpan.FromSeconds(30);
+        o.PersistedOperations.OnlyAllowPersistedDocuments = false;
+        o.IncludeExceptionDetails = true;
+        o.AllowOperationPlanRequests = true;
+    });
```

#### Internal directives hidden from schema endpoint

Previously, the `/graphql/schema.graphql` endpoint was returning the schema containing internal directives like `@authorize`. Starting with v16 the endpoint no longer includes internal directives by default.

If you need to retain the previous behavior, set `DisableInternalDirectives` to `true` through `ModifyOptions`. This treats every directive as public, even directives that explicitly call `Internal()` and regardless of `DefaultDirectiveVisibility`:

C#

```
builder.Services
    .AddGraphQLServer()
    .ModifyOptions(o => o.DisableInternalDirectives = true);
```

Be aware that internal directives may carry sensitive information (for example, authorization policies attached via `@authorize`). Only enable this if you understand and accept that risk.

#### Cache configuration

In v15, the operation cache acted as the cache for operation plans. v16 introduces a dedicated operation plan cache. Both document and operation plan cache are now configured on the gateway builder via `ModifyOptions` instead of as global services on the `IServiceCollection`:

Diff

```
-builder.Services.AddDocumentCache(capacity: 200);
-builder.Services.AddOperationCache(capacity: 100);

builder.Services
    .AddGraphQLGatewayServer()
+    .ModifyOptions(o =>
+    {
+        o.OperationDocumentCacheSize = 200;
+        o.OperationExecutionPlanCacheSize = 100;
+    });
```

If your application contains multiple Fusion gateways, the cache configuration has to be repeated for each one as the configuration is now scoped to a particular gateway.

#### Document hash provider configuration

Document hash providers are no longer registered through the `IServiceCollection`. Move the call to `IFusionGatewayBuilder` instead:

Diff

```
-builder.Services.AddSha256DocumentHashProvider();

builder.Services
    .AddGraphQLGatewayServer()
+    .AddSha256DocumentHashProvider();
```

The same applies to `AddMD5DocumentHashProvider` and `AddSha1DocumentHashProvider`.

#### Eager initialization by default

Previously, the Fusion gateway constructed the schema and the request executor on the first request. To get eager initialization, you had to opt in via `InitializeOnStartup` on the underlying `CoreBuilder`.

In v16, eager initialization is the default. The schema and the request executor are constructed during application startup, before Kestrel begins accepting traffic. Schema errors surface immediately when you start the gateway, rather than only when the first request arrives.

If you used `InitializeOnStartup`, remove it. If you also passed a warmup delegate, migrate it to `AddWarmupTask`:

Diff

```
-gatewayBuilder.CoreBuilder
-    .InitializeOnStartup(warmup: (executor, ct) => /* ... */);
+gatewayBuilder.AddWarmupTask((executor, ct) => /* ... */);
```

If you really need lazy initialization, opt out via `ModifyOptions`:

C#

```
gatewayBuilder.ModifyOptions(o => o.LazyInitialization = true);
```

#### Server options now configured via ModifyServerOptions

`GraphQLServerOptions` (GET requests, multipart uploads, batching, schema requests, the embedded Nitro tool, etc.) are now configured at the schema level using `ModifyServerOptions` on `IFusionGatewayBuilder` instead of per-endpoint:

Diff

```
-app.MapGraphQL().WithOptions(new GraphQLServerOptions
-{
-    EnableBatching = true,
-    Tool = { Enable = false }
-});
+gatewayBuilder.ModifyServerOptions(o =>
+{
+    o.Batching = AllowedBatching.All;
+    o.Tool.Enable = false;
+});
+
+app.MapGraphQL();
```

Per-endpoint overrides are still supported but now use a delegate pattern instead of an object initializer:

C#

```
app.MapGraphQL().WithOptions(o => o.EnableGetRequests = false);
```

#### Batching is now disabled by default

In v15, request batching was enabled by default (`EnableBatching = true`). In v16, batching is **disabled by default** as a security measure. The `EnableBatching` property has been replaced by `Batching`, which uses the `AllowedBatching` flags enum for fine-grained control:

Diff

```
-options.EnableBatching = true;
+options.Batching = AllowedBatching.All;
```

If you were relying on the previous default, you need to explicitly enable batching:

C#

```
gatewayBuilder.ModifyServerOptions(o => o.Batching = AllowedBatching.All);
```

A new `MaxBatchSize` property limits the number of operations in a single batch. The default is **1024**. Set it to `0` for unlimited.

#### Configuration provider API

The configuration provider abstractions used to load and watch the Fusion gateway configuration document have been redesigned around `IFusionConfigurationProvider`.

##### IObservable<GatewayConfiguration> replaced by IFusionConfigurationProvider

The old `IObservable<GatewayConfiguration>` source has been replaced by the new `IFusionConfigurationProvider` interface, which combines `IObservable<FusionConfiguration>` with `IAsyncDisposable` and exposes the latest configuration via a `Configuration` property. The configuration payload type is now `FusionConfiguration` (a `DocumentNode` plus a `JsonDocumentOwner` for schema settings) instead of `GatewayConfiguration`.

Diff

```
-public class CustomConfigurationProvider : IObservable<GatewayConfiguration>
-{
-    public IDisposable Subscribe(IObserver<GatewayConfiguration> observer) => /* ... */;
-}
+public class CustomConfigurationProvider : IFusionConfigurationProvider
+{
+    public FusionConfiguration? Configuration => /* latest snapshot */;
+
+    public IDisposable Subscribe(IObserver<FusionConfiguration> observer) => /* ... */;
+
+    public ValueTask DisposeAsync() => ValueTask.CompletedTask;
+}
```

##### RegisterGatewayConfiguration → AddConfigurationProvider

Diff

```
-gatewayBuilder.RegisterGatewayConfiguration(sp => new CustomConfigurationProvider());
+gatewayBuilder.AddConfigurationProvider(sp => new CustomConfigurationProvider());
```

##### ConfigureFromFile → AddFileSystemConfiguration

Diff

```
-gatewayBuilder.ConfigureFromFile("gateway.fgp");
+gatewayBuilder.AddFileSystemConfiguration("gateway.fgp");
```

The `watchFileForUpdates` parameter is gone — file watching is the default behavior of the file-system configuration provider.

##### ConfigureFromDocument → AddInMemoryConfiguration

Diff

```
-gatewayBuilder.ConfigureFromDocument(documentNode);
+gatewayBuilder.AddInMemoryConfiguration(documentNode);
```

`AddInMemoryConfiguration` also accepts an optional `JsonDocumentOwner` for the schema settings.

#### Nitro integration

The packages have been restructured in v16\. Versions now align with the rest of the platform, so you are migrating from `1.x` to `16.x`. For the full migration guide covering all package renames, API changes, and complete before/after examples, see [Migrating Nitro from 1 to 16](https://chillicream.com/docs/nitro/migration/migrate-from-1-to-16).

The key changes for Fusion projects:

- **Package rename:** `ChilliCream.Nitro.Core` becomes `ChilliCream.Nitro.GraphQL`, `ChilliCream.Nitro.Telemetry` becomes `ChilliCream.Nitro.OpenTelemetry`, and Azure packages are consolidated into `ChilliCream.Nitro.Azure`.
- **`ConfigureFromCloud()`** is replaced by `AddNitro().AddDefaults()` on the service collection.
- **Per-gateway feature options** are configured via `ModifyNitroOptions()` on the gateway builder.
- **`AddNitroExporter()`** is replaced by `AddOpenTelemetry()` on the `INitroBuilder`.
- **Asset cache** is now configured globally on `INitroBuilder` instead of per-gateway.
- **`AddDefaults()`** is a source-generated method that wires up the default integration when the correct packages are referenced.

Note

If you are self-hosting the Nitro backend, make sure to update it to the latest version as well. `10.1.0` is the minimum version required to work with the `ChilliCream.Nitro.*` packages.

**Before**

C#

```
builder.Services
    .AddFusionGatewayServer()
    .ConfigureFromCloud(o =>
    {
        o.ApiId = "...";
        o.ApiKey = "...";
        o.Stage = "...";
    });
```

**After**

C#

```
builder.Services
    .AddNitro(o =>
    {
        o.ApiId = "...";
        o.ApiKey = "...";
        o.Stage = "...";
    })
    .AddDefaults();

builder.Services.AddGraphQLGatewayServer();
```

#### Diagnostic listener API redesigned

Fusion diagnostics were redesigned in v16\. The high-level `ExecuteFederatedQuery`, `ResolveError`, `ResolveByKeyBatchError`, `QueryPlanExecutionError`, and `SubgraphRequestError` hooks are gone. The new API is execution-stage specific.

| Before                                                                  | After                                                                                         |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| HotChocolate.Fusion.Execution.Diagnostic.IFusionDiagnosticEventListener | HotChocolate.Fusion.Diagnostics.IFusionExecutionDiagnosticEventListener                       |
| FusionDiagnosticEventListener (base class)                              | FusionExecutionDiagnosticEventListener (base class)                                           |
| ExecuteFederatedQuery(IRequestContext)                                  | ExecuteRequest(RequestContext)                                                                |
| QueryPlanExecutionError(Exception)                                      | PlanOperationError(RequestContext, string operationId, Exception)                             |
| ResolveError(Exception) / ResolveByKeyBatchError(Exception)             | ExecutionNodeError(OperationPlanContext, ExecutionNode, Exception)                            |
| SubgraphRequestError(string subgraphName, Exception)                    | SourceSchemaTransportError(OperationPlanContext, ExecutionNode, string schemaName, Exception) |

Diff

```
-using HotChocolate.Fusion.Execution.Diagnostic;
+using HotChocolate.Fusion.Diagnostics;
+using HotChocolate.Fusion.Execution;
+using HotChocolate.Fusion.Execution.Nodes;

-public class DiagnosticEventListener : FusionDiagnosticEventListener
+public class DiagnosticEventListener : FusionExecutionDiagnosticEventListener
 {
-    public override IDisposable ExecuteFederatedQuery(IRequestContext context)
-        => base.ExecuteFederatedQuery(context);
+    public override IDisposable ExecuteRequest(RequestContext context)
+        => base.ExecuteRequest(context);

-    public override void QueryPlanExecutionError(Exception exception)
-        => base.QueryPlanExecutionError(exception);
+    public override void PlanOperationError(RequestContext context, string operationId, Exception error)
+        => base.PlanOperationError(context, operationId, error);

-    public override void SubgraphRequestError(string subgraphName, Exception exception)
-        => base.SubgraphRequestError(subgraphName, exception);
+    public override void SourceSchemaTransportError(OperationPlanContext context, ExecutionNode node, string schemaName, Exception error)
+        => base.SourceSchemaTransportError(context, node, schemaName, error);
 }
```

The new interface also exposes additional execution-stage hooks (`AddedOperationPlanToCache`, `SourceSchemaStoreError`, `SubscriptionEventError`).

##### Scoped duration and error hooks

In v15, the only scoped (`IDisposable`\-returning) hook on `IFusionDiagnosticEventListener` was `ExecuteFederatedQuery`, which wrapped the entire federated request. v16 broadens the scope significantly: each major execution stage and each individual execution node has its own `IDisposable`\-returning hook on `IFusionExecutionDiagnosticEventListener`, so you can measure the duration of, for example, planning an operation or a single subgraph fetch in isolation. The error hooks have likewise been redesigned around the new node-based execution model.

| v15                                                  | v16                                                                                                   |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| ExecuteFederatedQuery(IRequestContext)               | ExecuteRequest(RequestContext)                                                                        |
| —                                                    | PlanOperation(RequestContext, string operationPlanId)                                                 |
| —                                                    | ExecuteOperation(RequestContext)                                                                      |
| —                                                    | ExecuteOperationNode(OperationPlanContext, OperationExecutionNode, string schemaName)                 |
| —                                                    | ExecuteOperationBatchNode(OperationPlanContext, OperationBatchExecutionNode, string schemaName)       |
| —                                                    | ExecuteNodeFieldNode(OperationPlanContext, NodeFieldExecutionNode)                                    |
| —                                                    | ExecuteIntrospectionNode(OperationPlanContext, IntrospectionExecutionNode)                            |
| —                                                    | ExecuteSubscription(RequestContext, ulong subscriptionId)                                             |
| —                                                    | ExecuteSubscriptionNode(OperationPlanContext, ExecutionNode, string schemaName, ulong subscriptionId) |
| —                                                    | OnSubscriptionEvent(OperationPlanContext, ExecutionNode, string schemaName, ulong subscriptionId)     |
| QueryPlanExecutionError(Exception)                   | PlanOperationError(RequestContext, string operationId, Exception)                                     |
| ResolveError(Exception)                              | ExecutionNodeError(OperationPlanContext, ExecutionNode, Exception)                                    |
| ResolveByKeyBatchError(Exception)                    | ExecutionNodeError(OperationPlanContext, ExecutionNode, Exception)                                    |
| SubgraphRequestError(string subgraphName, Exception) | SourceSchemaTransportError(OperationPlanContext, ExecutionNode, string schemaName, Exception)         |

To time individual stages of the request pipeline (parsing, validation, variable coercion) you previously had to implement Hot Chocolate's core `IExecutionDiagnosticEventListener` separately and register it alongside the Fusion-specific listener. In v16 these stages have been folded into `IFusionExecutionDiagnosticEventListener` itself, so you can remove your `IExecutionDiagnosticEventListener` implementations and move the overrides (for example `ParseDocument`, `ValidateDocument`, `CoerceVariables`) onto your `FusionExecutionDiagnosticEventListener` subclass instead.

The dedicated `SubscriptionTransportError(...)` hook from the v15 Fusion diagnostics API is also no longer exposed separately. Subscription transport failures now flow through `SourceSchemaTransportError(...)` like any other source-schema transport error.

#### IRequestContext

Hot Chocolate has removed the `IRequestContext` abstraction in favor of the concrete `RequestContext` class. This applies to the Fusion diagnostic API as well:

| Before                      | After                                     |
| --------------------------- | ----------------------------------------- |
| context.DocumentId          | context.OperationDocumentInfo.Id.Value    |
| context.Document            | context.OperationDocumentInfo.Document    |
| context.DocumentHash        | context.OperationDocumentInfo.Hash.Value  |
| context.ValidationResult    | context.OperationDocumentInfo.IsValidated |
| context.IsCachedDocument    | context.OperationDocumentInfo.IsCached    |
| context.IsPersistedDocument | context.OperationDocumentInfo.IsPersisted |

If you have a custom request middleware on the Fusion pipeline:

Diff

```
public class CustomRequestMiddleware
{
-   public async ValueTask InvokeAsync(IRequestContext context)
+   public async ValueTask InvokeAsync(RequestContext context)
    {
-       string documentId = context.DocumentId;
+       string documentId = context.OperationDocumentInfo.Id.Value;

        await _next(context).ConfigureAwait(false);
    }
}
```

#### Clearer separation between schema and application services

Hot Chocolate has long maintained a second `IServiceProvider` for schema services, separate from the application service provider where you register your services and configuration. This schema service provider is scoped to a particular schema and contains all of the internal services for the gateway (diagnostic listeners, error filters, HTTP request interceptors, …).

To access application services within schema services like diagnostic event listeners or error filters, the v15 implementation used a combined service provider. In v16, the Fusion gateway uses the schema service provider exclusively — application services must now be explicitly cross-registered to be accessible.

Diff

```
builder.Services.AddSingleton<MyService>();
builder.Services.AddGraphQLGatewayServer()
+   .AddApplicationService<MyService>()

    // either
    .AddDiagnosticEventListener<MyDiagnosticEventListener>();
    // or
    .AddDiagnosticEventListener(sp => new MyDiagnosticEventListener(sp.GetRequiredService<MyService>()));

public class MyDiagnosticEventListener(MyService service) : FusionExecutionDiagnosticEventListener;
```

Sometimes the registration of required services is not as obvious. For example, the types for logging are registered in framework code.

Diff

```
builder.Services.AddLogging();
builder.Services.AddGraphQLGatewayServer()
+   .AddApplicationService<ILogger<MyLoggingDiagnosticEventListener>>()

    // either
    .AddDiagnosticEventListener<MyLoggingDiagnosticEventListener>();
    // or
    .AddDiagnosticEventListener(sp => new MyLoggingDiagnosticEventListener(sp.GetRequiredService<ILogger<MyLoggingDiagnosticEventListener>>()));

public class MyLoggingDiagnosticEventListener(ILogger<MyLoggingDiagnosticEventListener> logger) : FusionExecutionDiagnosticEventListener;
```

If you're using any of the following Fusion configuration APIs, ensure that the application services required for their activation are registered via `AddApplicationService<T>()`:

- `AddHttpRequestInterceptor`
- `AddErrorFilter`
- `AddDiagnosticEventListener`
- `AddOperationPlannerInterceptor`

If you need to access the application service provider from within the schema service provider, use:

C#

```
IServiceProvider applicationServices = schemaServices.GetRootServiceProvider();
```

#### Experimental @semanticNonNull support removed

Hot Chocolate v15 included experimental support for the `@semanticNonNull` directive, which let you mark fields as semantically non-null while still returning `null` (rather than propagating to the parent) when a resolver errored. This feature has been removed in v16 in favor of the [onError proposal](https://github.com/graphql/graphql-spec/pull/1163).

If you previously opted in to this feature on the Fusion gateway, remove the option:

Diff

```
gatewayBuilder
    .ModifyOptions(o =>
    {
-       o.EnableSemanticNonNull = true;
    });
```

If you still need to keep the behavior of not propagating nulls for errors on non-null fields, set the `DefaultErrorHandlingMode` to `ErrorHandlingMode.Null`:

C#

```
gatewayBuilder.ModifyRequestOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Null);
```

##### Clients that still need a schema with @semanticNonNull annotations

If you have a client that still relies on the schema being annotated with `@semanticNonNull`, you have a few options to obtain such a schema.

**Schema snapshot tests**

If you produce a schema string for snapshot tests via `ISchemaDefinition.ToString()`, switch to `SchemaFormatter` with `RewriteToSemanticNonNull` enabled:

C#

```
string schemaStr = SchemaFormatter.FormatAsString(
    schema,
    new SchemaFormatterOptions { RewriteToSemanticNonNull = true });
```

**Downloading the schema from the gateway**

If you're using `MapGraphQLSchema()` to expose the gateway schema at `/graphql/schema`, you can additionally call `MapGraphQLSemanticNonNullSchema()` to expose a variant annotated with `@semanticNonNull` at `/graphql/semantic-non-null-schema.graphql`:

C#

```
app.MapGraphQLSchema();
app.MapGraphQLSemanticNonNullSchema();
```

### Behavioral breaking changes

#### Subgraph errors are only forwarded if the associated field is not initialized

The v15 gateway forwarded every subgraph error. The v16 gateway only forwards errors whose associated field is missing or `null`, matching the [GraphQL spec](https://spec.graphql.org/draft/#sec-Errors.Execution-Errors). Errors attached to an initialized (non-null) field are now dropped.

Update affected subgraphs as described in [GraphQL errors](#graphql-errors).

### Noteworthy changes

#### Concurrent execution gate

Hot Chocolate v16 introduces a concurrency gate that limits how many GraphQL operations execute at the same time. The gate sits in the request pipeline just before operation execution and applies uniformly to queries, mutations, subscription handshakes, and each subscription event.

For the Fusion gateway, configure the limit through `ModifyServerOptions`:

C#

```
gatewayBuilder.ModifyServerOptions(o => o.MaxConcurrentExecutions = 128);
```

The default is **64**. Operations that arrive while the gate is full queue up and run as slots free. Set the limit to `null` to disable the gate entirely.

Every execution is bounded by the `ExecutionTimeout` option (default 30 seconds). The budget covers both the time an execution spends waiting for a concurrency slot and the time it spends running.

#### Parser limits

The parser now enforces a maximum recursion depth of **200** by default, a maximum of **4** directives per location, and a fragment visit budget of **1,000** per operation. These limits also apply to documents handled by the Fusion gateway. If your operations legitimately exceed these limits, raise them via `ModifyParserOptions` / `ModifyValidationOptions`:

C#

```
gatewayBuilder
    .ConfigureValidation((_, b) => b.ModifyOptions(o => o.MaxAllowedFragmentVisits = 5_000));
```

#### RunWithGraphQLCommandsAsync returns exit code

`RunWithGraphQLCommandsAsync` and `RunWithGraphQLCommands` now return exit codes (`Task<int>` and `int` respectively). Update your `Program.cs` if you forward these to the host:

Diff

```
-await app.RunWithGraphQLCommandsAsync(args);
+return await app.RunWithGraphQLCommandsAsync(args);
```

## Aspire

The Aspire integration changed in v16\. There is no separate `AddFusionGateway` resource anymore. The gateway and subgraphs are now regular Aspire projects.

First, update the `Aspire.AppHost.Sdk` and the `Aspire.Hosting.AppHost` package to 13.x:

Diff

```
-  <Sdk Name="Aspire.AppHost.Sdk" Version="x.x.x"/>
+  <Sdk Name="Aspire.AppHost.Sdk" Version="13.x.x"/>

   <ItemGroup>
-    <PackageReference Include="Aspire.Hosting.AppHost" Version="x.x.x" />
-    <PackageReference Include="HotChocolate.Fusion.Aspire" Version="15.x.x" />
+    <PackageReference Include="Aspire.Hosting.AppHost" Version="13.x.x" />
+    <PackageReference Include="HotChocolate.Fusion.Aspire" Version="16.x.x" />
   </ItemGroup>
```

Then update the AppHost setup. Add the GraphQL orchestrator, tell Aspire where to get each subgraph schema, and reference those subgraphs from the gateway:

Diff

```
-var products = builder.AddProject<Projects.Products>("products");
-var reviews = builder.AddProject<Projects.Reviews>("reviews");
-var accounts = builder.AddProject<Projects.Accounts>("accounts");
-
-builder
-    .AddFusionGateway<Projects.Gateway>("gateway")
-    .WithSubgraph(products)
-    .WithSubgraph(reviews)
-    .WithSubgraph(accounts);
-
-builder.Build().Compose().Run();
+builder.AddGraphQLOrchestrator();
+
+var products = builder.AddProject<Projects.Products>("products")
+    .WithGraphQLSchemaEndpoint();
+
+var reviews = builder.AddProject<Projects.Reviews>("reviews")
+    .WithGraphQLSchemaEndpoint();
+
+var accounts = builder.AddProject<Projects.Accounts>("accounts")
+    .WithGraphQLSchemaEndpoint();
+
+builder
+    .AddProject<Projects.Gateway>("gateway")
+    .WithGraphQLSchemaComposition()
+    .WithReference(products)
+    .WithReference(reviews)
+    .WithReference(accounts);
+
+builder.Build().Run();
```

`builder.AddGraphQLOrchestrator()` installs the startup hook that runs schema discovery and composition. You no longer call `.Compose()`. `builder.Build().Run()` is enough.

`WithGraphQLSchemaEndpoint()` downloads the subgraph schema from `/graphql/schema.graphql` at startup. If your schema endpoint uses a different path, pass it explicitly:

C#

```
builder
    .AddProject<Projects.Products>("products")
    .WithGraphQLSchemaEndpoint(path: "/schema.graphql");
```

If you keep schema files on disk, use `WithGraphQLSchemaFile()` instead. It looks for `schema.graphqls` in the subgraph project directory by default:

C#

```
builder
    .AddProject<Projects.Products>("products")
    .WithGraphQLSchemaFile(fileName: "./dir/schema.graphqls");
```

To create that file automatically when a subgraph starts, call `ExportSchemaOnStartup()` on the subgraph's `IRequestExecutorBuilder`.

Pass composition options to `WithGraphQLSchemaComposition`:

C#

```
builder
    .AddProject<Projects.Gateway>("gateway")
    .WithGraphQLSchemaComposition(
        settings: new GraphQLCompositionSettings
        {
            EnableGlobalObjectIdentification = true
        });
```

Each subgraph also needs an `Aspire` environment in `schema-settings.json`. This is the local GraphQL endpoint the composed gateway configuration uses when it runs under Aspire:

Diff

```
{
  "name": "my-subgraph",
+  "transports": {
+    "http": {
+      "url": "{{API_URL}}"
+    }
+  },
+  "environments": {
+    "Aspire": {
+      "API_URL": "http://localhost:5000/graphql"
+    }
+  }
}
```

## Cleanup

Once the gateway has been [cut over to v16](#upgrade-the-gateway) and every subgraph publishes a `.far`, the dual-format bridge is no longer needed. In every subgraph pipeline you can now drop all `dotnet fusion` usages (`subgraph pack`, `subgraph config set`, `compose`) and all `dotnet nitro fusion-configuration *` commands, since the equivalent work is now done by `dotnet nitro fusion *` (`upload`, `publish`, `validate`). Also remove the `--legacy-v1-archive` option from those `dotnet nitro fusion publish` / `validate` steps.

Since `dotnet fusion` is no longer used, you can remove any reference to the `HotChocolate.Fusion.CommandLine` package from your pipelines and from `./.config/dotnet-tools.json`.

You can also fully replace `ChilliCream.Nitro.CLI` with [ChilliCream.Nitro.CommandLine](https://chillicream.com/docs/nitro/cli/installation).

Note

Hold off until the v16 gateway has been running in production long enough that a rollback to v15 is off the table. Once the v15 compose step is gone the `.fgp` is no longer refreshed, and a rollback would mean restoring these steps first. Cleanup is independent per subgraph, so there is no need to do all repositories at once.

Finally, if you haven't done so already, each subgraph can now independently [switch to Hot Chocolate v16](#migrate-subgraph-to-v16) at its own pace.

[Edit this page on GitHub](https://github.com/ChilliCream/graphql-platform/edit/main/website/content/docs/fusion/migration/migrate-from-15-to-16.md)

Last updated on **June 30, 2026** by **Tobias Tengler**
