graphql-platform Documentation

Welcome to the home of the Hot Chocolate GraphQL server for .NET, the Strawberry Shake GraphQL client for .NET and Nitro the awesome Monaco based GraphQL IDE.

RAW Doc

Technical Documentation: ChilliCream/graphql-platform

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (README.md)





ChilliCream GraphQL Platform

Welcome to the ChilliCream GraphQL Platform!

We help you, developers and companies, to leverage your APIs to the next level with GraphQL. Strongly-typed schemas that match your APIs 100 percent. Efficient data fetching that reduces overall cost without extra effort. Consumer-friendly, declarative, self documented APIs that support you in your daily work to build powerful UIs effectively.

Most of our products are open-source and right here in this repository.

Everyone is welcome! Always remember to treat anyone respectful, no matter their gender, opinion, religion, or skin-tone. We're one world and together we're stronger!

Join our awesome community on Slack, if you would like to get in touch with us, need help, or just want to learn!

Our Products

Hot Chocolate

Hot Chocolate is the most efficient, feature-rich, open-source GraphQL server in the .NET ecosystem, that helps developers to build powerful GraphQL APIs and Gateways with ease.

Documentation

Nitro

Nitro App is an awesome, performant, feature-rich GraphQL IDE / API Cockpit that helps developers and data scientists to explore, share, and test any GraphQL API.

Nitro App can be installed as a Desktop App, used as a Web App, which can be installed through your browser of choice as well, or used as middleware on your GraphQL endpoint. Middlewares are available in .NET and NodeJS. More middlewares will follow.

Documentation

Strawberry Shake

Strawberry Shake is an incredible, open-source GraphQL client for the .NET ecosystem, that helps developers to build awesome UIs in Blazor, Maui, and more. Other than most .NET GraphQL clients, Strawberry Shake is type-safe GraphQL client that generates .NET types from your GraphQL schema out of the box. Moreover, Strawberry Shake comes with a reactive store like in Relay and Apollo Client, which is nice due to the fact that you can build reactive UIs in .NET with stuff like client-side data caching and data-fetching strategies.

Documentation

Green Donut

Green Donut is a lightweight, yet powerful DataLoader that simplifies batching, caching, and solves the N+1 problem.

Documentation

Official examples

Examples of things built on top of the ChilliCream GraphQL Platform that are open source and can be explored by others.

Contributing

Become a code contributor and help us make the ChilliCream GraphQL platform even better!

From our community

Check out what members of our awesome community have made!

License

The source code in this repository is licensed under the MIT License. The
MIT License covers the source code only. It does not cover the ChilliCream name and
trademarks, the logos and package icons, or the website content and design. See
TRADEMARKS.md for details.

Financial Contributors

Become a financial contributor and help us sustain our community.

Become a sponsor and get your logo on our README on Github with a link to your site.




















Backer

Become a backer and get your image on our README on Github with a link to your site.





















Website Content Docs Fusion Getting Started (`website/content/docs/fusion/getting-started.md`)


title: "Getting Started with GraphQL Federation"

In this tutorial, you will build and run a small Fusion setup locally.

By the end, you will have:

  • A Products subgraph on http://localhost:5001/graphql
  • A Reviews subgraph on http://localhost:5002/graphql
  • A Fusion gateway on http://localhost:5000/graphql
  • One composed API that clients query through the gateway

If you want the broader conceptual background first, read Overview.

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.

A gateway sits in front of all your subgraphs, receives client queries, routes each part of the query to the subgraphs that own the requested fields, and combines the results. From the client's perspective, they are querying a single GraphQL endpoint with one schema. The gateway handles everything transparently.

Instead of building one monolithic GraphQL server that knows about every domain in your system, you let each team own and ship their own GraphQL service. Fusion handles the hard part by composing those services into a single schema that clients query without knowing (or caring) how many services are behind it.

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

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

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

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.

You might assume this is how it works:

"My Users subgraph stores a tenantId on each User. To resolve the full Tenant object, I need my Users subgraph to call the Tenants subgraph and fetch it."

This seems reasonable, but it creates a problem: your subgraphs become coupled. The Users subgraph has to know where the Tenants subgraph lives, how to call it, and how to handle failures. As you add more subgraphs, these cross-service dependencies multiply.

How Fusion actually works:

"My Users subgraph knows that each User has a tenantId, and I declare that user.tenant returns a Tenant identified by that ID. I don't need to know how or where the Tenant gets resolved, because the gateway handles that."

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

This design has practical consequences:

  • Subgraphs stay independent. The Users subgraph does not import anything from the Tenants subgraph. It just knows that Tenant is an entity with an id field.
  • 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

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.

Subgraph: An independent GraphQL service that contributes types and fields to the overall API. Each subgraph runs in its own process, owns its own data, and defines resolvers for the fields it contributes. In Hot Chocolate, a subgraph is a standard Hot Chocolate server with a few additional attributes that tell Fusion how its types fit into the larger API.

Source Schema: The GraphQL schema exposed by a single subgraph. When you export a subgraph's schema (as a .graphqls file), that exported schema is the source schema. Fusion's composition engine reads source schemas from all your subgraphs and merges them into the composite schema.

Composite Schema: The unified, client-facing GraphQL schema produced by merging all source schemas. All types and fields from every subgraph appear in the composite schema, and clients can query across subgraph boundaries in a single request as if they were querying a single monolithic GraphQL server. They never interact with individual subgraphs directly.

Gateway: The service that sits between clients and subgraphs. It exposes the composite schema, receives client queries, analyzes each query, routes parts of it to the appropriate subgraphs, and combines the results into a single response. In Hot Chocolate, you create a gateway by calling AddGraphQLGateway() in a standard ASP.NET Core project.

Query Planning: The process the gateway uses to build an optimized execution plan for a client query across multiple subgraphs. When a query touches fields from different subgraphs, the gateway analyzes the query, determines which subgraphs own which fields, and creates a plan that fetches data in the right order.

Entity: A type with a stable key that can be referenced across subgraphs. Entity keys (such as id or sku) uniquely identify each instance and are used for cross-subgraph references and resolution.

Lookup: A query field the gateway uses to enrich an already-referenced entity with additional fields from another subgraph.

graphql
type Query {
  productById(id: ID!): Product @lookup
}
  • An internal lookup (marked with the @internal directive) is excluded from the composite schema, cannot be called by clients, and is used only by the gateway for cross-subgraph entity resolution.
graphql
type Query {
  productById(id: ID!): Product @internal @lookup
}

Shareable: A contract for fields that are defined in multiple subgraphs. By default, each non-key field must be defined by exactly one subgraph. If the same field appears in multiple subgraphs, composition fails until every definition is explicitly marked with the @sharable directive. This asserts that all definitions have the same semantics, and it allows the query planner to fetch the field from any subgraph subgraph providing this field.

graphql
type User {
  name: String @sharable
}

Prerequisites

To follow this tutorial, you need the following installed on your machine.

.NET SDK

Install the .NET 10 SDK or later. Verify your installation:

shell
dotnet --version

You should see 10.0.100 or higher.

Nitro CLI

The Nitro CLI is a .NET tool that handles schema composition. Install it globally:

shell
dotnet tool install -g ChilliCream.Nitro.CommandLine

Verify the installation:

shell
nitro version

Hot Chocolate Templates

Install the Hot Chocolate templates:

shell
dotnet new install HotChocolate.Templates

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

Create a new GraphQL server project from the template:

shell
mkdir fusion-getting-started
cd fusion-getting-started

dotnet new graphql -n Products

The template creates sample files in Products/Types (Author.cs, Book.cs, Query.cs). Delete those files before continuing so your schema only contains the tutorial types.

Your Products/Products.csproj should now look like this:

xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>

  <ItemGroup Condition="'$(ImplicitUsings)' == 'enable'">
    <Using Include="Products" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="HotChocolate.AspNetCore" />
    <PackageReference Include="HotChocolate.AspNetCore.CommandLine" />
    <PackageReference Include="HotChocolate.Types.Analyzers">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

</Project>

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:

json
"launchUrl": "http://localhost:5001/graphql",
"applicationUrl": "http://localhost:5001"

This ensures the subgraph runs on port 5001, which matches what we will configure for composition later.

Define the Product Type

Create a file called Product.cs in the Products project:

csharp
namespace Products;

public class Product
{
    public int Id { get; set; }

    public required string Name { get; set; }

    public double Price { get; set; }
}

This is a plain C# class with nothing GraphQL-specific yet.

Add In-Memory Data

Create a file called ProductRepository.cs:

csharp
namespace Products;

public static class ProductRepository
{
    private static readonly List<Product> Products =
    [
        new Product { Id = 1, Name = "Table", Price = 899.99 },
        new Product { Id = 2, Name = "Couch", Price = 1299.50 },
        new Product { Id = 3, Name = "Chair", Price = 54.00 },
    ];

    public static Product? GetById(int id)
        => Products.FirstOrDefault(p => p.Id == id);

    public static List<Product> GetAll()
        => Products;
}

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 for a more complete example.

Expose Products Through GraphQL

Create a file called ProductQueries.cs. This class defines the Query root fields for the Products subgraph:

csharp
namespace Products;

[QueryType]
public static partial class ProductQueries
{
    [Lookup]
    public static Product? GetProductById(int id)
        => ProductRepository.GetById(id);

    public static List<Product> GetProducts()
        => ProductRepository.GetAll();
}

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

Set Program.cs to:

csharp
var builder = WebApplication.CreateBuilder(args);

builder
    .AddGraphQL("Products")
    .AddTypes();

var app = builder.Build();

app.MapGraphQL();
app.RunWithGraphQLCommands(args);

This is a minimal Hot Chocolate server. #!csharp 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

Run the Products subgraph:

shell
cd Products
dotnet run

Open your browser to http://localhost:5001/graphql/ to access the Nitro GraphQL IDE. Try this query:

graphql
query {
  products {
    id
    name
    price
  }
}

You should see:

json
{
  "data": {
    "products": [
      { "id": 1, "name": "Table", "price": 899.99 },
      { "id": 2, "name": "Couch", "price": 1299.5 },
      { "id": 3, "name": "Chair", "price": 54 }
    ]
  }
}

Also test the lookup resolver, which the gateway will use later:

graphql
query {
  productById(id: 1) {
    id
    name
    price
  }
}

Stop the server with Ctrl+C before continuing.

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:

shell
dotnet run --project ./Products -- schema export

This generates two files in the Products project directory:

  • schema.graphqls: The Products subgraph's source schema, describing its types and fields.
  • schema-settings.json: A companion file that tells Fusion the subgraph's name and runtime URL and other subgraph options.

Because Program.cs uses #!csharp AddGraphQL("Products"), Products/schema-settings.json already contains "name": "Products". The generated transport URL still defaults to http://localhost:5000/graphql, so update it to match port 5001:

json
{
  "name": "Products",
  "transports": {
    "http": {
      "url": "http://localhost:5001/graphql"
    }
  }
}

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

Your Products subgraph now:

  • Exposes a products query that returns all products
  • Provides a productById lookup that the gateway will use to resolve Product references from other subgraphs
  • Has an exported schema file (schema.graphqls) describing its types
  • Has a settings file (schema-settings.json) telling Fusion where to reach it at runtime

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)

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

From the fusion-getting-started directory:

shell
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

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:

json
"launchUrl": "http://localhost:5002/graphql",
"applicationUrl": "http://localhost:5002"

The Reviews subgraph runs on port 5002, while the Products subgraph runs on 5001.

Define the Review Type

Create Review.cs in the Reviews project:

csharp
namespace Reviews;

public class Review
{
    public int Id { get; set; }

    public required string Body { get; set; }

    public int Stars { get; set; }

    public int ProductId { get; set; }
}

Each review has a ProductId that references a product from the Products subgraph.

Add In-Memory Data

Create ReviewRepository.cs:

csharp
namespace Reviews;

public static class ReviewRepository
{
    private static readonly List<Review> Reviews =
    [
        new Review { Id = 1, Body = "Sturdy and well-built.", Stars = 5, ProductId = 1 },
        new Review { Id = 2, Body = "A bit wobbly.", Stars = 3, ProductId = 1 },
        new Review { Id = 3, Body = "Very comfortable!", Stars = 5, ProductId = 2 },
        new Review { Id = 4, Body = "Good value for the price.", Stars = 4, ProductId = 3 },
    ];

    public static Review? GetById(int id)
        => Reviews.FirstOrDefault(r => r.Id == id);

    public static List<Review> GetByProductId(int productId)
        => Reviews.Where(r => r.ProductId == productId).ToList();

    public static List<Review> GetAll()
        => Reviews;
}

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

This is the most important file in this section. Create Product.cs:

csharp
namespace Reviews;

public sealed record Product(int Id)
{
    public List<Review> GetReviews()
        => ReviewRepository.GetByProductId(Id);
}

This is not a duplicate of the Product type from the Products subgraph. It is an entity stub, a lightweight declaration that says: "I know Product exists in the API, identified by Id, and I want to add a reviews field to it."

The Reviews subgraph does not define name, price, or any other Product fields. It only contributes the reviews field. When the gateway composes the API, it merges this stub with the full Product type from the Products subgraph. Clients see one Product type with:

  • id, name, and price from the Products subgraph
  • reviews from the Reviews subgraph

All fields appear on a single unified Product type in the composite schema.

Add Query Resolvers

Create ReviewQueries.cs:

csharp
namespace Reviews;

[QueryType]
public static partial class ReviewQueries
{
    [Lookup]
    public static Review? GetReviewById(int id)
        => ReviewRepository.GetById(id);

    public static List<Review> GetReviews()
        => ReviewRepository.GetAll();
}

And create ProductQueries.cs. This provides the internal lookup that lets the gateway resolve Product references within the Reviews subgraph:

csharp
namespace Reviews;

[QueryType]
public static partial class ProductQueries
{
    [Lookup, Internal]
    public static Product? GetProductById(int id)
        => new(id);
}

Two attributes to notice:

  • [Lookup] makes this a lookup resolver for the Product entity. The gateway calls this when it needs to resolve a Product reference.
  • [Internal] hides this field from the composite schema. Clients cannot call productById on the Reviews subgraph directly. It exists only for the gateway's internal use during query planning.

Why is the internal lookup needed? When a client queries review.product.reviews, the gateway needs a way to enter the Reviews subgraph's Product type so it can resolve the reviews field. The internal lookup provides this entry point. Given a product ID (which the gateway already knows from another subgraph), it constructs a Product stub that the reviews field can then resolve against.

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

Set Program.cs to:

csharp
var builder = WebApplication.CreateBuilder(args);

builder
    .AddGraphQL("Reviews")
    .AddTypes();

var app = builder.Build();

app.MapGraphQL();
app.RunWithGraphQLCommands(args);

This follows the same pattern as the Products subgraph, but sets the subgraph name to Reviews.

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:

csharp
using HotChocolate.Types;

namespace Reviews;

[ObjectType<Review>]
public static partial class ReviewNode
{
    [BindMember(nameof(Review.ProductId))]
    public static Product GetProduct([Parent] Review review)
        => new(review.ProductId);
}
  • [ObjectType<Review>] tells Hot Chocolate that this class extends the Review type.
  • [BindMember(nameof(Review.ProductId))] replaces the raw ProductId integer on Review with a resolved Product object. In the exported schema, clients see review.product (returning a full Product) instead of review.productId (returning a raw integer).
  • [Parent] tells Hot Chocolate to inject the parent object (the Review) into the resolver. This is how GetProduct() accesses the ProductId from the review it belongs to.

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

Run the Reviews subgraph:

shell
cd Reviews
dotnet run

Open http://localhost:5002/graphql/ and try:

graphql
query {
  reviews {
    id
    body
    stars
  }
}

You should see:

json
{
  "data": {
    "reviews": [
      { "id": 1, "body": "Sturdy and well-built.", "stars": 5 },
      { "id": 2, "body": "A bit wobbly.", "stars": 3 },
      { "id": 3, "body": "Very comfortable!", "stars": 5 },
      { "id": 4, "body": "Good value for the price.", "stars": 4 }
    ]
  }
}

Now try a query that exercises the entity stub and [BindMember]:

graphql
query {
  reviews {
    id
    body
    product {
      id
      reviews {
        body
      }
    }
  }
}

You should see each review with its product reference, and each product with its reviews. Notice that product.name and product.price are not available here. Those fields live in the Products subgraph and will only appear after composition in the gateway. Within the Reviews subgraph alone, Product only has the fields the Reviews subgraph contributes: id and reviews.

Stop the server with Ctrl+C.

Export the Schema

Export the Reviews subgraph's source schema. From the fusion-getting-started directory:

shell
dotnet run --project ./Reviews -- schema export

This generates two files in the Reviews project directory:

  • schema.graphqls: The Reviews subgraph's source schema, describing its types and fields.
  • schema-settings.json: A companion file that tells Fusion the subgraph's name and runtime URL and other subgraph options.

Because Program.cs uses #!csharp AddGraphQL("Reviews"), Reviews/schema-settings.json already contains "name": "Reviews". The generated transport URL still defaults to http://localhost:5000/graphql, so update it to match port 5002:

json
{
  "name": "Reviews",
  "transports": {
    "http": {
      "url": "http://localhost:5002/graphql"
    }
  }
}

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

Your Reviews subgraph now:

  • Exposes a reviews query and a reviewById lookup
  • Adds a reviews field to the Product type using an entity stub, without duplicating any Product data
  • Replaces the raw productId on reviews with a resolved Product reference via [BindMember]
  • Provides an internal productById lookup that the gateway uses for cross-subgraph resolution
  • Has an exported schema and settings file ready for composition

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

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

Before running composition, make sure your project looks like this:

text
fusion-getting-started/
├── Products/
│   ├── Product.cs
│   ├── ProductQueries.cs
│   ├── ProductRepository.cs
│   ├── Program.cs
│   ├── Products.csproj
│   ├── schema.graphqls          <-- exported schema
│   └── schema-settings.json     <-- subgraph settings
└── Reviews/
    ├── Review.cs
    ├── ReviewRepository.cs
    ├── ReviewQueries.cs
    ├── ProductQueries.cs
    ├── Product.cs
    ├── ReviewNode.cs
    ├── Program.cs
    ├── Reviews.csproj
    ├── schema.graphqls          <-- exported schema
    └── schema-settings.json     <-- subgraph settings

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

From the fusion-getting-started directory, run:

shell
mkdir Gateway
nitro fusion compose -f Products/schema.graphqls -f Reviews/schema.graphqls -a Gateway/gateway.far

If composition succeeds, you will see output similar to:

text
Validating source schemas...
Merging schemas...
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

The Nitro CLI performs four steps:

  1. Validate source schemas: check GraphQL validity and Fusion metadata.
  2. Merge source schemas: combine matching types and enforce field ownership rules.
  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

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

From the fusion-getting-started directory:

shell
cd Gateway
dotnet new graphql-gateway
cd ..

The graphql-gateway template already includes the HotChocolate.Fusion.AspNetCore package and a working gateway Program.cs.

Your Gateway/Gateway.csproj should look like this:

xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="HotChocolate.Fusion.AspNetCore" />
  </ItemGroup>

</Project>

Configure the Port

Edit Gateway/Properties/launchSettings.json and set launchUrl and applicationUrl under the http profile to:

json
"launchUrl": "http://localhost:5000/graphql",
"applicationUrl": "http://localhost:5000"

The gateway runs on port 5000. The subgraphs run on ports 5001 (Products) and 5002 (Reviews).

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:

shell
ls Gateway/gateway.far

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:

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddHttpClient("fusion");

builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .ModifyRequestOptions(o => o.CollectOperationPlanTelemetry = true);

var app = builder.Build();

app.MapGraphQL();
app.Run();

Three things to notice:

  • #!csharp AddHttpClient("fusion") registers a named HTTP client called "fusion". The gateway uses this client to send requests to the subgraphs. The name "fusion" is the default HTTP client name that Fusion uses when no explicit clientName is specified in the subgraph's schema-settings.json.
  • AddGraphQLGateway() registers the Fusion gateway services. This is what makes this project a gateway rather than a regular GraphQL server.
  • #!csharp 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.
  • #!csharp ModifyRequestOptions(o => o.CollectOperationPlanTelemetry = true) enables operation-plan telemetry. This is off by default.

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.

[!CAUTION]
Start the Products and Reviews subgraphs before the gateway. The gateway connects to each subgraph on startup and may log errors if they are not reachable.

Terminal 1: Products subgraph

shell
cd fusion-getting-started/Products
dotnet run

Terminal 2: Reviews subgraph

shell
cd fusion-getting-started/Reviews
dotnet run

Terminal 3: Gateway

shell
cd fusion-getting-started/Gateway
dotnet run

Wait for all three services to start. You should see output like this in each terminal:

text
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.

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

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:

graphql
query {
  products {
    id
    name
    price
  }
}

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

Your gateway now:

  • Loads the composed configuration from the gateway.far file
  • Exposes the unified composite schema on port 5000
  • Routes queries to the Products and Reviews subgraphs as needed
  • Acts as the single entry point for clients, so they never talk to the subgraphs directly

In the next section, you will run queries that demonstrate the gateway coordinating data across both subgraphs in a single request.

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

Open Nitro at http://localhost:5000/graphql/ and run this query:

graphql
query {
  products {
    id
    name
    price
    reviews {
      body
      stars
    }
  }
}

Example in Nitro:

You should see:

json
{
  "data": {
    "products": [
      {
        "id": 1,
        "name": "Table",
        "price": 899.99,
        "reviews": [
          { "body": "Sturdy and well-built.", "stars": 5 },
          { "body": "A bit wobbly.", "stars": 3 }
        ]
      },
      {
        "id": 2,
        "name": "Couch",
        "price": 1299.5,
        "reviews": [{ "body": "Very comfortable!", "stars": 5 }]
      },
      {
        "id": 3,
        "name": "Chair",
        "price": 54,
        "reviews": [{ "body": "Good value for the price.", "stars": 4 }]
      }
    ]
  }
}

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

Behind the scenes, the gateway executed a query plan with multiple steps:

To inspect the plan in Nitro, open the Operation Plan tab and enable it.

With CollectOperationPlanTelemetry enabled on the gateway, the plan includes telemetry such as step duration and status:

  1. Fetched the products from the Products subgraph. This returned id, name, and price for each product.
  2. Resolved the reviews from the Reviews subgraph. Using each product's id, the gateway called the Reviews subgraph's internal productById lookup to get a Product stub, then resolved the reviews field on each stub.
  3. Combined the results into a single response that looks exactly like it came from one GraphQL server.

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

You can also query in the other direction: start from reviews and reach into product data.

graphql
query {
  reviews {
    body
    stars
    product {
      name
      price
    }
  }
}

You should see:

json
{
  "data": {
    "reviews": [
      {
        "body": "Sturdy and well-built.",
        "stars": 5,
        "product": { "name": "Table", "price": 899.99 }
      },
      {
        "body": "A bit wobbly.",
        "stars": 3,
        "product": { "name": "Table", "price": 899.99 }
      },
      {
        "body": "Very comfortable!",
        "stars": 5,
        "product": { "name": "Couch", "price": 1299.5 }
      },
      {
        "body": "Good value for the price.",
        "stars": 4,
        "product": { "name": "Chair", "price": 54 }
      }
    ]
  }
}

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

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

You can also look up a single product by ID:

graphql
query {
  productById(id: 1) {
    name
    price
    reviews {
      body
      stars
    }
  }
}

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

You proved that Fusion works:

  • Clients send queries to one endpoint (the gateway)
  • The gateway routes parts of each query to the appropriate subgraphs
  • Multiple subgraphs contribute fields to the same types (Product, Review)
  • Cross-subgraph entity resolution happens automatically through lookups
  • The result is a unified API that feels like one service

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

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.

But what happens when two subgraphs need to define the same field on the same type? By default, Fusion treats this as an error. If two subgraphs both define Product.name, composition fails because Fusion does not know which subgraph should be authoritative.

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

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:

Products subgraph:

csharp
public class Product
{
    public int Id { get; set; }

    [Shareable]
    public required string Name { get; set; }

    public double Price { get; set; }
}

Reviews subgraph (entity stub):

csharp
public sealed record Product(int Id)
{
    [Shareable]
    public string Name => ProductRepository.GetProductName(Id);

    public List<Review> GetReviews()
        => ReviewRepository.GetByProductId(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

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

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

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:


Website Content Docs Fusion Index (`website/content/docs/fusion/index.md`)


title: Fusion metaTitle: "Fusion: Federated GraphQL Gateway"

Fusion lets you split one GraphQL API into multiple smaller services, without changing how clients consume it. Clients still send queries to one endpoint, and Fusion combines data from all services into one response. Teams can deploy independently, and contract conflicts are caught during build time.

What Is Fusion

Fusion is ChilliCream's API gateway for exposing one GraphQL API over multiple upstream services. Those upstream services can be GraphQL, OpenAPI-based REST, or gRPC. Each service owns its contract and implementation. Fusion composes those contracts at build time, and the gateway orchestrates execution at runtime. Fusion implements the GraphQL Composite Schemas specification, an open standard being developed under the GraphQL Foundation.

The architecture has three parts:

Subgraphs are the upstream services behind the Fusion gateway: GraphQL services, OpenAPI-based REST services, and gRPC services. Each subgraph owns part of the API surface and implementation logic, and can be developed and deployed independently.

A source schema is the contract document for a subgraph, such as a GraphQL schema, an OpenAPI document, or a gRPC/protobuf definition.

Composition processes all source schemas, validates them against each other, and produces a Fusion archive (.far) that contains the composite schema and gateway configuration. Type conflicts, missing fields, and incompatible enums are caught in CI before deployment.

The gateway receives client requests, determines which subgraphs to call, executes those calls, and merges the results.

GraphQL subgraphs stay standard GraphQL servers. The GraphQL Composite Schemas specification is designed so a standard GraphQL server can already act as a compatible subgraph. In a common Hot Chocolate setup, subgraphs remain normal Hot Chocolate servers with regular resolvers, without a separate distributed-runtime package or vendor-specific protocol layer.

The result: clients send one request to one endpoint and receive one unified response, while Fusion handles routing and aggregation across upstream services.

The following query touches three services, but the client doesn't know or care about this implementation detail.

graphql
query {
  products(first: 5) {
    nodes {
      name          # from Products service
      price         # from Products service
      reviews {     # from Reviews service
        stars
        author {
          username  # from Accounts service
        }
      }
    }
  }
}

Key Terminology

Term Definition
Subgraph An upstream service behind the Fusion gateway. A subgraph can be a GraphQL service, an OpenAPI-based REST service, or a gRPC service.
Source schema The contract document published by one subgraph (for example a GraphQL schema, OpenAPI document, or gRPC/protobuf definition).
Composite schema The unified, client-facing GraphQL schema produced during composition. Clients query this schema as if it were a single API.
Gateway The public entry point for client requests. It receives queries against the composite schema, routes requests across subgraphs, and assembles responses.
Entity A type with a stable key that can be referenced across GraphQL subgraphs. A subgraph can define an entity without resolving it locally.
Lookup A Query field annotated with a @lookup directive that resolves an entity by key in that subgraph.
Composition The offline step that validates source schemas and produces the composite schema and gateway configuration. Runs via the Nitro CLI or Aspire.

When to Use Fusion

Fusion adds operational complexity, including a gateway process, a composition step in your build pipeline, and distributed debugging. That complexity pays off in specific situations:

  • Multiple teams need to ship independently. If different teams own different parts of your API (e.g., a product catalog team and a reviews team), Fusion lets each team deploy on their own schedule without coordinating schema changes through a shared codebase.

  • You need to scale services differently. Your product search might need 10 instances while your user profile service needs 2. With separate services, you scale each one based on its actual load.

  • Your domain has clear boundaries. If your data naturally splits into distinct areas (accounts, products, orders, reviews), separate services map well to those boundaries. Each service owns its data store and its API contract.

  • You want build-time validation of distributed contracts. Composition catches conflicts between source schemas before deployment. Your CI pipeline can validate that a change in one service does not break the composed API.

When NOT to Use Fusion

Fusion is not the right choice for every project. Evaluate whether the additional complexity is justified:

  • One team, one service. If one team owns the entire API and deploys it as a single unit, a standard Hot Chocolate server is simpler and has lower operational overhead. You likely do not need a gateway, a composition pipeline, or distributed tracing.

  • A small or early-stage API. If your API has a handful of types and modest traffic, a distributed gateway setup often adds more complexity than value. Start with a monolith and split later when needed. Hot Chocolate supports an incremental path from monolith to modular monolith to distributed architecture.

  • No clear domain boundaries. If your types are deeply intertwined and most queries touch most of the schema, splitting into many services can create more cross-service calls than it removes. Fusion works best when services are relatively self-contained and have clear data contracts.

  • Your team is just getting started with GraphQL. Learn GraphQL and Hot Chocolate first. Get comfortable with types, resolvers, DataLoaders, and the execution pipeline. Fusion adds concepts on top of that foundation and is easier to adopt once the basics are understood.

The cost of premature distribution is real: more services to deploy, more infrastructure to monitor, and harder debugging when something goes wrong. Start simple, and add Fusion when the pain of a monolith outweighs the cost of distribution.

Migrating from a Monolith

If you already have a Hot Chocolate server, you can adopt Fusion incrementally.

Start with one upstream service. Point the Fusion gateway at your existing Hot Chocolate server as the only subgraph. Composition works with one source schema. Your clients connect to the gateway instead of directly to your server, but behavior stays the same.

Add services incrementally. When a new team or domain needs its own service, add another subgraph. The new service can extend types from the original service with entity stubs where needed. Composition merges both source schemas, and the gateway handles cross-service execution automatically. Your original service does not need a rewrite.

Clients see no difference. Whether you have one subgraph or ten, clients still call one endpoint and keep the same query surface. You can split your monolith over weeks or months without breaking the client contract.

The key insight: this is not a rewrite. It is a gradual process. You move types and fields to new services over time, and the gateway smooths over the transition.

Next Steps

Where you go from here depends on what you need:

  • "I want to build something." Start with the Getting Started tutorial. You will create two services and a gateway from scratch.

  • "I want to add another service to an existing project." Go to Adding a Subgraph. It covers creating a new service (subgraph) that extends existing entity types.

  • "I'm migrating from another distributed GraphQL framework." Read Coming from Apollo Federation or Migrating from Schema Stitching. These guides map familiar concepts to Fusion equivalents and walk through a migration.

  • "I need to deploy this." See Deployment & CI/CD for pipeline setup, schema management, and gateway configuration.

  • "I need CDN and browser caching behavior." See Cache Control for @cacheControl, composition merge behavior, and gateway response headers.


Website Content Docs Hotchocolate Index (`website/content/docs/hotchocolate/index.md`)


title: Hot Chocolate metaTitle: "Hot Chocolate: GraphQL Server for .NET"

Hot Chocolate is an open-source GraphQL server for .NET. You define your API shape using C# classes and methods, and Hot Chocolate translates that into a spec-compliant GraphQL schema. It handles parsing, validation, execution, and transport so you can focus on your domain logic.

What Is Hot Chocolate

Hot Chocolate is a GraphQL server framework that runs on ASP.NET Core. You write C# types and resolvers. Hot Chocolate turns them into a GraphQL schema, validates incoming operations against that schema, executes them, and returns results over HTTP, WebSocket, or Server-Sent Events.

Hot Chocolate implements the GraphQL 2025 specification and several draft features including @defer, @stream, and @requiresOptIn. It implements the GraphQL over HTTP specification for transport. It is compatible with all spec-compliant clients, including Strawberry Shake, Relay, and Apollo Client.

How You Build a Schema

Hot Chocolate supports two approaches to building a GraphQL schema. Both produce the same result: a fully typed, spec-compliant GraphQL schema. They differ in how you express it in C#.

Implementation-First

With implementation-first, your C# implementation is the single source of truth for your GraphQL schema. Define your API using familiar C# classes and attributes like [QueryType]. At build time, a source generator analyzes your code and creates the GraphQL types for you. You focus on your business logic, since your implementation is your schema. You don’t need to manually keep your code and schema in sync, deal with GraphQL-specific boilerplate, or write large type definitions in C#.

csharp
[QueryType]
public static partial class ProductQueries
{
    public static async Task<Product?> GetProductByIdAsync(
        int id,
        CatalogContext db,
        CancellationToken ct)
        => await db.Products.FindAsync([id], ct);
}

This approach is similar to how Meta built their GraphQL server. Your schema stays close to your domain code, while the tooling handles the translation.

Code-First

The code-first approach lets you define your GraphQL types and schema structure directly in C# using Hot Chocolate’s fluent type descriptor API.

csharp
public class ProductType : ObjectType<Product>
{
    protected override void Configure(IObjectTypeDescriptor<Product> descriptor)
    {
        descriptor
            .Field(p => p.Id)
            .Type<NonNullType<IdType>>();

        descriptor
            .Field(p => p.Name)
            .Type<NonNullType<StringType>>();
    }
}

Code-first is useful when you need to decouple the GraphQL schema shape from your C# model, or when you are building infrastructure that generates schemas programmatically.

Both approaches can be mixed in the same project. You can use implementation-first for most types and drop into code-first for specific cases that need more control.

GraphQL API Security Strategies

GraphQL APIs are typically designed for one of two usage models.

Either your API is exclusively consumed by applications you control (first-party), such as your own web, mobile, or internal services, where you define and manage every client operation.

Or your API is open to external developers or partners (third-party), and you have no control over the GraphQL operations they send.

This distinction shapes how you configure Hot Chocolate and operate your GraphQL server.

First-party GraphQL

A first-party API is consumed exclusively by your own applications. This is how Meta built and operates GraphQL internally. Because you control both the server and every client, you know every operation at build time. This enables you to maintain a precise schema usage history and strictly allow only approved GraphQL operations.

Hot Chocolate supports this scenario with trusted documents. You extract all operations from your client applications during their build process, register them with the server, and the server only accepts pre-registered operations.

When in the future you want to change or phase out parts of your schema you know the impact this change will have to your system before you apply it. This is a super power for API evolution.

Third-party GraphQL

A third-party API is consumed by external developers or clients outside your organization. GitHub’s GraphQL API is a canonical example. You publish a schema, and external teams build applications against it. Because you do not control the clients, they can send any operation they want.

Hot Chocolate provides cost analysis for this scenario. You assign weights to fields and connections, and the server rejects operations that exceed the performance budget before execution begins.

These two approaches complement each other. A common setup is to host both a public and an internal GraphQL API. The internal API uses trusted documents to strictly control operations, while the public API relies on cost analysis and other safeguards to manage external traffic and protect against abuse.

Key Terminology

Term Definition
Schema The contract that describes what data clients can query. Hot Chocolate generates it from your C# code.
Query type The root type for read operations. Clients enter the graph through fields on this type.
Mutation type The root type for write operations. Mutations execute serially and are expected to cause side effects.
Subscription type The root type for real-time operations. Clients subscribe to events and receive updates as they occur.
Resolver A function that fetches data for a single field. In implementation-first, each public method on a [QueryType] class is a resolver for third-party or first-party APIs.
Batch resolver A resolver that fetches data for multiple parent objects in a single call, improving performance by reducing the number of backend requests. Useful for solving the N+1 problem and optimizing data access patterns.
DataLoader A batching and caching layer that groups multiple individual data requests into a single batch call, eliminating the N+1 problem.
Source generator A Roslyn source generator that inspects your C# code at build time and generates the schema registration, resolver pipelines, and DataLoader infrastructure.
Cost analysis An analysis pass that evaluates operation cost before execution and rejects operations that exceed configured limits. Based on the IBM Cost Analysis specification.
Trusted documents Pre-registered operations that the server accepts by hash. Operations not in the store are rejected. Also known as persisted operations.

Scaling Beyond a Single Server

When your API grows beyond what a single service can handle, Fusion lets you split your schema across multiple independent services. Each service owns part of the API surface. A gateway composes them into one unified schema that clients query as a single endpoint.

Fusion is not a separate product. It builds on Hot Chocolate. A standard Hot Chocolate server can act as a Fusion subgraph without changes to its resolvers or type definitions. You can start with a single Hot Chocolate server and add Fusion later when you need independent deployment or team-level ownership boundaries.

Next Steps

Where you go from here depends on what you need:

  • "I want to build something." Start with the Getting Started tutorial. You will create a running GraphQL server in under five minutes.

  • "I want to understand the schema system." Read Defining a Schema. It covers queries, mutations, subscriptions, and all the GraphQL types.

  • "I need to fetch data efficiently." Go to DataLoader for batching and caching, or Resolvers for the full resolver API.

  • "I need to secure my API." See Securing Your API for authentication, authorization, cost analysis, and trusted documents.

  • "I'm migrating from an older version." Read the migration guide from v15 to v16.

  • "I want to split my API across services." See Fusion for distributed GraphQL with a gateway.


Website Content Docs Mocha Index (`website/content/docs/mocha/index.md`)


title: Mocha metaTitle: "Mocha: Messaging Bus for .NET"

csharp
// Inter-service messaging via the message bus
builder.Services
    .AddMessageBus()
    .AddOrderService() // source-generated handler registration
    .AddRabbitMQ();

// In-process CQRS via the mediator
builder.Services
    .AddMediator()
    .AddHandlers();

Mocha gives you two dispatch mechanisms. The message bus sends messages across service boundaries through a transport like RabbitMQ. The mediator dispatches commands, queries, and notifications within a single process using source-generated code - no reflection, no dictionary lookups. Use them independently or together.

What Mocha is

Mocha is a messaging framework for .NET with two complementary dispatch systems:

  • Message bus - sends messages across service boundaries through transports like RabbitMQ. Supports pub/sub events, request/reply, saga orchestration, inbox/outbox reliability, and pluggable transports. Follows the patterns described in Enterprise Integration Patterns.
  • Mediator - dispatches commands, queries, and notifications within a single process. A Roslyn source generator produces a concrete mediator class at compile time with specialized dispatch and pre-compiled pipeline delegates. No reflection, no runtime code generation.

Both integrate directly into ASP.NET Core's dependency injection and are designed for event-driven architectures. Use the message bus when messages cross process boundaries. Use the mediator when you want in-process CQRS with pipeline behaviors for cross-cutting concerns like validation, logging, and transactions. Most real-world services use both: the mediator handles internal command/query dispatch, and the message bus handles inter-service events.

The framework is handler-first in both cases. You implement handler interfaces, and Mocha builds the infrastructure around those declarations - whether that means wiring up transport endpoints and middleware pipelines for the bus, or generating a type-safe mediator class with pre-compiled dispatch for in-process handlers.

Terminology

These terms appear throughout the documentation. They are defined once here and used consistently everywhere.

Term Definition
Message A plain C# record representing business data. The unit of communication between handlers and services.
Event A message published via PublishAsync. Represents something that happened. Multiple handlers can receive an event.
Request A message sent via SendAsync or RequestAsync. When sent with RequestAsync, the sender awaits a typed response. When sent with SendAsync, it is fire-and-forget.
Handler A class implementing a Mocha handler interface that processes a specific message type.
Consumer The processing unit that wraps a handler or custom logic. Mocha builds consumers automatically from handlers, or you can implement IConsumer<T> or Consumer<T> directly.
Endpoint A named, addressable unit in the messaging topology. Each endpoint has a transport address, a middleware pipeline, and a kind (default, error, skipped, or reply). Receive endpoints consume messages; dispatch endpoints produce them.
Transport The infrastructure layer connecting Mocha to a message broker, such as RabbitMQ or an in-process channel.
Pipeline The chain of middleware that processes a message from the transport through to the handler.
Saga A long-running stateful workflow that coordinates multiple messages and transitions across services.
Mediator An in-process dispatcher that routes commands, queries, and notifications to their handlers without a transport layer. Source-generated at compile time.
Source generator A Roslyn analyzer that discovers handlers and sagas at compile time and generates typed registration code. Used by both the mediator and the message bus.
Command A mediator message representing an action. Implements ICommand (void) or ICommand (with response). Dispatched via SendAsync.
Query A mediator message representing a read operation. Implements IQuery. Dispatched via QueryAsync.

Architecture

When you call PublishAsync, here is what happens:

Your code in OrderService calls PublishAsync with an OrderPlaced message (1). Mocha serializes it and hands it to the dispatch endpoint (2), which sends it into RabbitMQ. The broker routes the message through two exchanges into the queue (the unnumbered transport layer between the services). On the BillingService side, the receive endpoint (3) picks the message off the queue, runs it through the middleware pipeline, and calls HandleAsync on your OrderPlacedHandler (4). Middleware in the pipeline handles cross-cutting concerns - tracing, retries, concurrency limits - without touching your handler code.

Core capabilities

Handler-first design

You write handlers. That is the primary abstraction. Implement an interface, register it with the builder, and your handler runs when the matching message arrives.

csharp
public class OrderPlacedHandler(AppDbContext db)
    : IEventHandler<OrderPlaced>
{
    public async ValueTask HandleAsync(
        OrderPlaced message,
        CancellationToken cancellationToken)
    {
        var invoice = new Invoice { OrderId = message.OrderId };
        db.Invoices.Add(invoice);
        await db.SaveChangesAsync(cancellationToken);
    }
}

Mocha provides handler interfaces for each messaging pattern:

Interface Pattern Bus method
IEventHandler<T> Pub/sub events PublishAsync
IEventRequestHandler Request/reply RequestAsync
IEventRequestHandler Send (fire-and-forget) SendAsync
IBatchEventHandler<T> Batch processing PublishAsync (batched)

Messaging patterns and consumers

Mocha supports three core patterns for message-driven systems. Each pattern answers a different question:

  • Events (pub/sub): Who needs to know? Publish once, all subscribers receive it. Use PublishAsync and IEventHandler<T>.
  • Send (fire-and-forget): Who should act? Deliver to one endpoint without waiting for a result. Use SendAsync and IEventRequestHandler.
  • Request/reply: What is the result? Send and await a typed response. Use RequestAsync and IEventRequestHandler.

Handlers are the fastest way to get started, but they are not the only option. If you need full control over message processing, implement IConsumer<T> for a lightweight consumer or extend Consumer<T> for complete customization.

Pluggable transports

Switch transports without changing your handler code, and use multiple transports at the same time. Register a default transport, then route specific messages through a different transport when you need different throughput or delivery characteristics:

csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedHandler>()
    .AddEventHandler<ClickStreamHandler>()
    // Default transport for most messages
    .RabbitMQ()
    // High-throughput transport for specific messages
    .AddEventHub(t => t.AddEventHandler<ClickStreamHandler>());

Middleware pipelines

Underneath, everything in Mocha is a middleware pipeline - dispatch, receive, and consumer processing are each a chain you can customize. Mocha compiles these pipelines into an optimized chain with no per-message dictionary lookups or dynamic dispatch at runtime. You can insert your own middleware at any point in the pipeline for cross-cutting concerns like logging, validation, or custom error handling.

OpenTelemetry-native observability

Every message dispatch, receive, and handler execution produces structured traces and metrics through OpenTelemetry. Correlation IDs propagate across service boundaries automatically.

csharp
builder.Services
    .AddMessageBus()
    .AddInstrumentation()
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();

Connect your services to Nitro to introspect your messaging configuration visually. Here is a real-world example: OrderApi publishes OrderPlaced, and the broker fans it out to both BillingService and InventoryService through separate exchanges and queues. The trace shows every hop:

Expand the visualization to see the full trace sidebar. Each numbered step - publish, dispatch, receive, consume - is a real OpenTelemetry span. The unnumbered nodes between dispatch and receive are the RabbitMQ exchanges and queues the message passed through. When something goes wrong, this is how you answer "where did the message go?" without digging through logs.

Resiliency

Mocha provides an inbox and outbox to guarantee reliable message processing. The outbox ensures that database writes and message dispatches succeed or fail together - no lost messages during failures. The inbox ensures that messages are processed exactly once, even when the transport delivers duplicates. Both are optimized for your specific database system.

csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedHandler>()
    .AddEntityFramework<AppDbContext>(p =>
    {
        p.UsePostgresOutbox();
        p.UseTransaction();
        p.UsePostgresInbox();
    })
    .AddRabbitMQ();

Saga orchestration

Sagas coordinate multi-step workflows that span multiple services and messages. You define a state machine with states and transitions, and Mocha validates the entire state machine - every state must be reachable and every path must lead to a final state. Reducing the possibility of deploying a saga that gets stuck in an intermediate state.

Mocha persists saga state, manages transitions, and supports compensation when steps fail. See Sagas for a full walkthrough.

In-process mediator

For commands and queries that stay within a single service, the mediator provides CQRS dispatch with middleware - without a transport layer. Define your messages with marker interfaces, implement handlers, and the source generator wires everything at compile time:

csharp
// Define a command and its handler
public record PlaceOrderCommand(Guid ProductId, int Quantity)
    : ICommand<PlaceOrderResult>;

public class PlaceOrderCommandHandler(AppDbContext db)
    : ICommandHandler<PlaceOrderCommand, PlaceOrderResult>
{
    public async ValueTask<PlaceOrderResult> HandleAsync(
        PlaceOrderCommand command, CancellationToken cancellationToken)
    {
        // business logic
        return new PlaceOrderResult(true, Guid.NewGuid());
    }
}
csharp
// Register and use
builder.Services
    .AddMediator()
    .AddCatalog()
    .UseEntityFrameworkTransactions<AppDbContext>();

app.MapPost("/orders", async (ISender sender) =>
    await sender.SendAsync(new PlaceOrderCommand(productId, 2)));

The mediator supports commands (with and without responses), queries, notifications, middleware, and EF Core transaction wrapping (commands only by default, configurable via delegate). The source generator produces a typed registration method per assembly (e.g. AddCatalog()) that wires up all handlers and pre-compiled dispatch pipelines automatically. See Mediator for the full guide.

Learning paths

Choose an entry point based on how you learn best:

  • Get something running first: Quick Start -zero to a working message bus in under five minutes with the InMemory transport.

  • Understand the concepts first: Messages then Messaging Patterns - learn what flows through the system and what patterns govern how it flows.

  • Evaluating Mocha for a specific broker: Transports - understand the transport abstraction and what is available.

  • In-process CQRS: Mediator - dispatch commands, queries, and notifications within a single service using the source-generated mediator.

  • See a real-world system: The Demo application is a complete e-commerce system with three services (Catalog, Billing, Shipping) that demonstrates event-driven communication, sagas, batch processing, the transactional outbox, and .NET Aspire orchestration.

Ready to build? Start with the Quick Start.


Website Content Docs Nitro Getting Started (`website/content/docs/nitro/getting-started.md`)


title: "Getting Started"

Ready to explore the features of Nitro? In this getting started guide, we'll show you how to get up and running with Nitro.
You have the option to choose between the convenience of the web app or the enhanced capabilities of the desktop version. We'll guide you through the setup process, so you can start enjoying all that Nitro has to offer.

Get ready to unlock the full potential of GraphQL with Nitro. Choose your preferred option and follow the setup instructions to start enjoying the features that Nitro has to offer.

Web App

To begin your exploration, simply visit nitro.chillicream.com and experience the convenience of Nitro's web-based version. This excellent starting point allows you to conveniently probe public GraphQL APIs and access most of the features that Nitro offers. For an even more immersive experience, you can also install the web app as a Progressive Web App (PWA) on your device, providing a native-like experience.

Desktop App

You can install the Nitro App directly from our download page. The download page provides DMG installers for macOS, x64 installers for Windows, and AppImage/Snap installers for Ubuntu and Ubuntu-based Linux distributions. Note that other Linux distributions or installer formats are currently unsupported.

Upon successful installation, follow the steps in the Your first Query guide to execute your first GraphQL query using Nitro.

Your First Query

Lets guide you through the process of creating your first GraphQL query document using Nitro.

Step 1: Start the Nitro application. You should see an interface similar to this:

Step 2: Click on the "Create Document" button to initiate the creation of your first query document.

Step 3: You'll see a new document called 'untitled 1' generated. Simultaneously, the connection settings dialog box will pop up automatically.
Now, input the following link into the 'HTTP Endpoint' field: https://workshop.chillicream.com/graphql
It should resemble this:

Step 4: Click on the 'Apply' button. This will save your settings and close the dialog box.

Step 5: At this point, your screen should look like this:

Take note of the following key elements:

  1. Request Pane: This is where you will input your queries.
  2. Response Pane: Here, you will see the output or response of your queries.
  3. Variables Pane: This is where you can define variables for your query.

Step 6: The green circle at the top right next to the schema url indicates that you are connected to the GraphQL server and that your schema is fetched. You now have full intellisense for your queries. Copy the following GraphQL query and paste it into the 'Operations' editor area:

graphql
{
  sessionById(id: "U2Vzc2lvbgppMQ==") {
    title
    track {
      name
    }
    startTime
    endTime
  }
}

Step 7: Click on the 'Run' button. You should see a response with data in the 'Response' area. Congratulations, you have completed a GraphQL query!

Step 8:
Now, let's save your document.

Sign in to your account by clicking the 'Sign In' button on the user icon in the bottom left corner of the screen. If you don't have an account, you can create a new one.

Step 9: Once you are signed in, save your document by clicking the 'Save' button next to the tabs. Your documents are now synced between your devices.

Step 10: Great job! You've successfully created, executed, and saved your first GraphQL query using Nitro. To learn more about the Nitro User Interface, head over to the Explore the UI guide.

Happy querying!


Website Content Docs Nitro Index (`website/content/docs/nitro/index.md`)


title: Nitro metaTitle: "Nitro: GraphQL IDE, Observability, and Governance"

Nitro combines a collaborative GraphQL IDE, schema and client registries, and OpenTelemetry monitoring in one API platform. Use it to explore schemas, run operations, coordinate API changes, and understand production behavior throughout the API lifecycle.

Are you hungry yet? Let's get started!

Features

  1. GraphQL IDE
    Whether you're building or consuming GraphQL APIs, Nitro simplifies the process by allowing you to effortlessly build and execute GraphQL queries and mutations. Its intuitive UI enables you to visualize and explore your GraphQL schemas, making it easier to understand and work with complex structures. The powerful editor provides syntax highlighting, auto-completion, and error highlighting, helping you write GraphQL queries and mutations with ease.

  2. Collaboration
    Additionally, Nitro places a strong emphasis on collaboration. It offers seamless team collaboration features, allowing developers to share documents, API configurations, environment settings and lets you collaborate on projects, and synchronize your work across multiple team members. By facilitating effective communication, Nitro ensures that everyone on your team is on the same page, leading to more efficient and coordinated development efforts.

  3. DevOps
    Nitro supports you during the entire lifecycle of an API. With its schema and client registry, Nitro ensures that your APIs are well-maintained and evolve seamlessly over time. Integrated into your development workflow, Nitro helps you manage your APIs effectively, making easy to build and maintain high-quality APIs.

  4. Schema Registry
    Nitro's schema registry acts as a centralized repository for your GraphQL schemas. It allows you to store, version, and manage your schemas effectively. Whenever you make changes to your schema, Nitro tracks and documents those changes, making it easy to keep a history of modifications. This ensures that you have full visibility into the evolution of your API, making it easier to understand and troubleshoot issues that may arise during development.

  5. Client Registry
    The client registry feature of Nitro complements the schema registry by providing a centralized location to manage and track the clients that consume your GraphQL APIs. It enables you to register and document all the client applications that interact with your APIs. By maintaining an up-to-date client registry, you can easily identify which clients may be affected by schema changes and take appropriate actions to ensure a smooth transition.

  6. Intuitive Interface
    Nitro provides a user-friendly interface that is easy to navigate, even for those new to GraphQL. Its clean and intuitive design makes it a pleasure to work with, enabling developers to focus on their tasks without unnecessary distractions or complications.


Website Content Docs Skills Getting Started (`website/content/docs/skills/getting-started.md`)


title: "Getting Started"

By the end of this guide you will have run skills and added your first skill to your agent.

skills is a .NET CLI that installs, updates, and authors Agent Skills, the portable SKILL.md files that extend AI coding agents like Claude Code, Cursor, and GitHub Copilot. You point it at a source (a GitHub repo, a git URL, or a local folder), and it places the skill where your agent looks for it. Your agent then loads the skill on demand when a task matches it.

This page takes you from zero to one working skill. It takes about five minutes.

Prerequisites

Before you start, make sure you have the following:

Requirement Why you need it
.NET 10 SDK or newer To run skills with dnx, the way these docs show it. The dnx command ships with the .NET 10 SDK.
An AI coding agent The target for your skill. This guide uses Claude Code. The skills CLI supports 55+ agents.

These docs run skills with dnx, so there is nothing to install first. If you would rather have skills on your PATH, install the global tool instead (it needs only the .NET SDK 8.0 or newer). See Install as a global tool.

Run `skills`

These docs run skills with dnx, which needs no install. You can also install it as a global tool if you prefer skills on your PATH.

Run with dnx

dnx ships with the .NET 10 SDK and runs a tool straight from NuGet, the way npx runs a package from npm. Nothing is installed up front.

shell
dnx skills add anthropics/skills --agent claude-code

On the first run, dnx prompts you to confirm the package download before it proceeds:

text
Tool package skills@<version> will be downloaded from source https://api.nuget.org/v3/index.json.
Proceed? [y/n] (y):

After you confirm, the install proceeds and prints the summary shown in Add your first skill. The first run downloads the package into your NuGet cache; later runs reuse it and start immediately. dnx skills is shorthand for dotnet tool exec skills.

To skip the download prompt (for example in CI), pass --yes:

shell
dnx --yes skills add anthropics/skills --agent claude-code

You can control the skills version that dnx runs and where it fetches it from:

Command What it does
#!shell dnx [email protected] add ... Runs an exact version.
#!shell dnx [email protected].* add ... Runs the latest 0.3.x version.
#!shell dnx --prerelease skills add ... Allows prerelease versions.
#!shell dnx --source <feed> skills add ... Fetches from a specific NuGet feed.

If your repository has a .config/dotnet-tools.json manifest that lists skills, dnx honors the pinned version from the manifest, which keeps one-shot runs consistent with the version your team committed.

Install as a global tool

If you would rather have skills on your PATH, install it as a global .NET tool. This needs the .NET SDK 8.0 or newer.

shell
dotnet tool install -g skills
text
You can invoke the tool using the following command: skills
Tool 'skills' (version '0.3.0') was successfully installed.

With the global tool you run skills directly: drop the dnx prefix from every example in these docs (dnx skills add ... becomes skills add ...). Confirm the install with skills --version, which prints a value like 0.3.0+<git-sha> (the trailing suffix records the exact commit the build came from).

To pin skills to a version your whole team shares, install it as a local tool through a manifest instead. From the repository root:

shell
dotnet new tool-manifest
dotnet tool install skills

Local tools are restored with dotnet tool restore and run through dotnet skills. Check the manifest (./.config/dotnet-tools.json) into source control so every collaborator uses the same version.

Add your first skill

You are ready to install a skill. The example below installs skills from anthropics/skills, Anthropic's reference repository, targeting Claude Code.

shell
dnx skills add anthropics/skills --agent claude-code

Agent names are case-sensitive. Use claude-code, not claude or Claude. For GitHub Copilot the name is github-copilot.

The skills CLI fetches the source, discovers the skills, installs them, and prints a summary:

text
Source: https://github.com/anthropics/skills.git
Found 2 skill(s)

┌─Installation Summary─────────────────────────────────────────────────────────┐
│ Canonical: /your-project/.agents/skills                                      │
│ Symlinked:  Claude Code                                                      │
└──────────────────────────────────────────────────────────────────────────────┘
┌─Installed 2 skill(s)─────────────────────────────────────────────────────────┐
│ ✓ pdf                                                                        │
│   → /your-project/.claude/skills/pdf                                         │
│ ✓ docx                                                                       │
│   → /your-project/.claude/skills/docx                                        │
└──────────────────────────────────────────────────────────────────────────────┘

Done!  Review skills before use; they run with full agent permissions.

The trailing line is a deliberate safety nudge. A skill can include scripts that the agent runs with your agent's full permissions. Install skills only from sources you trust, and read a new skill before you use it.

If you omit --agent, the CLI runs interactively and lets you pick which agents to target from the ones it detects on your machine. To install only specific skills from a multi-skill source, add --skill <name> (repeatable). For the full set of source forms, scopes, and flags, see Installing Skills.

Checkpoint. If everything worked, you should see the Installed 2 skill(s) panel and the Done! line, with no error. If you saw an Invalid agents panel, you misspelled the agent name (it is case-sensitive). If you saw No valid skills found, the source had no SKILL.md with both a name and a description. See Troubleshooting for more.

See what is installed

To confirm what landed in your project, list the installed skills:

shell
dnx skills list
text
Project Skills

Skill  Path                   Agents
pdf    ./.claude/skills/pdf   Claude Code
docx   ./.claude/skills/docx  Claude Code

The table shows each skill's name, where it lives on disk, and the agents linked to it (shown by their display name, Claude Code). To list global skills instead, add -g. For machine-readable output, add --json.

Use the skill

The skill now lives where your agent looks for it. You do not need to do anything else.

Your agent loads each installed skill's name and description at startup, then reads the full skill only when a task matches the description. This is progressive disclosure: the description is the trigger, and the body loads on demand. Start a task that matches the skill, and your agent picks it up automatically.

Update or remove the global tool

If you run skills with dnx, you never update it yourself: each run fetches the version you ask for. The commands below apply when you installed the global tool.

To update the global skills tool to the latest version:

shell
dotnet tool update -g skills
text
Tool 'skills' was successfully updated from version '0.3.0' to version '<new>'.

To uninstall it:

shell
dotnet tool uninstall -g skills
text
Tool 'skills' (version '0.3.0') was successfully uninstalled.

Note that dotnet tool update updates the skills CLI, not your installed skills. To check your skills for updates, use the dnx skills update command, covered in Installing Skills.

Next steps

  • Installing Skills: every source form, project versus global scope, picking agents and skills, symlink versus copy, and checking for skill updates.
  • Authoring Skills: scaffold a SKILL.md with dnx skills init, write a good description, and publish your skill.
  • Reference: the complete command and flag surface.

Website Content Docs Skills Index (`website/content/docs/skills/index.md`)


title: Skills metaTitle: "Skills: Agent Skills CLI for .NET"

skills is the .NET CLI that installs, updates, and authors Agent Skills: portable SKILL.md files that teach an AI coding agent how you work. Stop re-explaining your conventions every session. Package the knowledge once, then hand the same skill to Claude Code, Cursor, GitHub Copilot, or another of the 55+ supported agents with a single command.

Run it with dnx, which ships with the .NET 10 SDK, so there is nothing to install first:

shell
dnx skills add anthropics/skills --agent claude-code
text
Source: https://github.com/anthropics/skills.git
Found 2 skill(s)

┌─Installation Summary─────────────────────────────────────────────────────────┐
│ Canonical: /your-project/.agents/skills                                      │
│ Symlinked:  Claude Code                                                      │
└──────────────────────────────────────────────────────────────────────────────┘
┌─Installed 2 skill(s)─────────────────────────────────────────────────────────┐
│ ✓ pdf                                                                        │
│   → /your-project/.claude/skills/pdf                                         │
│ ✓ docx                                                                       │
│   → /your-project/.claude/skills/docx                                        │
└──────────────────────────────────────────────────────────────────────────────┘

Done!  Review skills before use; they run with full agent permissions.

The CLI detects the agents you have installed, then symlinks the skill into each one from a single canonical store. The agent loads the skill on its own when a task matches.

These docs show every command as dnx skills. Prefer skills on your PATH? Install the global tool with #!shell dotnet tool install -g skills, then drop the dnx prefix and run skills directly.

Need the prerequisites or a step-by-step walkthrough? See Get started. The source repository and skills package on NuGet are public.

What are Agent Skills

A skill is a folder with a single SKILL.md file. That file has YAML frontmatter (a name and a description) followed by a Markdown body of instructions. Optional subfolders carry supporting material the agent reads only when it needs it.

Think of a skill as an onboarding guide for a new team member. You write down a procedure once (how to run your test suite, how your commit messages are formatted, which API your team prefers), and the agent references the relevant section when a task calls for it. A general-purpose agent becomes a specialist in your codebase without bespoke prompt engineering on every request.

Skills differ from prompts. A prompt is conversation-level guidance you paste in for one task. A skill loads on demand across every conversation, so you never repaste the same instructions again.

The SKILL.md format is an open standard created by Anthropic and adopted across the ecosystem. The same file works in 55+ supported agents, including Claude Code, Cursor, and GitHub Copilot. To go deeper, read Anthropic's Agent Skills overview and the open specification at agentskills.io.

How skills load: progressive disclosure

Skills stay out of the agent's context window until they are needed. The format loads in three levels, each at a different time. This is the single most important idea to understand, because it is why you can install many skills without paying a context penalty.

Level What loads When Token cost
Level 1: Metadata The name and description from the frontmatter Always, at startup ~100 tokens per skill
Level 2: Instructions The SKILL.md Markdown body When the skill is triggered Keep under ~5k tokens
Level 3: Resources Bundled files in references/, scripts/, assets/ On demand, when the body points to them Effectively unlimited

At startup the agent loads only each skill's name and description. The description is what the agent matches against your request to decide whether to activate the skill, so write it to describe both what the skill does and when to use it. When a request matches, the agent reads the full body. Bundled resources never enter context until the body references them, which is why supporting material can be effectively unbounded.

The open standard frames the same flow as Discovery, Activation, and Execution. For the full model, see Anthropic's overview and the agentskills.io home page.

What a skill looks like

A skill is, at minimum, a folder containing one SKILL.md file. Everything else is optional and loaded only when referenced:

text
my-skill/
├── SKILL.md       # Required: frontmatter (name + description) + Markdown instructions
├── references/    # Optional: extra docs the agent reads on demand
├── scripts/       # Optional: code the agent runs via the shell
└── assets/        # Optional: templates, schemas, images, data files

The folder name should match the name in the frontmatter. Here is a minimal, valid SKILL.md with the two required fields:

markdown
---
name: roll-dice
description: Rolls one or more dice and returns the results. Use when the user asks to roll dice, flip for a decision, or pick a random number in a range.
---

# Roll dice

When the user asks to roll dice, parse the count and number of sides (default to one six-sided die),
then return each roll and the total.

The dnx skills init command scaffolds this layout for you; Authoring Skills walks through it with the generated SKILL.md and folder tree. The full field reference, including optional frontmatter, lives in the open specification and in Authoring Skills.

Why Skills

The skills CLI brings the install-once, run-anywhere skill workflow to the .NET SDK. Three things make it worth adding to your toolbox.

Project-scoped installs are recorded in a skills-lock.json file in your working directory. Commit it, and your whole team shares one reproducible skill set, the same way a lock file pins your package dependencies.

One install reaches every detected agent. The CLI materializes each skill once in a canonical store, then links it into the directory each installed agent expects. You do not manage per-agent copies by hand.

Personal skills install globally with --global. Those live outside any single project and follow you across every repository you work in.

The ecosystem

The Agent Skills world has three distinct layers. Knowing which is which helps you find what you need.

Layer What it is Where
Format / spec The open SKILL.md standard agentskills.io, anthropics/skills
Discovery A directory of published skills to browse skills.sh
Installers CLIs that fetch and install skills skills (.NET), npx skills (Node)

The skills CLI brings the npx skills workflow to the .NET SDK via dnx, so you can run it without a global install. The command surface deliberately mirrors what Node developers already know. The CLI has no registry of its own: you point dnx skills add at any git repository or local folder, and you discover skills to install from directories like skills.sh.

Next steps

  • Get started: run skills and add your first skill end to end.
  • Author a skill: scaffold a SKILL.md, write a strong description, and publish it.

Website Content Docs Strawberryshake Index (`website/content/docs/strawberryshake/index.md`)


title: Strawberry Shake metaTitle: "Strawberry Shake: GraphQL Client for .NET"

Strawberry Shake is an open-source GraphQL client for .NET. It generates strongly typed C# client code from your GraphQL operations and provides reactive APIs for working with local and remote data. It works with spec-compliant servers including Hot Chocolate, Apollo Server, and GraphQL Java.

You can use Strawberry Shake to:

  • Generate a C# .NET client from your GraphQL queries.
  • Interact with local and remote data through GraphQL.
  • Use reactive APIs to interact with your state.

Follow the getting-started guide to configure a client and run your first operation.


Website Content Docs Hotchocolate Defining A Schema Index (`website/content/docs/hotchocolate/defining-a-schema/index.md`)


title: "Type System"

A GraphQL schema defines the contract that clients interact with. It specifies the available operations, selectable fields, accepted arguments, and the structure of responses.

Hot Chocolate enables you to define this contract in C# and review the resulting GraphQL SDL. This page serves as your guide: it introduces the main type system concepts, demonstrates how a C# model translates to SDL, and directs you to detailed pages for each modeling task.

Understanding the Schema as the API Contract

When working with GraphQL, everything starts with the schema. The schema defines the contract between your API and its clients, describing the available operations and the data that can be requested.

Clients interact with your API by sending GraphQL operations. They do not call your C# methods or classes directly. Instead, they rely on the schema to understand what is possible.

To introduce the main concepts of the GraphQL type system, consider the following example. This schema highlights the essential building blocks you will encounter:

graphql
type Query {
  bookById(id: ID!): Book
}

type Mutation {
  createBook(input: CreateBookInput!): Book!
}

type Book {
  id: ID!
  title: String!
  authors: [Author!]!
}

type Author {
  id: ID!
  name: String!
}

input CreateBookInput {
  title: String!
  authorIds: [ID!]!
}

This example presents queries, mutations, object types, input types, and scalar values. Each part of the schema plays a specific role in shaping how clients interact with your API. The table below breaks down these elements and explains their purpose within the type system.

SDL part Type system member What it means Learn more
Query Operation root type Entry point for read operations. Queries
Mutation Operation root type Entry point for write operations. Mutations
bookById and createBook Fields Selectable members on a type. Object Types
id and input Arguments Values supplied to a field. Arguments
Book and Author Object types Returned data shapes. Object Types
CreateBookInput Input object type Structured data sent by a client. Input Object Types
ID and String Scalars Leaf values with no subfields. Scalars
! and [] Type modifiers Non-null and list wrappers. Lists and Non-Null

If you are unsure how to identify the parts of your schema, start by looking at the SDL. Elements that a client can select in a query are fields. Values that a client supplies to those fields are arguments or input fields. When you see ! or [] wrapping another type, these are type modifiers that indicate non-nullability or lists.

For example, a client might send the following query and mutation:

graphql
query {
  bookById(id: "1") {
    title
    authors {
      name
    }
  }
}

mutation {
  createBook(input: { title: "New Book", authorIds: ["2"] }) {
    id
    title
  }
}

In these examples, bookById and createBook are fields. The id and input values are arguments. The selections inside the curly braces, such as title and authors, are fields on the returned types.

Schema Authoring Styles

Hot Chocolate offers two C# authoring styles, both of which produce GraphQL SDL.

Implementation-First

The implementation-first approach allows you to define your GraphQL schema using standard C# types and attributes. This keeps your contract close to your domain code with minimal ceremony, provides compile-time feedback, and ensures a clear mapping between code and schema. This workflow is streamlined for most application schemas.

Common building blocks include:

  • [QueryType], [MutationType], and [SubscriptionType] for operation root fields
  • partial source-generator classes for annotated root type classes
  • Attributes such as [GraphQLName], [GraphQLDescription], [GraphQLIgnore], [ID], [Node], [InterfaceType], [UnionType], and [OneOf] when the inferred schema needs guidance
  • Generated dependency injection modules for GraphQL types

Code-First

The code-first approach allows you to define GraphQL types and their structure directly in C# using the Hot Chocolate type descriptor API. This is useful when your GraphQL schema needs to differ from your C# model or when building reusable schema components.

csharp
using HotChocolate.Types;

public sealed class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor
            .Field(t => t.Title)
            .Type<NonNullType<StringType>>()
            .Description("The title displayed to readers.");
    }
}

The following map helps you select the next detailed page without needing to learn every rule here.

Root Types

GraphQL defines a single root type for each operation kind. In C#, you can organize root fields across multiple semantic classes, and Hot Chocolate will merge them into the final root type.

Element Purpose Authoring cue Learn more
Query Read entry point. Query fields should be side-effect-free and may execute in parallel. [QueryType] or AddQueryType. Queries
Mutation Write entry point. Top-level mutation fields execute serially. [MutationType] or AddMutationType. Mutations
Subscription Event stream entry point. [SubscriptionType] or AddSubscriptionType. Subscriptions

Output Types

Output types describe the shapes of data that clients can select after a field resolves.

Element Use it for Learn more
Object type Normal returned data shapes, such as Book or Author. Object Types
Scalar Leaf values such as String, Int, Boolean, ID, UUID, URI, DateTime, Any, and custom scalars. Scalars
Enum A closed set of symbolic values in input or output positions. Enums
Interface Polymorphic output types that share fields. Interfaces
Union Polymorphic output types that do not need shared fields. Unions

Fields and Arguments

Fields and arguments define how clients interact with your schema. Fields are selectable members on types, while arguments allow clients to supply values to those fields.

Element Purpose Learn more
Field A selectable member on a root type or object type. Root fields start operations. Nested fields shape returned data. Queries, Mutations, Object Types
Argument A value supplied to a field. Common uses include lookup IDs, filters, paging arguments, and mutation payloads. Arguments

Not every C# method parameter becomes an argument. Service parameters, parent values, cancellation tokens, and resolver context parameters are resolver concerns.

Input Types

Input types define the shapes of data that clients can send to your API, such as arguments and input objects for mutations and queries.

Element Use it for Learn more
Argument A scalar, enum, ID, list, or input object supplied to a field. Arguments
Input object type Structured payloads for mutations, filters, and other complex field inputs. Input Object Types

GraphQL separates input and output type systems. Input objects can use defaults, Optional<T>, and @oneOf, but those rules are detailed on the input pages.

Type Modifiers

Type modifiers such as non-null and list indicate whether a field or argument is required or can accept multiple values.

Modifier Example What it says Learn more
Non-null String! The field or argument must not be null in the contract. Lists and Non-Null
List [Book] The value is a collection. Lists and Non-Null
List plus item non-null [Book!]! The list is required, and every item is required. Lists and Non-Null

Input optionality, default values, and output nullability are related but not identical. Refer to the type modifier and input object pages when the distinction matters.

Schema Organization and Advanced Modeling

Organize your schema for maintainability and support advanced modeling scenarios using type extensions, Relay helpers, and dynamic schemas.

Topic Use it for Learn more
Type extensions Split large object or root type definitions across classes. Extensions are merged into the final schema. Object Types
Relay helpers Use stable IDs, global object identification, node, nodes, [ID], [Node], and [NodeResolver]. Relay
Dynamic schemas Generate types from CMS, multi-tenant, or configuration-driven metadata with ITypeModule. Dynamic Schemas

Contract Metadata and Lifecycle

Metadata and lifecycle features help you document, annotate, and evolve your schema safely over time.

Topic Use it for Learn more
Descriptions Add schema documentation from XML comments, [GraphQLDescription], or descriptors. Documentation Comments
Directives Add schema metadata or executable behavior, depending on the directive kind. Directives
Deprecation and evolution Communicate lifecycle changes and plan compatible schema updates. Versioning

Use type extensions for static modularity. Choose dynamic schemas only when the schema must change based on external metadata or runtime configuration.

Next steps


Website Content Docs Hotchocolate Fetching Data Index (`website/content/docs/hotchocolate/fetching-data/index.md`)


title: Fetching Data

Hot Chocolate provides data middleware that applies common operations directly to your IQueryable or IExecutable data sources. Instead of implementing pagination, filtering, sorting, and projections by hand, you declare them on your fields and Hot Chocolate generates the corresponding GraphQL types and applies the operations at execution time.

Pagination

Hot Chocolate provides cursor-based connection pagination out of the box. Connections follow the Relay Cursor Connections Specification, giving clients a standardized way to page through large datasets. When backed by IQueryable, pagination translates directly to native database queries.

Learn more about pagination

Filtering

When you return a list of entities, clients often need to filter them by operations like equals, contains, or startsWith. Hot Chocolate generates the necessary filter input types from your .NET models and translates applied filters into native database queries.

Learn more about filtering

Sorting

Hot Chocolate generates sort input types from your .NET models, allowing clients to specify which fields to sort by and in which direction. Like filtering, sort operations translate to native database queries when backed by IQueryable.

Learn more about sorting

Projections

Projections optimize database queries by selecting only the columns that match the fields requested in the GraphQL query. If a client requests name and id, Hot Chocolate queries only those columns from the database.

Learn more about projections

Batching

DataLoaders and batch resolvers solve the N+1 problem in GraphQL. When the execution engine resolves a list of objects and each needs related data, a DataLoader collects all individual requests and sends a single query for all keys at once.

  • DataLoader for key-based batching with deduplication and caching.
  • Batch Resolvers for simpler cases where caching is not needed.

Integrations

Hot Chocolate is not bound to a specific database. The data middleware works with any IQueryable provider. We provide specific guidance for the most common data sources:


Website Content Docs Hotchocolate Performance Index (`website/content/docs/hotchocolate/performance/index.md`)


title: Performance

This section covers ways to improve the performance of your Hot Chocolate GraphQL server.

Startup Performance

Hot Chocolate constructs the schema eagerly at startup by default. You can register warmup tasks to pre-populate in-memory caches before the server begins accepting requests.

Learn more about server warmup

Trusted Documents

The size of individual GraphQL requests can become a significant bottleneck. This affects both the transport layer and the server, since large requests need to be parsed and validated repeatedly. Hot Chocolate supports persisted operations (also known as trusted documents) to address this. With persisted operations, you store operation documents on the server in a key-value store. When executing a persisted operation, the client sends the key under which the operation document is stored instead of the full document. This saves bandwidth and improves execution time because the server validates, parses, and compiles persisted operation documents only once.

Hot Chocolate supports two flavors of persisted operations.

Regular Persisted Operations

The first approach stores operation documents ahead of time (before deployment). You extract the operations from your client applications at build time. This reduces the size of requests and the bundle size of your application because operations can be removed from the client code at build time and replaced with operation document hashes.

Strawberry Shake, Relay, and Apollo client all support this approach.

Learn more about persisted operations

Automatic Persisted Operations

Automatic persisted operations let you store operation documents dynamically on the server at runtime. This approach gives your applications the same performance benefits as regular persisted operations without requiring a more complex build process.

However, you do not get bundle size improvements for your applications because the operations are still needed at runtime.

Both Strawberry Shake and Apollo client support this approach.

Learn more about automatic persisted operations


Website Content Docs Hotchocolate Resolvers Index (`website/content/docs/hotchocolate/resolvers/index.md`)


title: Resolvers metaTitle: "GraphQL Resolvers in Hot Chocolate"

Every field in a GraphQL schema is backed by a resolver function that produces the field's value. Understanding how resolvers compose into a tree is the key mental model for building efficient GraphQL APIs with Hot Chocolate.

The Resolver Tree

When Hot Chocolate receives a query, it builds a resolver tree that mirrors the shape of the request. Consider this query:

graphql
query {
  me {
    name
    company {
      id
      name
    }
  }
}

This produces the following resolver tree:

mermaid
graph LR
  A(query: QueryType) --> B(me: UserType)
  B --> C(name: StringType)
  B --> D(company: CompanyType)
  D --> E(id: IdType)
  D --> F(name: StringType)

The execution engine traverses this tree starting from root resolvers. A child resolver can only execute after its parent has produced a value. Sibling resolvers at the same level run in parallel. Because of this parallel execution, resolvers (except top-level mutation field resolvers) must be free of side effects.

Execution completes when every resolver in the tree has produced a result.

Defining a Resolver

Resolvers can be defined in a way that should feel very familiar to C# developers, as they either translate to methods or delegates.

In the implementation-first approach, a public method is automatically inferred as a resolver. This means the method defines both the field in your schema and the logic to resolve its value.

csharp
[QueryType]
public partial class Query
{
    public static string Foo() => "Bar";
}

This generates the following schema:

sdl
type Query {
  foo: String!
}

Resolvers do not have to be methods. Public properties are also inferred as resolvers and exposed as fields in your schema.

csharp
[QueryType]
public partial class Query
{
    public static User User => new User("Ted");
}

public record User(string Name);

In this case, the property Name of the User object is also inferred as a resolver.

In the code-first approach, you define a resolver by assigning a resolver delegate to a field. This delegate contains the logic for resolving the field's value.

csharp
public class QueryType : ObjectType
{
    protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
    {
        descriptor
            .Field("foo")
            .Type<NonNullType<StringType>>()
            .Resolve(ctx => "bar");
    }
}

You can also use ObjectType<T> with a backing POCO. Public methods and properties on the POCO are bound as fields automatically. Use the Field method with a lambda expression to configure individual fields.

csharp
public class Query
{
    public string Foo() => "Bar";
}

public class QueryType : ObjectType<Query>
{
    protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
    {
        descriptor
            .Field(f => f.Foo())
            .Type<NonNullType<StringType>>();
    }
}

Async Resolver

Resolvers can be synchronous or asynchronous. Most data fetching operations, such as calling a service or database, are asynchronous.

The most important aspect of async resolvers is to honor the CancellationToken. This allows execution to be cancelled if the client abandons the request, preventing unnecessary work and resource usage.

When using the implementation-first approach, you can add a CancellationToken parameter to your resolver method. The execution engine will automatically inject the request's cancellation token.

csharp
public class Query
{
    public async Task<Product> GetProductByIdAsync(
      int id,
      ProductService productService,
      CancellationToken cancellationToken)
      => await productService.GetAsync(cancellationToken);
}

When using the code-first approach, you can access the CancellationToken through the IResolverContext provided to your resolver.

csharp
descriptor
    .Field("foo")
    .Resolve(context =>
    {
        CancellationToken ct = context.RequestAborted;

        // Omitted code for brevity
    });

Arguments

In GraphQL, fields are conceptually similar to methods in C#. Just like methods, fields can have arguments, and you can access these argument values directly in your resolvers.

When using the implementation-first approach, any parameter in your resolver method that is not a service, a CancellationToken, or specially annotated is treated as a GraphQL argument. The execution engine will inject the argument value from the query into these parameters. For example, in the method below, the id parameter is recognized as an argument, while ProductService is injected as a service from the DI container.

csharp
public class Query
{
    public async Task<Product> GetProductByIdAsync(
      int id,
      ProductService productService,
      CancellationToken cancellationToken)
      => await productService.GetAsync(cancellationToken);
}

When using the code-first approach, you can access field arguments using the resolver context.

csharp
descriptor
    .Field("foo")
    .Argument("id", a => a.Type<NonNullType<IntType>>())
    .Resolve(context =>
    {
        var id = context.ArgumentValue<int>("id");

        // Omitted code for brevity
    });

Learn more about arguments

Injecting Services

Hot Chocolate automatically recognizes types registered in the DI container and injects them into resolver parameters.

csharp
public class Query
{
    public List<User> GetUsers(UserService userService)
        => userService.GetUsers();
}

While you can take attributes to annotate services, you do not have to for non-keyed services.

csharp
public class Query
{
    public List<User> GetUsers([Service] UserService userService)
        => userService.GetUsers();
}

Learn more about dependency injection

Accessing parent values

Each field resolver has access to the value that was resolved for its parent type.

For example, consider the following schema:

sdl
type Query {
  me: User!;
}

type User {
  id: ID!;
  friends: [User!]!;
}

The User schema type is represented by a User runtime class. The id field is a property on this class.

csharp
public class User
{
    public string Id { get; set; }
}

The friends resolver, by contrast, is independent: it is not declared on the User type and uses the user's Id to compute its result.
From the friends resolver's perspective, the User runtime object is its parent.

Access the parent value like this:

In the implementation-first approach, the parent object can be injected as a resolver parameter:

csharp
[ObjectType<User>]
public static partial class UserNode
{
    public static Task<List<User>> GetFriendsAsync(
        [Parent] User user,
        UserService userService,
        CancellationToken cancellationToken)
    {
        // Omitted code for brevity
    }
}

If database projections are enabled, the parent object may only contain the fields requested by the client. To ensure the projections engine also loads properties required by the resolver, declare those requirements on the parent parameter:

csharp
[ObjectType<User>]
public static partial class UserNode
{
    public static Task<List<User>> GetFriendsAsync(
        [Parent(requires: nameof(User.Id))] User user,
        UserService userService,
        CancellationToken cancellationToken)
    {
        // Omitted code for brevity
    }
}

Use nameof to make this requirement refactoring-safe.

In the code-first approach, the parent object is available via the IResolverContext.

csharp
public class User
{
    public string Id { get; set; }
}

public class UserType : ObjectType<User>
{
    protected override void Configure(IObjectTypeDescriptor<User> descriptor)
    {
        descriptor
            .Field("friends")
            .Resolve(context =>
            {
                User parent = context.Parent<User>();

                // Omitted code for brevity
            });
    }
}

What's in This Section

  • Dependency Injection explains how services are injected into resolvers, scoping behavior, keyed services, and switching the service provider.
  • Errors covers how exceptions become GraphQL errors, error filters for mapping domain exceptions, and throwing GraphQLException for explicit errors.
  • Field Middleware shows how to build reusable middleware that runs before or after resolvers, including ordering, class-based middleware, and attribute-based middleware.

Website Content Docs Hotchocolate Security Index (`website/content/docs/hotchocolate/security/index.md`)


title: Securing Your API

Securing a GraphQL API requires more than authentication and authorization. Unlike REST, where each endpoint has a predictable cost, a single GraphQL query can traverse deep relationships and request large datasets. You need a strategy that matches your API's threat model.

Hot Chocolate provides two golden paths depending on whether your API is public or private.

Public APIs: Cost Analysis

Public APIs face unpredictable clients. You do not control who sends queries or how complex those queries are. An attacker can craft a deeply nested query that consumes significant server resources.

Cost analysis is your primary defense for public APIs. It compiles each operation into a cost plan and evaluates that plan with the request's coerced variables. Queries that exceed a configured field-cost, type-cost, or response-size limit are rejected before execution.

Combine cost analysis with:

  • Pagination limits to cap the number of items returned per connection.
  • Execution depth limits to prevent deeply nested queries.
  • Execution timeouts to abort long-running queries.

Learn more about cost analysis

Private APIs: Trusted Documents

Private APIs serve known clients that you control, such as your own web or mobile applications. For these APIs, trusted documents (also called persisted operations) provide the strongest security guarantee.

With trusted documents, you extract all GraphQL operations from your client at build time and register them with the server. At runtime, the server only accepts operations it recognizes. This eliminates the risk of arbitrary queries entirely.

Learn more about trusted documents

Defense in Depth

Regardless of whether your API is public or private, apply these additional protections:

Authentication

Authentication determines who is making a request. Hot Chocolate integrates with the ASP.NET Core authentication system, supporting JWT, cookies, and other authentication schemes.

Learn more about authentication

Authorization

Authorization controls what an authenticated user can access. Hot Chocolate provides the @authorize directive for field-level and type-level access control, integrating with ASP.NET Core roles and policies.

Learn more about authorization

Request Limits

Hot Chocolate enforces limits during parsing, validation, and execution. These include limits on fields, directives, nesting depth, execution depth, and execution time.

Learn more about request limits

Introspection

Introspection powers developer tools but can also reveal your schema to attackers. You can restrict or disable introspection in production.

Learn more about introspection

FIPS Compliance

Hot Chocolate uses MD5 for document hashing by default. If you need FIPS compliance, switch to SHA256:

csharp
builder
    .AddGraphQL()
    .AddSha256DocumentHashProvider();

Learn more about hashing providers

Next Steps


Website Content Docs Hotchocolate Server Index (`website/content/docs/hotchocolate/server/index.md`)


title: Server

This section covers how you configure and operate a Hot Chocolate GraphQL server. You will find details on transport protocols, middleware, dependency injection, and runtime behavior.

Endpoints

Hot Chocolate provides ASP.NET Core endpoint middleware for accepting HTTP and WebSocket GraphQL requests, downloading the schema, and serving the Nitro GraphQL IDE.

Learn more about endpoints

HTTP Transport

Hot Chocolate implements the GraphQL over HTTP specification. The default incremental delivery format is v0.2, and clients can select the format through the Accept header.

Learn more about the HTTP transport

Cache Control

Cache control lets your GraphQL server emit Cache-Control and Vary response headers that CDNs, reverse proxies, and browsers can use for HTTP caching decisions.

Learn more about cache control

Interceptors

Interceptors let you intercept GraphQL requests before execution. There are interceptors for both HTTP requests and WebSocket sessions.

For WebSockets, the interceptor also handles lifecycle events such as when a client first connects.

Learn more about interceptors

Dependency Injection

Hot Chocolate recognizes services registered in your DI container and injects them into resolvers automatically. Services are resolved implicitly without requiring the [Service] attribute.

Learn more about dependency injection

Warmup

Hot Chocolate constructs the schema eagerly at startup. You can go further by registering warmup tasks that pre-populate caches before the server begins accepting traffic.

Learn more about warmup

Global State

Global State lets you define properties on a per-request basis and makes them available to all resolvers and middleware.

Learn more about global state

Introspection

Introspection lets you query the type system of your GraphQL server using regular GraphQL queries. While this powers developer tooling, it can also be an attack vector. You can control who is allowed to issue introspection queries.

Learn more about introspection

Files

Hot Chocolate provides file upload support through the Upload scalar, even though file handling is not traditionally a GraphQL server concern. You can also return presigned URLs for a hybrid approach.

Learn more about handling files

Instrumentation

Hot Chocolate exposes diagnostic events across the server, execution engine, and DataLoader layers. The built-in OpenTelemetry integration aligns with the proposed GraphQL semantic conventions.

Learn more about instrumentation

Batching

Batching lets you send and execute multiple GraphQL operations in a single request. Variable batching is enabled by default. Request batching is disabled by default and can be enabled through the AllowedBatching flags enum.

Learn more about batching

Command Line

The command-line interface lets you export your GraphQL schema from the terminal, which is useful for CI/CD pipelines.

Learn more about the command line


Website Content Docs Mocha Mediator Index (`website/content/docs/mocha/mediator/index.md`)


title: Mediator

csharp
builder.Services
    .AddMediator()
    .AddCatalog(); // source-generated from your assembly name

That registers the mediator infrastructure, discovers your handlers at compile time, and wires up the dispatch pipeline. .AddCatalog() is a source-generated extension method - it knows your handlers and message types at compile time and produces direct dispatch code with no reflection.

What the mediator is

The mediator sits between your application code and your handlers. Instead of injecting handler interfaces directly, you inject IMediator (or ISender / IPublisher) and dispatch messages through it. The mediator routes each message to the correct handler based on its type.

csharp
// Without mediator - tight coupling
app.MapPost("/orders", async (PlaceOrderCommandHandler handler) =>
    await handler.HandleAsync(new PlaceOrderCommand(...)));

// With mediator - decoupled dispatch
app.MapPost("/orders", async (ISender sender) =>
    await sender.SendAsync(new PlaceOrderCommand(...)));

The mediator provides three things your handlers cannot do alone: a middleware pipeline that wraps every handler invocation with cross-cutting concerns (logging, transactions, validation), polymorphic dispatch that routes messages by type at runtime, and a seam between your application layer and your domain logic. This is the Mediator pattern -- objects communicate through a central hub instead of referencing each other directly.

If you have used MediatR, the concepts are familiar. Mocha Mediator takes a different approach to performance: a Roslyn source generator analyzes your handler registrations at compile time and produces pre-compiled pipeline delegates. No MakeGenericType, no service provider lookups to resolve the pipeline, no reflection at runtime.

When to use the mediator vs. the message bus

Mocha has two dispatch mechanisms. Use the right one for the situation:

Use the mediator when... Use the message bus when...
Dispatch stays in-process Messages cross process or service boundaries
You want CQRS separation of commands and queries You want pub/sub events across services
You need a request/response pipeline with middleware You need transport-level features (retries, outbox)
Handlers live in the same assembly or solution Handlers live in different services

The mediator and the message bus complement each other. A common pattern is to use the mediator for in-process CQRS dispatch within a service, and the message bus for inter-service event-driven communication.

Messages

Messages are plain C# types that implement a marker interface. The marker interface tells the mediator how to route the message and what return type to expect.

Commands

Commands represent actions that change state. Use imperative verb-noun naming: PlaceOrder, ProcessPayment.

csharp
// A command that returns no response
public record DeleteOrderCommand(Guid OrderId) : ICommand;

// A command that returns a response
public record PlaceOrderCommand(
    Guid ProductId,
    int Quantity,
    string CustomerId) : ICommand<PlaceOrderResult>;

public record PlaceOrderResult(bool Success, Guid? OrderId = null, string? Error = null);

Queries

Queries represent read operations that return data without side effects. Use noun-based naming: GetProducts, GetOrderById.

csharp
public record GetProductsQuery : IQuery<List<Product>>;

public record GetProductByIdQuery(Guid Id) : IQuery<Product?>;

Notifications

Notifications represent events that multiple handlers can observe. Use past-tense naming: OrderPlaced, PaymentCompleted.

csharp
public record OrderPlacedNotification(
    Guid OrderId,
    decimal Amount) : INotification;

Message type reference

Interface Purpose Dispatch method Return type
ICommand Action, no response SendAsync ValueTask
ICommand Action with response SendAsync ValueTask
IQuery Read operation QueryAsync ValueTask
INotification Multi-handler event PublishAsync ValueTask

Handlers

Each message type has a corresponding handler interface. The mediator routes each message to exactly one handler - except notifications, which fan out to all registered handlers.

Command handlers

csharp
// Handles a void command
public sealed class DeleteOrderCommandHandler(AppDbContext db)
    : ICommandHandler<DeleteOrderCommand>
{
    public async ValueTask HandleAsync(
        DeleteOrderCommand command, CancellationToken cancellationToken)
    {
        var order = await db.Orders.FindAsync(command.OrderId);
        if (order is not null)
        {
            db.Orders.Remove(order);
            await db.SaveChangesAsync(cancellationToken);
        }
    }
}

// Handles a command with a response
public sealed class PlaceOrderCommandHandler(AppDbContext db)
    : ICommandHandler<PlaceOrderCommand, PlaceOrderResult>
{
    public async ValueTask<PlaceOrderResult> HandleAsync(
        PlaceOrderCommand command, CancellationToken cancellationToken)
    {
        var order = new Order
        {
            Id = Guid.NewGuid(),
            ProductId = command.ProductId,
            Quantity = command.Quantity,
            CustomerId = command.CustomerId
        };

        db.Orders.Add(order);
        await db.SaveChangesAsync(cancellationToken);

        return new PlaceOrderResult(true, order.Id);
    }
}

Query handlers

csharp
public sealed class GetProductsQueryHandler(AppDbContext db)
    : IQueryHandler<GetProductsQuery, List<Product>>
{
    public async ValueTask<List<Product>> HandleAsync(
        GetProductsQuery query, CancellationToken cancellationToken)
        => await db.Products.ToListAsync(cancellationToken);
}

Notification handlers

Multiple handlers can subscribe to the same notification type. The mediator invokes all of them.

csharp
public sealed class SendOrderConfirmationEmail(IEmailService email)
    : INotificationHandler<OrderPlacedNotification>
{
    public async ValueTask HandleAsync(
        OrderPlacedNotification notification, CancellationToken cancellationToken)
    {
        await email.SendAsync(
            $"Order {notification.OrderId} confirmed", cancellationToken);
    }
}

public sealed class UpdateAnalyticsDashboard(IAnalytics analytics)
    : INotificationHandler<OrderPlacedNotification>
{
    public async ValueTask HandleAsync(
        OrderPlacedNotification notification, CancellationToken cancellationToken)
    {
        await analytics.RecordOrderAsync(
            notification.OrderId, notification.Amount);
    }
}

Handler interface reference

Interface Message type Response
ICommandHandler<TCommand> ICommand void
ICommandHandler<TCommand, TResponse> ICommand TResponse
IQueryHandler<TQuery, TResponse> IQuery TResponse
INotificationHandler<TNotification> INotification void

Dispatching messages

Inject IMediator, ISender, or IPublisher from DI and call the appropriate method.

csharp
// Send a command with a response
app.MapPost("/orders", async (PlaceOrderRequest request, ISender sender) =>
{
    var result = await sender.SendAsync(
        new PlaceOrderCommand(request.ProductId, request.Quantity, request.CustomerId));

    return result.Success
        ? Results.Created($"/api/orders/{result.OrderId}", result)
        : Results.BadRequest(result.Error);
});

// Send a query
app.MapGet("/products", async (ISender sender) =>
    await sender.QueryAsync(new GetProductsQuery()));

// Publish a notification
app.MapPost("/orders/{id}/ship", async (Guid id, IPublisher publisher) =>
{
    await publisher.PublishAsync(new OrderShippedNotification(id));
    return Results.Ok();
});

ISender handles commands and queries. IPublisher handles notifications. IMediator combines both interfaces - inject it when you need both in the same class.

Untyped dispatch

When the message type is not known at compile time, use the object-based overloads:

csharp
// Dispatch a command or query by runtime type
object message = GetMessageFromSomewhere();
object? result = await sender.SendAsync(message);

// Dispatch a notification by runtime type
object notification = GetNotificationFromSomewhere();
await publisher.PublishAsync(notification);

The runtime type of the message must implement one of the marker interfaces (ICommand, ICommand, IQuery, or INotification). An exception is thrown if it does not.

Registration and source generation

Register the mediator

csharp
builder.Services
    .AddMediator()
    .AddCatalog(); // source-generated from assembly name "Demo.Catalog"

AddMediator() registers the core mediator infrastructure and the default notification publish mode. The source-generated Add{ModuleName}() method registers:

  • All command, query, and notification handlers found in your assembly
  • Pre-compiled terminal delegates for each message type (no reflection at runtime)

You do not register handlers manually unless you need to. The source generator discovers them by scanning for classes that implement handler interfaces.

Module naming

The source generator names the registration method based on your assembly:

  1. If you apply #!csharp [assembly: MediatorModule("Billing")], the method is AddBilling()
  2. Otherwise, it uses the last segment of the assembly name: Demo.Catalog produces AddCatalog()

To set an explicit module name, add the attribute to any file in your project:

csharp
using Mocha.Mediator;

[assembly: MediatorModule("Billing")]

Manual handler registration with AddHandler

When you need to register handlers outside the source generator's reach - from a plugin assembly, a dynamically loaded module, or in integration tests - use AddHandler<T>():

csharp
builder.Services
    .AddMediator()
    .AddHandler<PlaceOrderCommandHandler>()
    .AddHandler<GetProductsQueryHandler>()
    .AddHandler<OrderShippedEmailHandler>();

AddHandler<T>() inspects the type for handler interfaces (ICommandHandler, IQueryHandler, INotificationHandler), builds the pipeline configuration, and registers the handler in DI. It throws InvalidOperationException if T does not implement a handler interface.

You can mix source-generated and manual registration. If both register the same handler type, the configurations are composed:

csharp
builder.Services
    .AddMediator()
    .AddCatalog()                              // source-generated handlers
    .AddHandler<ExternalPaymentHandler>();      // additional handler from another assembly

Prefer the source generator. The source-generated registration path uses pre-compiled terminal delegates with no reflection. The source-generated output is designed for long-term stability across versions. Manual registration is available for edge cases but its internal behavior may change between releases.

Configure service lifetime

By default, handlers are registered as Scoped. To change the default:

csharp
builder.Services
    .AddMediator()
    .ConfigureOptions(options =>
    {
        options.ServiceLifetime = ServiceLifetime.Transient;
    })
    .AddCatalog();

Call ConfigureOptions before Add{ModuleName}() so the source-generated method reads the updated lifetime.

Configuration options reference

Option Type Default Description
ServiceLifetime ServiceLifetime Scoped Default DI lifetime for handler registrations
NotificationPublishMode NotificationPublishMode Sequential How notification handlers are dispatched

Named mediators

To run multiple independent mediator instances (each with its own handlers and middleware), use named mediators. Named mediators use .NET's keyed dependency injection.

csharp
// Register a named mediator
builder.Services
    .AddMediator("billing")
    .AddBilling();

// Register the default (unnamed) mediator
builder.Services
    .AddMediator()
    .AddCatalog();

Resolve a named mediator from DI:

csharp
app.MapPost("/payments", async (
    [FromKeyedServices("billing")] ISender sender,
    ProcessPaymentRequest request) =>
{
    var result = await sender.SendAsync(
        new ProcessPaymentCommand(request.Amount));
    return Results.Ok(result);
});

// Or resolve from IServiceProvider directly
var billingMediator = serviceProvider
    .GetRequiredKeyedService<IMediator>("billing");

Each named mediator has its own handler registrations, middleware pipeline, and runtime. The default mediator (registered with AddMediator() without a name) is resolved normally without keyed services.

Putting it together

Here is a complete minimal API application with commands, queries, and notifications:

text
/* Detailed source-code truncated for AI context efficiency. */

If everything worked, dotnet run starts the server and you can:

  • POST /orders with a JSON body to place an order
  • GET /products to list products
  • #!http POST /orders/{id}/ship to publish a shipped notification

Troubleshooting

`InvalidOperationException: No pipeline registered for message type`

The source generator did not find a handler for your message type. Verify:

  • Your handler class implements the correct interface (e.g., ICommandHandler<YourCommand, YourResponse>)
  • Your message type implements the correct marker interface (e.g., ICommand<YourResponse>)
  • You called the source-generated .Add{ModuleName}() method on the mediator builder
  • The handler is in the same project that the source generator can see
  • The project references Mocha.Analyzers as an analyzer (not a regular project reference)

Handlers are not being called

If dispatch succeeds but your handler code does not execute, check that:

  • Your middleware calls the next delegate - a middleware that forgets to call next silently short-circuits the pipeline
  • You are not accidentally registering handlers manually in addition to the source-generated method, which could result in duplicate registrations

The source-generated method does not appear

If IntelliSense does not show Add{ModuleName}():

  • Confirm the Mocha.Analyzers package is referenced with OutputItemType="Analyzer" in your .csproj
  • Rebuild the project - source generators run during compilation
  • Check the build output for analyzer warnings prefixed with MO

`InvalidOperationException` when calling `AddHandler()`

The type you passed does not implement any handler interface. Make sure T implements one of: ICommandHandler<TCommand>, ICommandHandler<TCommand, TResponse>, IQueryHandler<TQuery, TResponse>, or INotificationHandler<TNotification>.

Named mediator returns wrong handlers

Each named mediator resolves handlers from the same DI container. Make sure you register each module's handlers on the correct IMediatorHostBuilder instance - the one returned by the AddMediator("name") call for that name.

Next steps

You have a working mediator with CQRS dispatch. Here is where to go next:

  • Customize the pipeline: Pipeline & Middleware - add validation, logging, transactions, and other cross-cutting concerns. Configure notification publish modes and OpenTelemetry instrumentation.

Website Content Docs Mocha Transports Index (`website/content/docs/mocha/transports/index.md`)


title: "Transports"

A transport is the infrastructure layer that connects Mocha to a message broker. It manages connections, provisions topology (exchanges, queues, bindings), and handles the low-level details of dispatching and receiving messages. You write handlers and publish messages. The transport handles the rest.

The transport abstraction means your handlers, patterns, and pipeline are identical regardless of which broker you use. Only the infrastructure changes. Swap .AddInMemory() for .AddPostgres(), .AddRabbitMQ(), or .AddAzureServiceBus() and your application code stays unchanged. This portability is the core value of the Message Channel pattern: the sender and receiver are decoupled from the physical infrastructure that carries the message.

Mocha ships with four transports:

Transport Package Use case
InMemory Mocha.Transport.InMemory Development, testing, single-process scenarios
PostgreSQL Mocha.Transport.Postgres Database-backed messaging when you already operate Postgres
RabbitMQ Mocha.Transport.RabbitMQ Production, distributed systems, multi-service architectures
Azure Service Bus Mocha.Transport.AzureServiceBus Managed cloud messaging on Azure

Add a transport

Every transport implements the same MessagingTransport base class. The transport is always the last call in the builder chain:

csharp
// InMemory - zero configuration
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddInMemory();
csharp
// RabbitMQ - production-ready
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddRabbitMQ();
csharp
// PostgreSQL - database-backed transport
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddPostgres("Host=localhost;Database=mocha_messaging;Username=postgres;Password=postgres");
csharp
// Azure Service Bus - managed cloud messaging
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddAzureServiceBus(connectionString);

Each Add{Transport}() method registers a transport instance, applies default conventions, and wires up the middleware pipelines.

Choose a transport

Use this decision matrix to pick the right transport. Each column includes trade-offs - choose the one whose trade-offs you can accept:

Criterion InMemory PostgreSQL RabbitMQ Azure Service Bus
Setup effort None, zero dependencies Requires a PostgreSQL database Requires a running broker Azure subscription and namespace
Message durability Messages lost on process exit Messages are stored in database tables Messages survive broker restarts Durable, broker-managed
Multi-process Single process only Multiple services sharing the same database Multiple services, multiple instances Multiple services, multiple instances
Request/reply Supported Supported Supported Supported
Native scheduling Yes, built-in and cancellable (non-durable) Yes, built-in and cancellable None, requires UsePostgresScheduling() Yes, durable and cancellable
Operational cost None Database capacity, migrations, monitoring Broker infrastructure, monitoring, upgrades Pay-per-use Azure resource
Network latency None, in-process Database round trip Broker round trip Cloud network round trip

InMemory limitations: Because all messages live in process memory, the InMemory transport cannot model multi-service fan-out, cannot survive process restarts, and does not exercise RabbitMQ-specific behavior like connection recovery, acknowledgement semantics, or topology conflicts.

PostgreSQL trade-offs: PostgreSQL is a good fit when you already operate PostgreSQL and want database-backed messaging without another broker. It favors operational simplicity and transactional consistency over dedicated broker throughput.

RabbitMQ operational cost: RabbitMQ requires expertise to operate in production - cluster management, disk and memory alarms, queue type selection, and monitoring. Use a managed broker (CloudAMQP, Amazon MQ) if you want to reduce operational burden.

Azure Service Bus trade-offs: Azure Service Bus is fully managed and durable, with native scheduling and cancellation. It is a strong fit for Azure-hosted workloads, while cost and available throughput depend on the namespace SKU.

Scope and middleware

Mocha uses a three-level feature scope: bus, transport, and endpoint. Features set at the bus level apply to all transports. Features set at the transport level override bus-level defaults for that transport. Features set at the endpoint level override both.

text
Bus scope (global defaults)
  └── Transport scope (transport-specific overrides)
       └── Endpoint scope (endpoint-specific overrides)

To add middleware at the transport level:

csharp
builder.Services
    .AddMessageBus()
    .AddRabbitMQ(transport =>
    {
        // Add dispatch middleware scoped to this transport
        transport.UseDispatch(myDispatchMiddleware);

        // Add receive middleware scoped to this transport
        transport.UseReceive(myReceiveMiddleware);

        // Insert middleware relative to existing ones
        transport.UseReceive(myMiddleware, after: "ConcurrencyLimiter");
        transport.UseDispatch(myMiddleware, before: "Serialization");
    });

This scoping model lets you run different middleware configurations per transport without affecting other transports in a multi-transport setup.

Customize queues and binding

Use transport.Queue("name") when you need to customize receive topology. The queue builder is the primary surface for custom queue names, multiple handlers on one queue, queue-level settings, and handler assignment.

csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddRabbitMQ(transport =>
    {
        transport.BindExplicitly();

        transport.Queue("order-processing")
            .BindImplicitly()
            .Handler<OrderPlacedEventHandler>();
    });

Calling Queue("name") without a handler, consumer, or Receives<T>() declares only the queue. As soon as you add a handler, consumer, or received message type, Mocha materializes a receive endpoint for that queue.

Control implicit and explicit binding

By default, transports bind handlers implicitly using naming conventions:

csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddRabbitMQ(transport =>
    {
        transport.BindImplicitly(); // This is the default
    });

With implicit transport binding, registered handlers are auto-discovered, assigned to convention-named queues, and connected to convention-derived exchange, topic, or subscription bindings.

Use BindExplicitly() at the transport scope when the queues you configure should be the complete receive topology:

csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddRabbitMQ(transport =>
    {
        transport.BindExplicitly();

        transport.Queue("order-events")
            .BindImplicitly()
            .Handler<OrderPlacedEventHandler>();
    });

Use BindImplicitly() on the queue when you want a custom queue name but still want Mocha to generate the source bindings for that queue's handlers. Use BindExplicitly() on the queue when you provide those source bindings yourself, for example with BindFrom(...) or transport-specific topology declarations.

Claim handlers for a transport

When you need to keep the convention-derived queue name and only tune a single handler endpoint, use transport.Handler<T>() at the end of the transport configuration. This claims the handler for the transport and returns a descriptor that lets you configure the endpoint through ConfigureEndpoint():

csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddRabbitMQ(transport =>
    {
        transport.Handler<OrderPlacedEventHandler>()
            .ConfigureEndpoint(e => e.MaxPrefetch(50).MaxConcurrency(10));
    });

The handler still gets a convention-named endpoint - Handler<T>() does not change the name. It gives you a handle to configure that endpoint without needing BindExplicitly() or knowing the endpoint name.

For raw IConsumer types, the equivalent is transport.Consumer<T>():

csharp
transport.Consumer<OrderAuditConsumer>()
    .ConfigureEndpoint(e => e.MaxConcurrency(3));

Handler<T>() and Consumer<T>() are the primary tool for multi-transport routing. When a handler is claimed by a transport, it is bound to that transport regardless of which transport is marked as the default.

Use multiple transports

You can register multiple transports and route specific handlers to specific transports. Mark one transport as the default with .IsDefaultTransport(). Any handler not explicitly claimed by another transport is bound to the default:

csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddEventHandler<AuditHandler>()
    .AddRabbitMQ(r => r.IsDefaultTransport())       // default for unclaimed handlers
    .AddInMemory(m => m.Handler<AuditHandler>());   // AuditHandler claimed by InMemory
// OrderPlacedEventHandler → RabbitMQ (default, implicit)
// AuditHandler → InMemory (claimed)

You can also use the queue descriptor with explicit binding:

csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddEventHandler<ClickStreamHandler>()
    // Default transport for most messages
    .AddRabbitMQ()
    // High-throughput transport for click-stream data
    .AddInMemory(transport =>
    {
        transport.BindExplicitly();

        transport.Queue("click-stream")
            .BindImplicitly()
            .Handler<ClickStreamHandler>();
    });

Each transport manages its own connections, topology, and middleware pipeline independently. A handler bound to one transport does not consume from another transport's endpoints.

Next steps

Runnable example: MultiTransport


Website Content Docs Nitro Apis Overview (`website/content/docs/nitro/apis/overview.md`)


title: APIs

An API within the context of Nitro, refers to a representation of your GraphQL Servers.
This representation is more than a mere conceptual framework — it serves as a practical tool that allows you to group your documents and share common settings like connection and authorization parameters among them.

Additionally, an API forms the foundation for your client registry, schema registry setup and the telemetry. For more detailed information on these features, refer to the Schema Registry guide
the Client Registry and the Telemetry guide.

API Types

API Collection

A compilation of GraphQL Documents with shared connection settings, enabling the grouping of documents for sharing with your team.

API Service

Incorporates all features of an API Collection and adds the capability to register your schema and clients in the schema registry. It also includes the use of telemetry for service monitoring. This type is ideal for representing a single deployment service or a subgraph.

API Gateway

Encompasses all the features of the API Service, along with the ability to publish and manage fusion configuration. Additionally, it supports distributed telemetry for comprehensive monitoring of your Gateway.

Creating an API

Creating an API in Nitro is a user-friendly process. There are three methods available:

  1. Click on the New API button located at the top of the document explorer toolbar.
  2. Right-click within the document explorer and select New API from the context menu. This creates a new API within the currently selected folder. Note: APIs can be organized in folders but cannot be nested within each other.
  3. Choose the API Type. Options include API Collection, API Service, or API Gateway.

Website Content Docs Nitro Documents Index (`website/content/docs/nitro/documents/index.md`)


title: Documents

The Document View is a feature that allows users to work with documents and API documents within the application. It provides functionality for executing queries, mutations, and subscriptions. This section describes the various components and functionalities available in the Document View.

1. Operation

The Operation section is where you can write and execute your queries, mutations, and subscriptions. It provides a convenient interface for interacting with the GraphQL API.

2. Schema Reference

The Schema Reference section allows you to explore the schema in a tree view or the explorer view. It provides detailed information about the available schema and its components. For more information on using the Schema Reference, refer to the Schema Reference guide.

3. Schema Definition

The Schema Definition section displays the schema definition in SDL (Schema Definition Language) format. It allows you to inspect the schema structure and understand its various types and fields. Refer to the Schema Definition guide for further details.

4. Document Status Indicator

The Document Status Indicator visually represents whether the current document is saved or not. A white dot indicates that the document is not saved, while a different icon may indicate a saved or modified state.

5. Query Editor

The Query Editor provides a dedicated space for writing your queries, mutations, and subscriptions. It offers features such as syntax highlighting, auto-completion, and error checking to assist in query composition. Learn more about using the query editor in the Operations guide.

6. Response Pane

The Response Pane displays the response of your queries, mutations, and subscriptions. It shows the data returned by the API and provides a structured view for easier analysis. Refer to the Response Pane guide for additional information.

7. Schema Indicator

The Schema Indicator is a visual cue that indicates the connection status with the GraphQL server and the successful retrieval of the schema. A green circle represents a connected state.

8. Connected Schema Endpoint

This section displays the currently connected schema endpoint, providing information about the GraphQL server being accessed.

9. Settings

The Document Settings section allows you to configure various settings specific to the document. Refer to the Connection Settings guide for detailed information about it. You can also configure the authentication settings for the document. Refer to the Authentication Settings guide for more details.


Website Content Docs Nitro Explore The Ui Index (`website/content/docs/nitro/explore-the-ui/index.md`)


title: Explore the UI

Let's explore the main components of the Nitro user interface:

  1. Sidebar: The sidebar contains various icons representing different sections of the application. Interacting with these icons will alter the content displayed in the main area. For instance, in the current view, the highlighted icon indicates that we are in the Documents section. For detailed information, please refer to our guide on Documents.

  2. Environments: This section is for managing environments.

  3. History: This section displays the list of recently executed queries.

  4. Settings: To modify application preferences, you can navigate to the Settings section. Learn more about it in our Settings Guide.

  5. User Menu: This section allows users to sign in/out, manage their accounts, and handle billing. You can read more about account management and billing in the organization section Organizations.

  6. Document Tree: Here you can manage files, folders, documents, and APIs. Read more about it in the Explorer.

  7. Main Area: The main area showcases the content related to the selected section from the sidebar. In this instance, it displays the Documents section.

  8. Status Bar: The status bar provides quick access to workspace, organization, and synchronization features. It also displays additional information. More details are provided in our Status Bar Guide.


Website Content Docs Nitro Organizations Index (`website/content/docs/nitro/organizations/index.md`)


title: "Organizations"

This guide will walk you through the features and functionalities of Nitro Organizations, a structure designed to help teams and individuals work efficiently and effectively on GraphQL related services.

What is an Organization

An organization typically represents an entire entity or a larger unit within an entity. Each organization can host multiple workspaces, allowing further categorization and organization of resources. A workspace might correspond to a specific project, a specific team within the organization, or a group of related APIs.

Each Nitro user has a personal organization. This is a private space for individual work or for projects that are not associated with any team or company organization. The personal organization is not visible to other users, is created by default and cannot be deleted.

Creating an Organization

Nitro allows for multiple organizations under one account. To add a new organization, navigate to the organization switcher in the status bar and click on the "Add Organization" button.

You will be redirected to the management portal. Here you can create a new organization by clicking the 'Create' button.

Each organization has a unique name and a display name which can be set during creation.
The name must be lowercase and may only include dashes (-) and underscores (_).

Managing Organizations

To manage your organizations, you have to open the management portal.

In the client app, you can do this by clicking 'Manage Organizations' and then 'Manage' on the organization that you'd like to manage.

You can also navigate to here to open the management portal and press 'Manage' on the organization that you like to manage.

Switching Organizations

In Nitro, you can be signed into multiple organizations at the same time and switch between them in the status bar. The organization switcher also allows you to create new organizations.

Managing Members

  1. The count of members and the number of seats available in your subscription are displayed in the management overview section. (members/total seats)

  2. This list show all the members of the organization and their role.

  3. Members can be invited to join an organization via email. In the management overview section, the 'Invite User' section allows you to send an invitation email containing a join link.

  4. Open invitations are displayed below. You can revoke an invitation by clicking on the 'Cancel' button. Once an invitation is expired, you can resend it by clicking on the 'Resend' button.

Joining an Organization

Invited users can join an organization in two ways:

  1. By clicking on the join link they received via email.
  2. By opting to join during login on the sign in page.

During sign-in, the user can select into which organization (join orgs or personal) they want to sign in.

[Description of visualization: Screenshot showing the organization selection during sign-in]

User Roles and Access

There are three distinct roles within an organization: Owner, Admin, and Collaborator. Each role carries specific permissions:

Role Transfer Ownership Delete Organization Invite Users View Subscriptions Add Redirect URLs
Owner Yes Yes Yes Yes Yes
Admin No No Yes Yes Yes
Collaborator No No No No No
  • Owner: The owner can transfer ownership, delete the organization, and has all the permissions of an admin. Each organization can only have one owner.

  • Admin: Admins can invite users to the organization, view subscriptions, and add redirect URLs. They also have all the permissions of a collaborator.

  • Collaborator: Collaborators can log into the organization and leave the organization. They cannot add redirect URLs or invite new users.

Managing Permissions

You can manage the permissions of each member in the member list.

  1. Press the 'Edit' button on the right side of the member list to open the edit dialog.

  2. Select the desired role for the member.

  3. Press the 'Save' button to save the changes.

  4. You can also remove a member from the organization by clicking on the 'Revoke Access' button.

Transferring Ownership

Ownership of the organization can be transferred to any existing member from the management portal.
In the danger zone section, click on the 'Transfer' button to transfer ownership.

Redirect URLs

Each time a user signs into an organization from a new origin, a redirect URL must be added to the organization.

This security measure is designed to prevent your access tokens from being leaked to any arbitrary site hosted by third parties.

If the user is a collaborator, they won't have the permissions to add the URL and will see an informative message. An admin, on the other hand, can directly add the URL to the organization.

You can add the redirect URL in two ways:

  1. By signing in from the new origin. This will trigger a prompt to add the URL to the organization.

  2. By adding the URL directly from the management portal.

In the portal you have more options to configure the allowed destinations. You can for example allow all localhost origins. Or disable the access to the web UI in the cloud

  1. Configure the allowed types of origins.
  2. Add redirect Urls to the organization.
  3. Remove redirect Urls from the organization.

Website Content Docs Nitro Settings Index (`website/content/docs/nitro/settings/index.md`)


title: Settings - Overview

The Settings section provides users with various customization options. To access the settings, click on the cog icon located in the navigation bar on the left.

Currently, the most significant setting available is the Themes feature. This allows users to personalize the appearance and visual style of the application.

Please note that organization settings are exclusively accessible through the management portal. For more information about organization settings, refer to the Organizations documentation.


Website Content Docs Strawberryshake Caching Index (`website/content/docs/strawberryshake/caching/index.md`)


title: "Caching"

We are still working on the documentation for Strawberry Shake, so help us by finding typos, missing things, or write some additional docs with us.

StrawberryShake stores the result of GraphQL requests in a normalized entity store. The entity store allows your client to execute GraphQL requests with various strategies to reduce the need for network requests. Moreover, the normalized entities are updated by every request the client does, which means that you can build fully reactive components that change as the state in the store changes.

mermaid
sequenceDiagram
    participant Generated Client
    participant Operation Store
    participant Entity Store
    participant GraphQL Server
    Generated Client->>Operation Store: Queries local store
    Operation Store->>GraphQL Server: Queries GraphQL server
    Note over Entity Store: Normalize response into entities
    GraphQL Server->>Entity Store: Returns GraphQL response
    Note over Operation Store: Builds operation result from entities
    Entity Store->>Operation Store: Returns entities for operation
    Operation Store->>Generated Client: Returns operation result

Strategies

We support three basic strategies to interact with the store and fetch data.

Network Only

Network only is the simplest strategy and will fetch from the network and only then update the store. This means that our initial call will always get fresh data and at the same time update other request results watching the same entities.

If we use the reactive APIs in combination with network only we will still get updates whenever other requests fetch data for the entities we are watching.

Cache First

Cache first is essentially the opposite of network only since it will first fetch from the store, and if the store has the data needed, it will not make any network requests. If the store does not have the data needed, it will go to the network and try to get the data and update the store.

Cache and Network

The last strategy is a combination of the first two. The client will first try to get the data from the store. This gives us fast data response time if the store already has the data. After that the store will update that data for this request with data from the network which in consequence will trigger our subscription and serve us new data.

Configuration

The global strategy default can be set on our dependency injection setup method.

csharp
builder.Services
    .AddConferenceClient(ExecutionStrategy.CacheFirst) // <----
    .ConfigureHttpClient(client => client.BaseAddress = new Uri("http://localhost:5050/graphql"))
    .ConfigureWebSocketClient(client => client.Uri = new Uri("ws://localhost:5050/graphql"));

The global strategy default can then be overwritten by any Watch method for a particular request.

csharp
storeSession =
    ConferenceClient
        .GetSessions
        .Watch(ExecutionStrategy.CacheFirst) // <----
        .Where(t => !t.Errors.Any())
        .Select(t => t.Data!.Sessions!.Nodes)
        .Subscribe(result =>
        {
            sessions = result;
            StateHasChanged();
        });

Website Content Docs Strawberryshake Get Started Index (`website/content/docs/strawberryshake/get-started/index.md`)


title: "Get started with Strawberry Shake and Blazor"

In this tutorial we will walk you through the basics of adding a Strawberry Shake GraphQL client to a Blazor for WebAssembly project. For this example we will create a Blazor for WebAssembly application and fetch some simple data from our demo backend.

Watch the video on YouTube

Strawberry Shake is not limited to Blazor and can be used with any .NET standard compliant library.

In this tutorial, we will teach you:

  • How to add the Strawberry Shake CLI tools.
  • How to generate source code from .graphql files, that contain operations.
  • How to use the generated client to query data.

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.

Open your preferred terminal and select a directory where you want to add the code of this tutorial.

  1. Create a dotnet tool-manifest.
shell
dotnet new tool-manifest
  1. Install the Strawberry Shake tools.
shell
dotnet tool install StrawberryShake.Tools --local

Step 2: Create a Blazor WebAssembly project

Next, we will create our Blazor project so that we have a little playground.

  1. First, a new solution called Demo.sln.
shell
dotnet new sln --name Demo
  1. Create a new Blazor for WebAssembly application.
shell
dotnet new blazorwasm --name Demo
  1. Add the project to the solution Demo.sln.
shell
dotnet sln add ./Demo

Step 3: Install the required package

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. All of this functionality comes packaged with the StrawberryShake.Blazor meta package.

  1. Add the StrawberryShake.Blazor package to your project.
shell
dotnet add Demo package StrawberryShake.Blazor

Step 4: Add a GraphQL client to your project using the CLI tools

To add a client to your project, you need to run #!shell dotnet graphql init {{ServerUrl}} --clientName {{ClientName}}.

In this tutorial we will use our ChilliCream demo project to create a list of cryptocurrencies that we will add to our Blazor application.

If you want to have a look at our demo GraphQL server head over here.

  1. Add the crypto client to your Blazor application.
shell
dotnet graphql init https://demo.chillicream.com/graphql/ --clientName CryptoClient --Path ./Demo
  1. Customize the namespace of the generated client to be Demo.GraphQL. For this head over to the .graphqlrc.json and insert a namespace property in the StrawberryShake section.
json
{
  "schema": "schema.graphql",
  "documents": "**/*.graphql",
  "extensions": {
    "strawberryShake": {
      "name": "CryptoClient",
      "namespace": "Demo.GraphQL",
      "url": "https://demo.chillicream.com/graphql/",
      "records": {
        "inputs": false,
        "entities": false
      },
      "transportProfiles": [
        {
          "default": "Http",
          "subscription": "WebSocket"
        }
      ]
    }
  }
}

Now that everything is in place let us write our first query to ask for a list of session titles of the conference API.

  1. Choose your favorite IDE and the solution. If your are using VSCode do the following:
shell
code ./Demo
  1. Create new query document GetSessions.graphql with the following content:
graphql
query GetAssets {
  assets {
    nodes {
      name
      price {
        lastPrice
      }
    }
  }
}

If you are working with Visual Studio on Windows edit the properties of GetSessions.graphql and set the build action for the GraphQL file to GraphQL compiler.

  1. Compile your project.
shell
dotnet build

With the project compiled the strawberry shake generator produced a client but also components that we can use in Blazor.

  1. Head over to the Program.cs and add the new CryptoClient to the dependency injection.

In some IDEs it is still necessary to reload the project after the code was generated to update the IntelliSense. So, if you have any issues in the next step with IntelliSense just reload the project and everything should be fine.

csharp
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Demo;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

builder.Services
    .AddCryptoClient()
    .ConfigureHttpClient(client => client.BaseAddress = new Uri("https://demo.chillicream.com/graphql"));

await builder.Build().RunAsync();
  1. Go to _Imports.razor and add Demo.GraphQL to the common imports
csharp
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using Demo
@using Demo.Shared // (from .NET 8, `Demo.Layout`)
@using Demo.GraphQL
@using Demo.GraphQL.Components
@using StrawberryShake

Step 5: Use the generated Razor component to display the data

In this section we will integrate the Razor component and print a simple list on our index page to display the crypto currencies.

  1. Head over to Pages/Index.razor (from .NET 8, Home.razor).

  2. Remove everything from your page but the @page "/"

csharp
@page "/"
  1. Add the UseGetAssets component to your page.
csharp
@page "/"

<UseGetAssets Context="result">
    <ChildContent>

    </ChildContent>
    <ErrorContent>
        Something went wrong ...<br />
        @result.First().Message
    </ErrorContent>
    <LoadingContent>
        Loading ...
    </LoadingContent>
</UseGetAssets>

The query component allows you to handle the loading and the error state when fetching data. Both states can be handled but do not have to be.

  1. With that done let's render the actual content.
csharp
@page "/"

<UseGetAssets Context="result">
    <ChildContent>
        <ul>
        @foreach (var item in result.Assets!.Nodes!)
        {
            <li>@item.Name (@item.Price.LastPrice)</li>
        }
        </ul>
    </ChildContent>
    <ErrorContent>
        Something went wrong ...<br />
        @result.First().Message
    </ErrorContent>
    <LoadingContent>
        Loading ...
    </LoadingContent>
</UseGetAssets>
  1. Start the Blazor application with #!shell dotnet watch --project ./Demo and see if your code works.

Awesome! You have created your first application with Blazor and GraphQL.


Website Content Docs Strawberryshake Networking Index (`website/content/docs/strawberryshake/networking/index.md`)


title: "Networking"

Strawberry Shake supports multiple network protocols to communicate with your GraphQL server. Each transport integration is represented by a specific NuGet package to keep your client size as small as possible.

Protocols

Transport Protocol Package Strawberry Shake Version
HTTP GraphQL over HTTP StrawberryShake.Transport.Http 11.1
WebSocket subscriptions-transport-ws StrawberryShake.Transport.WebSockets 11.1
WebSocket graphql-transport-ws StrawberryShake.Transport.WebSockets 12.0
SignalR GraphQL over SignalR StrawberryShake.Transport.SignalR 12.0
gRPC GraphQL over gRPC StrawberryShake.Transport.gRPC 12.0
InMemory Hot Chocolate In-Memory StrawberryShake.Transport.InMemory 11.1

Transport Profiles

In order to have a small client size and generate the optimized client for your use-case Strawberry Shake uses transport profiles. By default Strawberry Shake will generate a client that uses GraphQL over HTTP for queries and mutations and graphql-transport-ws for subscriptions. Meaning if you are only using queries and mutations you need to add the package StrawberryShake.Transport.Http.

There are cases in which we want to define specialized transport profiles where we define for each request type a specific transport. You can define those transport profiles in your .graphqlrc.json.

The following .graphqlrc.json can be copied into our getting started example and will create two transport profiles. The first is called Default and matches the internal default. It will use GraphQL over HTTP by default and use graphql-transport-ws for subscriptions. The second profile is called WebSocket and will also use GraphQL over HTTP by default but for mutations and subscriptions it will use graphql-transport-ws.

json
{
  "schema": "schema.graphql",
  "documents": "**/*.graphql",
  "extensions": {
    "strawberryShake": {
      "name": "ConferenceClient",
      "namespace": "Demo.GraphQL",
      "url": "http://localhost:5050/graphql",
      "transportProfiles": [
        {
          "name": "Default",
          "default": "HTTP",
          "subscription": "WebSocket"
        },
        {
          "name": "WebSocket",
          "default": "HTTP",
          "mutation": "WebSocket",
          "subscription": "WebSocket"
        }
      ]
    }
  }
}

The generator will generate the dependency injection code with a new enum called ConferenceClientProfileKind. The name of the enum is inferred from your GraphQL client name. The enum can be passed into the dependency injection setup method and allows you to switch between the two transport profiles through configuration.

csharp
builder.Services
    .AddConferenceClient(profile: ConferenceClientProfileKind.WebSocket)
    .ConfigureHttpClient(client => client.BaseAddress = new Uri("http://localhost:5050/graphql"))
    .ConfigureWebSocketClient(client => client.Uri = new Uri("ws://localhost:5050/graphql"));

Website Content Docs Strawberryshake Performance Index (`website/content/docs/strawberryshake/performance/index.md`)


title: "Performance"

In this section we will give you an overview on how to optimize the performance of your GraphQL client. We will put an emphasis on network performance since this is the most critical performance obstacle you are facing when developing fluent applications that also have to work over bad mobile network connections or even when the application becomes fully offline.

Persisted operations and automatic persisted operations

Improve performance by sending smaller requests and pre-compile operations

The size of individual GraphQL requests can become a major pain point. This is not only true for the transport but also introduces inefficiencies for the server since large requests need to be parsed and validated. Hot Chocolate implements for this problem persisted operations. With persisted operations, we can store operation documents on the server in a key-value store. When we want to execute a persisted operation, we can send the key under which the operation document is stored instead of the operation document itself. This saves precious bandwidth and also improves execution time since the server will validate, parse, and compile persisted operation documents just once.

There are two flavors of persisted operations that Strawberry Shake supports and that is also supported by our GraphQL server Hot Chocolate.

Persisted operations

The first approach is to store operation documents ahead of time (ahead of deployment).
This is done by extracting the operations from your client application at build time. It will reduce the size of the requests and the bundle size of your application since operations can be completely removed from the client code at build time and are replaced with operation document hashes. Apart from performance, persisted operations can also be used for security by configuring Hot Chocolate to only accept persisted operations on production environments.

Read more on how to set up Strawberry Shake for persisted operations here.

Automatic persisted operations

Automatic persisted operations allow us to store operation documents dynamically on the server at runtime. With this approach, we can give our application the same performance benefits as with persisted operations without having to opt in to a more complex build process.

We are currently still working on this feature so stay tuned on this one.

Persisted State

Apart from focusing on reducing network request size we also can optimize using the network less by using the stores more efficiently. If you are not yet familiar with the store concepts first head over to here.

One thing particular here is to persist the state that is aggregated in the stores to either the browsers IndexDB or to some small database like SQLite or LiteDB. When the user leaves the app and later returns to (closes the browser and reopens it) we can fill the state from our storage and have immediately data for the user while our store at the same time will start refreshing that data over the network if that is available.

Read more on how to set up Strawberry Shake for persisted state here.


Website Content Docs Hotchocolate Fetching Data Batching Index (`website/content/docs/hotchocolate/fetching-data/batching/index.md`)


title: "Batching" metaTitle: "GraphQL Batching in Hot Chocolate: Solve the N+1 Problem"

Batching groups many small data fetches into one call to your data source and is the standard answer to the N+1 problem in GraphQL. Because GraphQL resolves data field by field, you should plan for batching from the start: it determines whether your API issues one query per request or hundreds.

This chapter explains why the N+1 problem appears in GraphQL and introduces the two batching tools Hot Chocolate provides: DataLoader and batch resolvers.

[!NOTE]
This chapter is about batching data fetches inside a single GraphQL operation. If you are looking for a way to send multiple GraphQL operations in one HTTP request, see Server Batching.

Why the N+1 Problem Happens in Every API

The N+1 problem exists with any API technology. A REST client that fetches a list of products and then calls /brands/{id} for each product produces the exact same access pattern: one request for the list, plus N requests for related data. The same happens inside a REST backend that loads a list and then lazily loads a relation per item.

GraphQL does not make this problem worse. What GraphQL changes is where the problem lives. With REST, each client decides how to stitch resources together, so the N+1 pattern is distributed across every client and cannot be fixed centrally. With GraphQL, clients declare what they need in one query, and the server resolves it. The deliberate decision is to handle data fetching centrally in the backend, and the backend is exactly where it can be optimized well: it is one place, it is close to the data, and one fix helps every client.

Why Per-Field Resolvers Make N+1 Visible

Every field in a GraphQL schema is backed by a resolver, and the execution engine walks the query as a tree. Consider this query:

Client query

graphql
{
  products(first: 5) {
    nodes {
      name
      brand {
        name
      }
    }
  }
}

The products resolver runs once and returns five products. Then the brand resolver runs once per product. A naive brand resolver that queries the database directly issues five queries for five products, and fifty queries for fifty products:

text
1 query:  products(first: 5)
5 queries: brand for product 1..5   ← N+1

The per-field resolver model is what makes the N+1 pattern visible and measurable in GraphQL. That visibility is a feature: because all data fetching flows through resolvers, you can intercept it in one place and batch it.

DataLoader

A DataLoader batches by key. Resolvers ask the DataLoader for a value by key, the DataLoader collects all requested keys while the engine executes resolvers, and then fetches all of them in one call (for example a single #!sql WHERE id IN (...) query). DataLoaders also cache and deduplicate within a request: the same key requested from anywhere in the query tree is fetched once, and every resolver sees the same result.

DataLoaders are the default choice for batching in Hot Chocolate. Use them whenever data is loaded by key and may be requested from more than one place in a query.

Learn more about DataLoader

Batch Resolvers

A batch resolver batches by field. Instead of running a resolver once per parent object, the execution engine collects all parent objects that reach a specific field and calls your resolver once with the full list. You do not define a DataLoader class or a key: you receive the parents and return one result per parent.

Batch resolvers have no cache and no cross-field deduplication. They fit computed values, aggregations, and external services with native batch endpoints, where the result is specific to one field.

Learn more about Batch Resolvers

Next Steps


Website Content Docs Hotchocolate Fetching Data Integrations Index (`website/content/docs/hotchocolate/fetching-data/integrations/index.md`)


title: Integrations

The IExecutable and IExecutable<T> interfaces abstract data sources in Hot Chocolate. Your data or domain layer can wrap a data source in an executable and pass it to the GraphQL layer. A resolver that returns IExecutable<T> is recognized as a list.

csharp
public class User
{
    public string Name { get; }
}

public interface IUserRepository
{
    public IExecutable<User> FindAll();
}

public class Query
{
    public IExecutable<User> GetUsers(IUserRepository repo) =>
        repo.FindAll();
}
sdl
type Query {
    users: [User!]!
}

This abstraction completely decouples the GraphQL layer from database-specific knowledge.

Filtering, sorting, and projections can pick up the executable and apply logic to it. A database-specific provider is still needed for these features, but it is opaque to the GraphQL layer.

The execution engine calls ToListAsync, FirstOrDefaultAsync, or SingleOrDefaultAsync on the executable. The executable runs these operations in the most efficient way for the database.

API

Source

csharp
object Source { get; }

The Source property holds the current state of the executable. For Entity Framework, this holds the IQueryable. For MongoDB, it is the DbSet<T> or IAggregateFluent<T>. Source is read-only. If you have a custom IExecutable implementation and need to change the source, create a method that returns a new executable with the new source.

ToListAsync

csharp
ValueTask<IList> ToListAsync(CancellationToken cancellationToken);

Returns a list of items.

FirstOrDefaultAsync

csharp
ValueTask<object?> FirstOrDefaultAsync(CancellationToken cancellationToken);

Returns the first element of the sequence, or a default value if the sequence contains no elements.

SingleOrDefaultAsync

csharp
ValueTask<object?> SingleOrDefaultAsync(CancellationToken cancellationToken);

Returns the only element of the sequence, or a default value if no element exists. Throws an exception if more than one element satisfies the condition.

Print

csharp
string Print();

Prints the executable in its current state. This is useful for debugging and logging the generated query.

Example

The following shows the Entity Framework implementation:

csharp
public class EntityFrameworkExecutable<T> : QueryableExecutable<T>
{
    public IQueryable<T> Source { get; }

    object IExecutable.Source => Source;

    public EntityFrameworkExecutable(IQueryable<T> queryable) : base(queryable)
    {
    }

    public QueryableExecutable<T> WithSource(IQueryable<T> source)
    {
        return new QueryableExecutable<T>(source);
    }

    public override async ValueTask<IList> ToListAsync(
        CancellationToken cancellationToken) =>
        await Source.ToListAsync(cancellationToken).ConfigureAwait(false);

    public override async ValueTask<object?> FirstOrDefaultAsync(
        CancellationToken cancellationToken) =>
        await Source.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);

    public override async ValueTask<object?> SingleOrDefaultAsync(
        CancellationToken cancellationToken) =>
        await Source.SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false);

    public override string Print() => Source.ToQueryString();
}

Next Steps


Profiling (`docs/profiling.md`)

Profiling

How to take CPU, allocation and memory snapshots of a .NET process that is running inside the dev container. The tools attach to a running process, so start the application and generate traffic the way you normally do.

Tools

The dev container installs these tools, pinned in .devcontainer/dockerfile:

Tool Version Location
dotnet-trace, dotnet-counters, dotnet-gcdump, dotnet-dump, dotnet-symbol 10.0.750501 ~/.dotnet/tools, dotnet global tools
dottrace 2026.2.3.1 ~/.dotnet/tools, dotnet global tool JetBrains.dotTrace.GlobalTools
dotmemory 2026.2.3.1 /opt/dotmemory with the launcher in /usr/local/bin, unpacked from JetBrains.dotMemory.Console.<rid>

~/.dotnet/tools is on PATH for every process in the container, including non-interactive shells. JetBrains publishes the dotMemory console profiler only as a per-architecture NuGet package, not as a dotnet tool, so it is unpacked instead of installed with dotnet tool.

The application runs in the same container as the tools, so no /tmp or PID namespace sharing is needed. The runtime creates its diagnostics socket under $TMPDIR (default /tmp). If you override TMPDIR for the application, run the tools with the same value.

Output folder

Write everything to perf/<label>-<git-short-sha>/ in the repository root. perf/ is ignored by git.

bash
OUT=perf/fusion-gateway-$(git rev-parse --short HEAD)
mkdir -p "$OUT"

The workspace folder is mounted from the host, so Rider on the host can open the .dtp and .dmw files straight from that folder.

Find the process

bash
dotnet-trace ps

Use the PID of the application process, not of dotnet run that launched it. For a web application, lsof -nP -iTCP: -sTCP:LISTEN -t returns the same PID.

CPU sampling trace

bash
dotnet-trace collect -p <pid> --duration 00:00:00:30 --format speedscope -o "$OUT/cpu.nettrace"
dotnet-trace report "$OUT/cpu.nettrace" topN -n 25
dotnet-trace report "$OUT/cpu.nettrace" topN -n 25 --inclusive

This writes cpu.nettrace and cpu.speedscope.json. Open the speedscope file at https://www.speedscope.app, or analyze the nettrace with PerfView or dotnet-trace report.

Allocation trace

bash
dotnet-trace collect -p <pid> --profile gc-verbose --duration 00:00:00:15 -o "$OUT/alloc.nettrace"

gc-verbose records sampled allocation ticks with their call stacks and every GC. It is expensive, so keep the duration short. PerfView and the TraceEvent library read the GCAllocationTick events.

Heap snapshots

bash
dotnet-gcdump collect -p <pid> -o "$OUT/heap.gcdump"
dotnet-gcdump report "$OUT/heap.gcdump" > "$OUT/heap-report.txt"

The report lists live objects by type and size. Take two gcdumps a few minutes apart to find growth.

Live counters

bash
dotnet-counters monitor -p <pid> --counters System.Runtime,Microsoft.AspNetCore.Hosting,System.Net.Http
dotnet-counters collect -p <pid> --format json -o "$OUT/counters.json" --duration 00:00:00:20 --counters System.Runtime,Microsoft.AspNetCore.Hosting,System.Net.Http

monitor shows the values live in the terminal and has no file output. collect writes the same counters to a file, as JSON or CSV.

dotTrace

dottrace attach supports the Sampling and Timeline profiling types. Tracing and Line-by-line are only available through dottrace start, which launches the process under the profiler.

Sampling, controlled through stdin. The profiler prints ##dotTrace["connected"], ##dotTrace["started"] and ##dotTrace["snapshot-saved"] as it goes, so send each command after the previous one was confirmed:

bash
dottrace attach <pid> --profiling-type=Sampling --save-to="$OUT/dottrace-sampling.dtp" \
  --collect-data-from-start=off --service-input=stdin --service-output=on
##dotTrace["start"]
##dotTrace["get-snapshot"]
##dotTrace["disconnect"]

The same session from a script, waiting for the confirmations instead of typing:

bash
LOG="$OUT/dottrace.log"; FIFO=$(mktemp -u); mkfifo "$FIFO"
dottrace attach <pid> --profiling-type=Sampling --save-to="$OUT/dottrace-sampling.dtp" --overwrite \
  --collect-data-from-start=off --service-input=stdin --service-output=on < "$FIFO" > "$LOG" 2>&1 &
exec 3>"$FIFO"
until grep -q '"connected"' "$LOG"; do sleep 1; done
printf '\n##dotTrace["start"]\r\n' >&3
sleep 30
printf '\n##dotTrace["get-snapshot"]\r\n' >&3
until grep -q '"snapshot-saved"' "$LOG"; do sleep 1; done
printf '\n##dotTrace["disconnect"]\r\n' >&3
wait; exec 3>&-; rm -f "$FIFO"

Tracing, for call counts, starts the application under the profiler instead of attaching. Point it at the application binary, not at dotnet run, so the profiled process is the application itself:

bash
dottrace start --framework=NetCore --profiling-type=Tracing --save-to="$OUT/dottrace-tracing.dtp" \
  --collect-data-from-start=off --service-input=stdin --service-output=on \
  bin/Release/net10.0/<App>

A snapshot is the .dtp file plus its numbered .dtp.0000 companions. Keep them together.

dotMemory

One snapshot now:

bash
dotmemory get-snapshot <pid> --save-to-dir="$OUT/dotmemory"

Attach and take a snapshot every 30 seconds, at most three, then detach:

bash
dotmemory attach <pid> --trigger-timer=30s --trigger-max-snapshots=3 --timeout=2m --save-to-dir="$OUT/dotmemory"

--trigger-mem-inc=50mb and --trigger-mem-limit=1gb trigger on memory growth instead of time. The result is a .dmw workspace that holds all snapshots of the session.

Dumps and symbols

dotnet-dump collect -p -o "$OUT/full.dmp" writes a full process dump for dotnet-dump analyze. dotnet-symbol downloads the runtime symbols a dump from another machine needs; dumps taken inside the container already resolve.

Caveats

  • Profile Release builds. Debug builds disable optimizations and distort both time and allocations.
  • Sampling gives time but not call counts. Use dotTrace Tracing mode for call counts, which means starting the process with dottrace start.
  • On Linux, EventPipe samples measure thread time, so idle thread pool frames such as LowLevelLifoSemaphore.WaitForSignal, WaitHandle.WaitOneNoCheck, Monitor.Wait, the finalizer thread in GC.RunFinalizers, PollGCWorker and a blocked file watcher in Interop+Sys.Read dominate the exclusive time. Ignore them and read the frames below them. The speedscope export puts a synthetic CPU_TIME or UNMANAGED_CODE_TIME leaf under every sample, and ?!? stands for native frames without symbols, mostly GC threads. For the CPU a process really uses, read dotnet.process.cpu.time from dotnet-counters.
  • Take the dotMemory snapshots last. In two sessions the profiled Fusion gateway crashed 45 seconds and two minutes after dotmemory attach had disconnected, under load, with a native fault that the runtime turned into a createdump minidump. Set DOTNET_DbgEnableMiniDump=1 on the application if you want that dump, and restart the application after a dotMemory session before measuring anything else.
  • .dtp and .dmw snapshots are for Rider on the host. Agents should use the dotnet-trace and dotnet-gcdump outputs, which are readable in the container.
  • Attaching needs no special container privileges. It only requires that the application was not started with DOTNET_EnableDiagnostics=0.

Updating Dotnet (`docs/updating-dotnet.md`)

Updating .NET

What to check when TargetFrameworks in src/Directory.Build.props or the SDK version in global.json changes. Each section covers code that follows the source of ASP.NET Core or .NET, so an upstream change can call for the same change here.

Multipart form reader

MultipartFormReader in src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers reads the form of a multipart request for HttpMultipartMiddleware. It follows the multipart branch of FormFeature.InnerReadFormAsync, and its antiforgery check, in src/Http/Http/src/Features/FormFeature.cs on dotnet/aspnetcore. MultipartReader, FileBufferingReadStream, FormFile, and header parsing stay the framework's, so their fixes reach applications through servicing releases.

The reader was last compared with these tags, whose multipart branches are identical:

Target framework Tag
net8.0 v8.0.31
net9.0 v9.0.20
net10.0 v10.0.12
net11.0 v11.0.0-rc.1.26425.128

To list the upstream changes between a recorded tag and a new one, here from v10.0.12 to v10.0.13:

bash
git clone --filter=blob:none --no-checkout https://github.com/dotnet/aspnetcore.git
git -C aspnetcore log --oneline v10.0.12..v10.0.13 -- src/Http/Http/src/Features/FormFeature.cs

Apply a change to the section loop, its limits, the file buffering, or the antiforgery check to the reader, and record the new tag in the table.

The reader differs from FormFeature in these ways:

  • Each limit has its own error code. MultipartBodyLengthLimit applies to every section (HC0135), and maxAllowedRequestSize also applies to the operations field (HC0010); whichever of the two is smaller limits operations and gives its code. BufferBodyLengthLimit, when BufferBody is set, applies to the whole body (HC0136). FormFeature does not apply maxAllowedRequestSize; it throws InvalidDataException for MultipartBodyLengthLimit and IOException for BufferBodyLengthLimit.
  • With BufferBody set, it buffers the body even when an earlier component already made the body seekable, so BufferBodyLengthLimit applies in that case too.
  • It refuses a request whose IAntiforgeryValidationFeature has IsValid set to false, without the HttpContext.Items keys that FormFeature checks first.
  • It does not read URL-encoded forms, which never reach the multipart middleware.
  • It does not apply endpoint IFormOptionsMetadata, which the public FormFeature constructor does not apply either.

Website Content Docs Fusion Adding A Subgraph (`website/content/docs/fusion/adding-a-subgraph.md`)


title: "Adding a Subgraph"

You have an existing Fusion project with a gateway, one or more subgraphs, and a working composition pipeline. Now you need to add a new subgraph. Maybe your team owns a new domain (shipping, billing, inventory), or you are splitting an existing subgraph into smaller services. Either way, the process is the same: create a new Hot Chocolate project, define your types and any entity extensions, export the schema, compose, and verify.

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 tutorial first.

Prerequisites

Before you begin, you need:

  • An existing Fusion project with at least one subgraph and a gateway
  • The Nitro CLI installed (#!shell dotnet tool install -g ChilliCream.Nitro.CommandLine)
  • The .NET 10 SDK or later

You should be able to compose and run your existing project successfully. If composition is currently broken, fix that first.

Subgraph Project Structure

Every Fusion subgraph follows the same layout:

text
src/
  Shipping/
    Types/
      Product.cs              # Entity stub
      ShipmentNode.cs         # Type extension (replaces foreign key with entity reference)
      ShippingQueries.cs      # Query resolvers (lookups)
    Shipment.cs               # Domain type
    ShipmentRepository.cs     # In-memory data
    Program.cs                # Server configuration
    Shipping.csproj           # Project file
    appsettings.json          # App settings
    schema-settings.json      # Fusion subgraph settings (created on first schema export)
    schema.graphqls           # Exported schema (generated, do not edit)

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 a new GraphQL server project from the template:

shell
dotnet new graphql -n Shipping

The template creates sample files in Shipping/Types (Author.cs, Book.cs, Query.cs). Delete those files before continuing so your schema only contains your own types.

Your Shipping/Shipping.csproj should look like this:

xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup Condition="'$(ImplicitUsings)' == 'enable'">
    <Using Include="Shipping" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="HotChocolate.AspNetCore" />
    <PackageReference Include="HotChocolate.AspNetCore.CommandLine" />
    <PackageReference Include="HotChocolate.Types.Analyzers">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

</Project>

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:

json
"launchUrl": "http://localhost:5003/graphql",
"applicationUrl": "http://localhost:5003"

This ensures the subgraph runs on port 5003, which matches what you will configure in schema-settings.json later.

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

Create Shipment.cs in the Shipping project:

csharp filename="shipping/shipment.cs"
namespace Shipping;

public class Shipment
{
    public int Id { get; set; }

    public int ProductId { get; set; }

    public required string TrackingNumber { get; set; }

    public required string Status { get; set; }
}

Each shipment has a ProductId that references a product from the Products subgraph.

Add In-Memory Data

Create ShipmentRepository.cs:

csharp filename="shipping/shipmentrepository.cs"
namespace Shipping;

public static class ShipmentRepository
{
    private static readonly List<Shipment> Shipments =
    [
        new Shipment { Id = 1, ProductId = 1, TrackingNumber = "SH-001", Status = "Delivered" },
        new Shipment { Id = 2, ProductId = 1, TrackingNumber = "SH-002", Status = "In Transit" },
        new Shipment { Id = 3, ProductId = 2, TrackingNumber = "SH-003", Status = "Shipped" },
    ];

    public static Shipment? GetById(int id)
        => Shipments.FirstOrDefault(s => s.Id == id);

    public static List<Shipment> GetByProductId(int productId)
        => Shipments.Where(s => s.ProductId == productId).ToList();
}

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:

csharp filename="shipping/types/product.cs"
namespace Shipping;

public sealed record Product(int Id)
{
    public List<Shipment> GetShipments()
        => ShipmentRepository.GetByProductId(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

Create Types/ShippingQueries.cs with a public lookup for Shipment and an internal lookup for Product:

csharp filename="shipping/types/shippingqueries.cs"
namespace Shipping;

[QueryType]
public static partial class ShippingQueries
{
    [Lookup]
    public static Shipment? GetShipmentById(int id)
        => ShipmentRepository.GetById(id);

    [Internal, Lookup]
    public static Product? GetProductById(int id)
        => new(id);
}
  • GetShipmentById is a public lookup. Clients can call it directly, and the gateway uses it for entity resolution.
  • GetProductById is an internal lookup. It is hidden from the composite schema and exists only for the gateway to enter the Shipping subgraph's Product type during entity resolution. It constructs a stub from the ID without checking whether the product exists, which is safe because the gateway only calls internal lookups after another subgraph has already confirmed the entity exists.

For more on public vs. internal lookups and when to use each, see Entities and Lookups.

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:

csharp filename="shipping/types/shipmentnode.cs"
using HotChocolate.Types;

namespace Shipping;

[ObjectType<Shipment>]
public static partial class ShipmentNode
{
    [BindMember(nameof(Shipment.ProductId))]
    public static Product GetProduct([Parent] Shipment shipment)
        => new(shipment.ProductId);
}

[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

Set Program.cs to:

csharp
var builder = WebApplication.CreateBuilder(args);

builder
    .AddGraphQL("Shipping")
    .AddTypes();

var app = builder.Build();

app.MapGraphQL();
app.RunWithGraphQLCommands(args);

#!csharp 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

From the project root, export the schema:

shell
dotnet run --project ./Shipping -- schema export

This generates two files in the Shipping directory:

  • schema.graphqls contains the subgraph's GraphQL schema.
  • schema-settings.json contains the subgraph settings.

Because Program.cs uses #!csharp AddGraphQL("Shipping"), the generated schema-settings.json already contains "name": "Shipping". The transport URL defaults to http://localhost:5000/graphql, so update it to match port 5003:

json
{
  "name": "Shipping",
  "transports": {
    "http": {
      "url": "http://localhost:5003/graphql"
    }
  }
}

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

Run composition with all subgraph schemas, including your new one:

shell
nitro fusion compose \
  -s Products/schema.graphqls \
  -s Reviews/schema.graphqls \
  -s Shipping/schema.graphqls \
  -a gateway.far

If composition succeeds, copy the updated gateway.far to your gateway project directory:

shell
cp gateway.far Gateway/gateway.far

If you already have a composed gateway.far with the Products and Reviews subgraphs, you can add the new subgraph to the existing archive:

shell
nitro fusion compose \
  -s Shipping/schema.graphqls \
  -a gateway.far

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:

graphql
query {
  productById(id: 1) {
    name
    price
    shipments {
      trackingNumber
      status
    }
  }
}

This query touches two subgraphs:

  1. The gateway calls the Products subgraph to fetch name and price.
  2. The gateway uses the internal lookup to enter the Shipping subgraph and resolve shipments for the same product.

The client sees one unified response with fields from both subgraphs merged into a single Product.

You can also query in the other direction, starting from a shipment and navigating to the product:

graphql
query {
  shipmentById(id: 1) {
    trackingNumber
    status
    product {
      name
      price
    }
  }
}

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

If composition fails after adding your new subgraph, the error messages point to specific issues.

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 for details.

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.

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

  • Need cross-subgraph field dependencies? See Data Requirements for the full range of @require patterns and FieldSelectionMap syntax.
  • Composition failed? See Composition for the full merging rules, common errors, and fixes.
  • Want to understand entities more deeply? See Entities and Lookups for the complete guide to entity stubs, public vs. internal lookups, and composite keys.
  • Ready to deploy? See Deployment and CI/CD for setting up independent subgraph deployments with the Nitro CLI.

Website Content Docs Fusion Authentication And Authorization (`website/content/docs/fusion/authentication-and-authorization.md`)


title: "Authentication and Authorization"

The Fusion gateway builds on ASP.NET Core authentication and authorization. You can use the same features you use in any ASP.NET Core service, including JWT validation, cookie authentication, OpenID Connect, and mTLS.

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

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.

A minimal JWT bearer setup for the gateway:

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://login.example.com/";
        options.Audience = "https://api.example.com";
    });

builder.Services.AddAuthorization();

builder.Services
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far");

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapGraphQL();

app.Run();

Everything in the AddJwtBearer callback is standard ASP.NET Core. For details on JWKS rotation, multi-issuer setups, custom audience validation, refresh policies, and clock skew handling, see the Microsoft Learn reference. You do not need to implement those features yourself.

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

To reject anonymous requests at the gateway before they reach the GraphQL pipeline, attach an authorization policy to the endpoint:

csharp
app.MapGraphQL().RequireAuthorization();

Or apply a named policy that requires a specific scope:

csharp
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("graphql:read", policy =>
        policy.RequireAuthenticatedUser()
              .RequireClaim("scope", "graphql:read"));
});

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

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. If you are building a browser-based client, review UI and SPA considerations.

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. 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

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

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.

Fusion uses the official ASP.NET Core header propagation mechanism to forward headers to subgraphs:

csharp
builder.Services.AddHeaderPropagation(options =>
{
    options.Headers.Add("Authorization");
    options.Headers.Add("GraphQL-Preflight");
});

See the Microsoft Learn documentation on header propagation for more details.

You must configure header propagation on each HTTP client individually. Otherwise, it will not take effect. By default, Fusion uses a named HTTP client called "fusion" to call subgraphs. You can change this name in the schema-settings.json of each subgraph. If you keep the default, configure header propagation on the "fusion" client:

json
{
  "transports": {
    "http": {
      "url": "http://products:5001/graphql",
      "clientName": "fusion"
    }
  }
}

Register header propagation for that client:

csharp
builder.Services
    .AddHttpClient("fusion")
    .AddHeaderPropagation();

Add the UseHeaderPropagation middleware before the GraphQL middleware in your pipeline so headers are forwarded:

csharp
// Later in the pipeline.
app.UseHeaderPropagation();

app.MapGraphQL();

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.

To intercept outgoing requests, add a DelegatingHandler to the HTTP client that calls subgraphs. This lets you derive headers from validated claims, exchange the incoming token for a new token with the correct audience, or call an external service to get additional caller information to forward.

csharp
public sealed class ClaimsHeaderHandler(IHttpContextAccessor httpContextAccessor)
    : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var user = httpContextAccessor.HttpContext?.User;

        var userId = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (!string.IsNullOrEmpty(userId))
        {
            request.Headers.TryAddWithoutValidation("X-User-Id", userId);
        }

        var tenantId = user?.FindFirst("tenant_id")?.Value;
        if (!string.IsNullOrEmpty(tenantId))
        {
            request.Headers.TryAddWithoutValidation("X-Tenant-Id", tenantId);
        }

        return base.SendAsync(request, cancellationToken);
    }
}

Register the handler and configure the client:

csharp
builder.Services.AddHttpContextAccessor();
builder.Services.AddTransient<ClaimsHeaderHandler>();

builder.Services
    .AddHttpClient("fusion")
    .AddHeaderPropagation()
    .AddHttpMessageHandler<ClaimsHeaderHandler>();

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.

Gateway-to-subgraph trust

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

To ensure that only the gateway can call the subgraphs, use mutual TLS (mTLS) for gateway-to-subgraph communication.

Useful links:

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.

The subgraph ultimately decides whether a request can access specific data. Only the subgraph has the full context about the data and its business rules, so it is best suited to make authorization decisions.

For more information on subgraph-level authorization, @authorize, policies, and claims in Hot Chocolate subgraphs, see Hot Chocolate authorization.


Website Content Docs Fusion Batching (`website/content/docs/fusion/batching.md`)


title: "Batching"

One wave of a query plan can produce many calls to the same subgraph: one entity lookup per item of a list, or several distinct lookups that all target the same service. Batching collapses those calls into fewer round trips, without changing the query plan.

Where Batching Applies

Consider a query that reads a list of books from one subgraph and each book's rating from another:

graphql
{
  books {
    title
    rating
  }
}

The gateway fetches the books from the first subgraph, then needs rating for every book from the second. With two books that is two lookups, with two hundred books it is two hundred. Batching sends them as one request instead; the same lookups still happen, they just travel together.

The Three Batching Capabilities

Fusion knows three ways to put more than one call into one HTTP request. Each one is declared as a capability of the subgraph it is used against.

Variable Batching

Variable batching sends one operation with many sets of variables in one request. The variables property carries an array instead of an object, and the subgraph runs the operation once per set:

json
{
  "query": "query($id: ID!) { bookById(id: $id) { rating } }",
  "variables": [{ "id": "1" }, { "id": "2" }]
}

Variable batching is a Hot Chocolate protocol extension, not part of the GraphQL over HTTP specification, so only subgraphs that implement it can be sent this shape.

Request Batching

Request batching sends an array of independent requests in one HTTP request. Each element is a complete GraphQL request with its own query and its own variables, so a batch can carry different operations:

json
[
  {
    "query": "query($id: ID!) { bookById(id: $id) { rating } }",
    "variables": { "id": "1" }
  },
  {
    "query": "query($id: ID!) { authorById(id: $id) { name } }",
    "variables": { "id": "7" }
  }
]

When the subgraph supports request batching but not variable batching, the gateway flattens each operation's variable sets into one array element per set, so the batch still travels as a single request.

Request batching is also a protocol extension. Subgraphs that do not accept a JSON array as the request body cannot be sent this shape.

Alias Batching

Alias batching needs no protocol extension. The gateway merges the calls into one plain GraphQL operation whose root selections are aliased per item, which any spec-compliant GraphQL server can answer.

For the two book lookups above, the gateway sends:

graphql
query Op_68b774cc_Batch_2963222070b1fe07(
  $_0___fusion_1_id: ID!
  $_1___fusion_1_id: ID!
) {
  _0_bookById: bookById(id: $_0___fusion_1_id) {
    ..._fusion_body_1
  }
  _1_bookById: bookById(id: $_1___fusion_1_id) {
    ..._fusion_body_1
  }
}

fragment _fusion_body_1 on Book {
  rating
}
json
{
  "_0___fusion_1_id": "1",
  "_1___fusion_1_id": "2"
}

The subgraph answers with one response, and the gateway splits it by alias:

json
{
  "data": {
    "_0_bookById": { "rating": "1" },
    "_1_bookById": { "rating": "2" }
  }
}

The merged operation follows three rules:

  • Each item gets a prefix. The root selection is aliased with __, and the variables that vary per item are renamed with the same prefix.
  • Shared variables are declared once. A variable the gateway forwards from the client request carries one value for the whole batch, so it keeps its name and appears once in the variable definitions.
  • Repeated bodies become one fragment. When several items select the same body, the body is emitted once as a generated fragment and each item spreads it.

Only query operations are merged. Mutations and subscriptions are never merged, and a request that uploads files keeps its own multipart round trip while the remaining requests still share one.

How Fusion Chooses a Protocol

The gateway decides per subgraph, from the capabilities that subgraph declares. There are two independent dimensions.

Many variable sets of one operation. Variable batching wins whenever the subgraph declares it. Alias batching serves this dimension when variable batching is absent.

Distinct operations in one wave. Request batching wins whenever the subgraph declares it. Alias batching serves this dimension when request batching is absent. When neither is available, the gateway sends one HTTP request per operation.

The two decisions are taken independently, so a subgraph that declares only one of the two protocol extensions uses it for its own dimension and alias batching for the other. A batch that would merge fewer than two items is sent on the subgraph's native protocol instead.

Configuring Batching Capabilities

Batching capabilities live under capabilities.batching in a source schema's HTTP transport settings:

json
{
  "name": "Products",
  "transports": {
    "http": {
      "url": "http://products/graphql",
      "capabilities": {
        "batching": {
          "variableBatching": true,
          "requestBatching": true,
          "aliasBatching": true
        }
      }
    }
  }
}

All three flags are optional and independent. A flag you declare always wins; a flag you omit keeps its default.

Defaults

The default depends on how the source schema is connected:

Source schema variableBatching requestBatching aliasBatching
GraphQL (the default connector) on on off
Apollo Federation (see the section below) off off on

The default GraphQL connector assumes a subgraph that implements both protocol extensions, which a Hot Chocolate source schema does. An Apollo Federation subgraph is only assumed to speak plain GraphQL.

The connector kind only sets the starting value, so partial settings mix with it per flag:

  • #!json { "requestBatching": true } on an Apollo Federation subgraph yields request batching and alias batching, and request batching is then preferred for distinct operations.
  • #!json { "aliasBatching": false } on an Apollo Federation subgraph yields no batching at all.
  • #!json { "variableBatching": false, "requestBatching": false } on an Apollo Federation subgraph still leaves alias batching on. Declare "aliasBatching": false as well to turn batching off completely.

[!NOTE]
The settings template a Hot Chocolate subgraph exports declares all three flags explicitly, so a gateway that uses it does not fall back to the defaults. variableBatching and requestBatching mirror the batching the subgraph's server options allow, which AddSourceSchemaDefaults() turns on, and aliasBatching is always true.

Apollo Federation Subgraphs

An Apollo Federation subgraph is entered through the _entities field, and the entity keys travel in its representations argument. One _entities operation therefore already resolves many entities with a single variable set, so variable batching never applies on this path.

That leaves distinct operations: several _entities calls with different sub-selections, or several different lookups against the same subgraph in one wave. For those, the gateway uses request batching if the subgraph declares it, alias batching otherwise, and one request per operation when neither is available.

By default only alias batching is on, so two _entities calls in one wave travel as one merged operation:

graphql
query Op_88f73202_Batch_d9cfb37825dbe30d(
  $_0_representations: [_Any!]!
  $_1_representations: [_Any!]!
) {
  _0__entities: _entities(representations: $_0_representations) {
    ... on Child {
      b: value(suffix: "!")
    }
  }
  _1__entities: _entities(representations: $_1_representations) {
    ... on Child {
      a: value
    }
  }
}

See the Apollo Federation Connector page for the connector-specific settings.

Errors in a Batched Request

A merged operation produces a single response, and the gateway attributes its errors by path: the first segment of an error path is the alias of the item, so the error is routed to that item and rewritten to the item's own field name. An error without a path applies to the whole request and is handed to every item of the batch.

The result is the same as if the calls had been sent separately. In the two-book example, an error on the second book's rating appears on that book alone:

json
{
  "data": {
    "books": [
      { "title": "C# in Depth", "rating": "1" },
      { "title": "The Lord of the Rings", "rating": null }
    ]
  },
  "errors": [
    {
      "message": "Rating unavailable.",
      "path": ["books", 1, "rating"]
    }
  ]
}

Independent of batching, the capabilities.onError setting selects the error handling mode the gateway asks a subgraph for ("propagate" or "null"). It is not set by default, so the gateway sends no onError value and the subgraph applies its own behavior.

Next Steps

  • "I want to tune the transport itself": Performance Tuning covers the named HttpClient, HTTP/2, request deduplication, and concurrency limits.
  • "I want to understand where the lookups come from": Entities and Lookups explains how the gateway enters a subgraph by key.
  • "I run Apollo Federation subgraphs": Apollo Federation Connector covers the connector end to end.

Website Content Docs Fusion Cache Control (`website/content/docs/fusion/cache-control.md`)


title: "Cache Control"

Cache-Control is the HTTP header field that tells browsers, reverse proxies, and CDNs how they are allowed to store and reuse a response instead of sending the same request back to the server every time. Together with related headers such as Vary, it makes cached responses safe and predictable by defining whether a response may be reused, how long it may be reused, and which parts of the request affect that decision.

Why Cache Control Matters

Good cache rules help you:

  • Reduce latency for users.
  • Reduce load on your backend services.
  • Keep cache behavior predictable.
  • Protect user-specific data from shared caches.

To make caching work safely and predictably, you need two things:

  1. A deterministic GET route so caches get a stable cache key.
  2. Correct Cache-Control (and Vary) headers so caches know where and how long a response may be reused.

Why GraphQL Needs Extra Care

GraphQL usually exposes one endpoint, but each request can ask for different fields. Two requests to the same URL can therefore return very different response shapes and different data sensitivity.

A single GraphQL response can also mix public data and user-specific data. Since HTTP cache headers apply to the full response, the gateway has to compute one safe final policy that represents everything selected in that operation.

That means one response can include:

  • Public data (safe for shared caches), and
  • User-specific data (not safe for shared caches).

The GraphQL operation type matters. Query operations are side-effect free reads, so they are the primary target for HTTP and CDN caching. Mutations change data and should not be cached as shared HTTP responses. Subscriptions are long-running streams and are not HTTP-cacheable.

Deterministic GET Routes

The GraphQL over HTTP specification allows query operations to be sent over HTTP GET.

http
GET /graphql?query=query GetProducts{products{nodes{name}}}

The same applies when variables are included.

http
GET /graphql?query=query GetProducts($first:Int!){products(first:$first){nodes{name}}}&variables={"first":5}

In real requests, these values are URL-encoded, and for larger operations the query string quickly becomes difficult to work with. This is where persisted operations help.

Persisted Operation Routes

In large first-party GraphQL APIs, a common approach used by companies such as Netflix, Meta, and X is to rely on trusted documents. Client operations are stored in an operation store, and clients send a stable operation identifier instead of the full query text.

You can read more in the First-Party API guide.

With trusted documents in place, persisted-operation routes become short and stable.

http
GET /graphql/persisted/GetProducts/123456789

Variables can then be sent as query parameters.

http
GET /graphql/persisted/GetProducts/123456789?variables={"first":5}

In order to use persisted-operation routes you need to add the middleware MapGraphQLPersistedOperations.

csharp
var app = builder.Build();

app.MapGraphQLPersistedOperations();

`@cacheControl` in GraphQL

A deterministic route alone is not enough. The gateway also needs policy metadata to decide whether a response is public or private, and how long it may be reused.

GraphQL provides the @cacheControl directive for this purpose. You can place it on fields and types to describe cache intent.

graphql
type Query {
  productById(id: ID!): Product @cacheControl(maxAge: 300, sharedMaxAge: 900)

  me: UserProfile
    @cacheControl(maxAge: 60, scope: PRIVATE, vary: ["Authorization"])
}

How Fusion Assembles the Final Headers

Fusion computes one effective response policy by traversing the planned operation tree. It starts at the selected root fields, reads @cacheControl metadata on each field, falls back to the field return type when values are missing, and continues recursively through child selections, including interfaces and unions.

All collected constraints are merged into one final policy. The merge is conservative: max-age and s-maxage take the lowest value, scope resolves to the strictest value (private over public), and vary values are merged, normalized, and deduplicated. Every GraphQL response already lists Accept in Vary, and the merged vary names are added to it. Some CDNs, Akamai by default, do not cache a response whose Vary lists anything other than Accept-Encoding. Configure the CDN to cache it anyway, or remove Accept from Vary in a custom formatter.

Fusion computes these cache constraints for query operations. Mutation requests, Subscription request, introspection requests, and operations with no cache constraints do not get cache-control headers.

Cache Policy Starts in Subgraphs

In Fusion, cache policy starts where data is resolved, which means your subgraphs define cache intent for their own fields.

graphql
type Product {
  id: ID!
  name: String!
}

type UserProfile {
  id: ID!
  email: String!
}

type Query {
  productById(id: ID!): Product @cacheControl(maxAge: 300, sharedMaxAge: 900)

  me: UserProfile
    @cacheControl(maxAge: 60, scope: PRIVATE, vary: ["Authorization"])
}

If your subgraph runs on Hot Chocolate, you can express the @cacheControl directive with the [CacheControl] attribute.

csharp
using HotChocolate.Caching;

[QueryType]
public static class Query
{
    [CacheControl(300, SharedMaxAge = 900)]
    public static Product? GetProductById(int id)
        => ProductRepository.GetById(id);

    [CacheControl(60, Scope = CacheControlScope.Private, Vary = ["Authorization"])]
    public static UserProfile GetMe()
        => UserProfileRepository.GetCurrent();
}

Enable cache-control metadata on the subgraph:

csharp
builder
    .AddGraphQL()
    .AddCacheControl();

Enable Cache Control in the Fusion Gateway

The gateway must be configured to read cache-control metadata during planning and to write the final HTTP headers on the response.

csharp
var builder = WebApplication.CreateBuilder(args);

builder
    .AddGraphQLGateway()
    .AddCacheControl()
    .UseQueryCache();

AddCacheControl() enables cache-constraint planning. UseQueryCache() writes the final Cache-Control and Vary headers to HTTP responses.

Skip Cache Control for a Specific Request

Use SkipQueryCaching() on OperationRequestBuilder to bypass cache-control for a specific request.

csharp
using HotChocolate.AspNetCore;
using HotChocolate.Execution;

public sealed class NoCacheHeaderInterceptor : DefaultHttpRequestInterceptor
{
    public override ValueTask OnCreateAsync(
        HttpContext context,
        IRequestExecutor requestExecutor,
        OperationRequestBuilder requestBuilder,
        CancellationToken cancellationToken)
    {
        if (context.Request.Headers.ContainsKey("X-Skip-Cache-Control"))
        {
            requestBuilder.SkipQueryCaching();
        }

        return base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken);
    }
}

Putting It Together

In day-to-day terms, the flow is simple:

  • Subgraphs declare cache intent.
  • Fusion composes that metadata.
  • The gateway calculates one safe policy for each query result.
  • HTTP caches enforce the resulting headers.

Website Content Docs Fusion Cli (`website/content/docs/fusion/cli.md`)


title: "Nitro CLI Reference"

The Nitro CLI provides the nitro fusion command group for composing, validating, publishing, and running Fusion gateway configurations. Use these commands for local development workflows and for publishing configurations to Nitro Cloud.

Installation

shell
npm install @chillicream/nitro

The npm package bundles platform-specific native binaries. Supported platforms: linux-x64, linux-musl-x64, linux-arm64, osx-x64, osx-arm64, win-x64, win-x86, win-arm64.

Homebrew (macOS)

shell
brew install ChilliCream/tools/nitro-cli

.NET tool

Install globally:

shell
dotnet tool install --global ChilliCream.Nitro.CommandLine

Or install per-project:

shell
dotnet new tool-manifest
dotnet tool install ChilliCream.Nitro.CommandLine

Verify installation

shell
nitro --version

Commands Overview

Command Purpose Cloud
compose Compose source schemas into a Fusion archive No
download Download gateway configuration from Nitro Cloud Yes
migrate Migrate v1 to v2 configuration files No
publish Publish a Fusion configuration to a stage Yes
run Start a local Fusion gateway No
settings set Configure composition settings in an archive No
upload Upload a source schema for later composition Yes
validate Validate a schema against a stage Yes

Cloud Authentication Options

Commands that interact with Nitro Cloud (download, publish, upload, validate) accept these global options:

Option Description Default
--api-key <key> API key for authentication NITRO_API_KEY env var

If --api-key is not provided and no NITRO_API_KEY environment variable is set, the CLI uses the session from nitro login.

nitro fusion compose

Composes source schemas into a Fusion archive (.far file).

Syntax

shell
nitro fusion compose [options]

Options

Option Description Default
--source-schema-file (alias: -f) Path to a local source schema file or directory. Can be repeated. Auto-discovers *.graphql/*.graphqls in working directory
--source-schema-url URL from which to download a source schema. Can be repeated. --
--source-schema-settings-file Settings file for the corresponding URL occurrence. Can be repeated. --
--archive (alias: -a) Output path for the Fusion archive ./gateway.far
--environment <name> (alias: --env, -e) Environment name for variable substitution ASPNETCORE_ENVIRONMENT or Development
--enable-global-object-identification Enable Relay-style global object identification false
--node-resolution <gateway|source-schema> Choose who resolves Query.node identifiers gateway
--shareable-field-runtime-type-routing Choose how type-conditioned selections are routed for Apollo shareable abstractions source-local
--allow-non-resolvable-interface-objects Allow Apollo interface objects without a resolvable key false
--include-satisfiability-paths Include satisfiability diagnostic paths false
--watch Recompose on file or paired remote-settings changes false
--exclude-by-tag <tag> Exclude fields/types by tag. Can be repeated. --
--working-directory (alias: -w) Working directory for resolving paths Current directory

Each local .graphql or .graphqls file must have a companion -settings.json file. For example, schema.graphqls requires schema-settings.json. Local files do not use --source-schema-settings-file.

For remote schemas, repeat --source-schema-url and --source-schema-settings-file the same number of times. Nitro pairs them by occurrence: the first URL uses the first settings file, the second URL uses the second settings file, and so on. Keep each pair adjacent in scripts. The settings file selects GET or Apollo _service.sdl acquisition as described in Getting the Subgraph Schema.

If no local file, remote URL, or source removal is specified, the CLI scans the working directory for all .graphql and .graphqls files.

When an archive has no stored setting, composition uses gateway for node resolution and source-local for shareable-field runtime type routing. If you recompose an existing archive without the corresponding flag, Nitro preserves the stored value. See Shareable Abstract Field Routing for the routing behavior.

Examples

Compose from specific files:

shell
nitro fusion compose \
  --source-schema-file ./Products/schema.graphqls \
  --source-schema-file ./Reviews/schema.graphqls \
  --archive gateway.far \
  --environment Development \
  --enable-global-object-identification

Compose two remote schemas and one local schema. Each remote settings option corresponds to the URL occurrence above it:

shell
nitro fusion compose \
  --source-schema-url https://products.example.com/graphql \
  --source-schema-settings-file ./Products/schema-settings.json \
  --source-schema-url https://reviews.example.com/graphql \
  --source-schema-settings-file ./Reviews/schema-settings.json \
  --source-schema-file ./Inventory/schema.graphqls \
  --archive gateway.far

After a successful composition, Nitro prints:

text
✅ Composite schema written to '/absolute/path/to/gateway.far'.

Auto-discover and compose all schemas in the current directory:

shell
nitro fusion compose --archive gateway.far

Watch mode for local development:

shell
nitro fusion compose --watch

Watch mode observes local schema directories and paired remote settings files. A watched change triggers recomposition and fetches the remote schemas again. Nitro does not poll remote URLs.

Exclude fields tagged as experimental or internal:

shell
nitro fusion compose \
  --exclude-by-tag experimental \
  --exclude-by-tag internal-only \
  --archive gateway.far

nitro fusion download

Downloads gateway configuration from Nitro Cloud.

Syntax

shell
nitro fusion download [options]

Options

Option Description Default
--api-id The API identifier NITRO_API_ID env var
--stage <name> The stage name NITRO_STAGE env var
--output-file Output file path ./gateway.far

Examples

shell
nitro fusion download \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg== \
  --stage production \
  --output-file gateway.far

nitro fusion migrate

Migrates configuration files from v1 to v2 format.

Syntax

shell
nitro fusion migrate <TARGET> [options]

Arguments

Argument Values Description
TARGET subgraph-config Migration target

Options

Option Description Default
--working-directory (alias: -w) Directory to scan for configuration files Current directory

Behavior

The subgraph-config target converts subgraph-config.json files to the schema-settings.json format. The migration maps subgraph to name and http.baseAddress to transports.http.url. If a schema-settings.json file already exists in the target directory, the migration skips that directory.

Examples

Migrate all subgraph configs in the current directory:

shell
nitro fusion migrate subgraph-config

Migrate configs in a specific directory:

shell
nitro fusion migrate subgraph-config --working-directory ./subgraphs

nitro fusion publish

Publishes a Fusion configuration to a stage on Nitro Cloud. Three input modes are available (mutually exclusive).

Syntax

shell
nitro fusion publish [options]

Options

Option Description Default
--archive (alias: -a) Pre-composed Fusion archive --
--source-schema <name@version> (alias: -s) Source schema identifier. Can be repeated. --
--source-schema-file (alias: -f) Source schema file. Can be repeated. --
--api-id The API identifier NITRO_API_ID env var
--stage <name> Target stage NITRO_STAGE env var
--tag <tag> Version tag NITRO_TAG env var
--working-directory (alias: -w) Working directory Current directory
--source-metadata <json> JSON metadata about the source --

Input Modes

Mode 1: Pre-composed archive. Publish an existing .far file with --archive.

shell
nitro fusion publish \
  --archive gateway.far \
  --tag v1.0.0 \
  --stage production \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==

Mode 2: Source schema files. Compose and publish from local files with --source-schema-file.

shell
nitro fusion publish \
  --source-schema-file ./Products/schema.graphqls \
  --source-schema-file ./Reviews/schema.graphqls \
  --tag v1.0.0 \
  --stage production \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==

Mode 3: Uploaded schema references. Reference previously uploaded schemas by name and version with --source-schema.

shell
nitro fusion publish \
  --source-schema [email protected] \
  --source-schema [email protected] \
  --tag v1.0.0 \
  --stage production \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==

Sub-Commands for Advanced Orchestration

For complex deployment scenarios such as blue-green deployments or manual validation gates, use the publish sub-commands.

nitro fusion publish begin

Request a deployment slot.

Option Description Default
--api-id The API identifier NITRO_API_ID env var
--stage <name> Target stage NITRO_STAGE env var
--tag <tag> Version tag NITRO_TAG env var
--wait-for-approval Wait for manual approval before proceeding false
--source-metadata <json> JSON metadata about the source --
shell
nitro fusion publish begin \
  --stage production \
  --tag v1.0.0 \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==

nitro fusion publish start

Start composition for the deployment.

Option Description Default
--request-id Deployment request ID Uses cached value from begin
shell
nitro fusion publish start

nitro fusion publish validate

Validate the configuration before committing.

Option Description Default
--request-id Deployment request ID Uses cached value from begin
--archive (alias: -a) Fusion archive to validate --
shell
nitro fusion publish validate --archive gateway.far

nitro fusion publish commit

Finalize the deployment.

Option Description Default
--request-id Deployment request ID Uses cached value from begin
--archive (alias: -a) Fusion archive to deploy --
shell
nitro fusion publish commit --archive gateway.far

nitro fusion publish cancel

Cancel the deployment.

Option Description Default
--request-id Deployment request ID Uses cached value from begin
shell
nitro fusion publish cancel

Orchestrated Workflow

A typical advanced deployment follows this sequence:

  1. nitro fusion publish begin -- reserve a deployment slot
  2. nitro fusion publish start -- start composition
  3. nitro fusion publish validate -- validate the configuration
  4. nitro fusion publish commit -- finalize the deployment

To abort at any point, run nitro fusion publish cancel.

nitro fusion run

Starts a local Fusion gateway from an archive file.

Syntax

shell
nitro fusion run <ARCHIVE_FILE> [options]

Options

Argument/Option Description Default
<ARCHIVE_FILE> (required) Path to the Fusion archive --
--port <number> (alias: -p) Port to bind Random unused port

The gateway auto-opens a browser with the Nitro IDE. CORS is enabled, and GraphQL-Preflight and Authorization headers are propagated.

Examples

Start a gateway on a specific port:

shell
nitro fusion run gateway.far --port 5000

Start a gateway on a random port:

shell
nitro fusion run gateway.far

nitro fusion settings set

Modifies composition settings in a Fusion archive.

Syntax

shell
nitro fusion settings set <SETTING_NAME> <SETTING_VALUE> [options]

Options

Option Description Default
--archive (alias: -a) Fusion archive to modify --
--environment <name> (alias: --env, -e) Environment name for variable substitution ASPNETCORE_ENVIRONMENT or Development

Available Settings

Setting Values Description
allow-non-resolvable-interface-objects true, false Allow Apollo interface objects without a resolvable key
global-object-identification true, false Enable Relay-style node queries
include-satisfiability-paths true, false Include paths in satisfiability diagnostics
node-resolution gateway, source-schema Choose who resolves Query.node IDs
shareable-field-runtime-type-routing source-local, common-runtime-types Choose routing for type-conditioned selections on shareable fields
cache-control-merge-behavior ignore, include, include-private How to merge @cacheControl directives
enum-values-merge-behavior auto, strict, union How to merge enum values across source schemas
tag-merge-behavior ignore, include, include-private How to merge @tag directives
exclude-by-tag Comma-separated tags Exclude fields/types by tag
default-list-size Non-negative integer, or null to unset Assumed size for lists without @listSize metadata

Examples

Enable source-schema node resolution on an existing archive:

shell
nitro fusion settings set global-object-identification true --archive gateway.far
nitro fusion settings set node-resolution source-schema --archive gateway.far

Use common-runtime-types routing on an existing archive:

shell
nitro fusion settings set shareable-field-runtime-type-routing common-runtime-types \
  --archive gateway.far

For the behavior of each routing value, see Shareable Abstract Field Routing.

Configure cache control merging:

shell
nitro fusion settings set cache-control-merge-behavior include --archive gateway.far

Exclude tagged fields:

shell
nitro fusion settings set exclude-by-tag experimental,internal-only --archive gateway.far

nitro fusion upload

Uploads a source schema to Nitro Cloud for later composition.

Syntax

shell
nitro fusion upload [options]

Options

Option Description Default
--api-id The API identifier NITRO_API_ID env var
--tag <tag> Version tag NITRO_TAG env var
--source-schema-file (alias: -f) Source schema file (required) --
--working-directory (alias: -w) Working directory Current directory
--source-metadata <json> JSON metadata about the source --

Examples

Upload a source schema:

shell
nitro fusion upload \
  --source-schema-file ./src/Products/schema.graphqls \
  --tag v1.2.3 \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==

Upload with a git commit SHA as the tag:

shell
nitro fusion upload \
  --source-schema-file ./schema.graphqls \
  --tag $(git rev-parse --short HEAD) \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==

nitro fusion validate

Validates a schema against a stage on Nitro Cloud. Two input modes are available (mutually exclusive).

Syntax

shell
nitro fusion validate [options]

Options

Option Description Default
--archive (alias: -a) Fusion archive to validate --
--source-schema-file (alias: -f) Source schema files. Can be repeated. --
--api-id The API identifier NITRO_API_ID env var
--stage <name> Stage to validate against NITRO_STAGE env var

Examples

Validate from source schema files:

shell
nitro fusion validate \
  --source-schema-file ./Products/schema.graphqls \
  --stage production \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==

Validate from an archive:

shell
nitro fusion validate \
  --archive gateway.far \
  --stage production \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==

Schema Settings File Reference

Each source schema requires a schema-settings.json file alongside its .graphqls schema file. This file configures the source schema name, transport settings, and per-environment variable substitutions.

json
{
  "name": "products-api",
  "transports": {
    "http": {
      "url": "{{API_URL}}"
    }
  },
  "environments": {
    "development": {
      "API_URL": "http://localhost:5110/graphql"
    },
    "production": {
      "API_URL": "https://api.example.com/graphql"
    }
  }
}

Fields

`name`

Type: string (required)

The unique source schema name used in composition. Must match the name used in #!csharp builder.AddGraphQL("products-api").

`transports.http.url`

Type: string (required)

The URL template for the subgraph's GraphQL endpoint. Use {{VARIABLE_NAME}} for environment-specific substitution.

`transports.http.devUrl`

Type: string (optional)

The URL a local development environment reaches this source schema at. It supports the same {{VARIABLE_NAME}} substitution as url.

devUrl is only consulted for source schemas that do not run in the local AppHost when composing for a local Aspire run. Every other composition ignores it, including nitro fusion compose, publish, and upload. It never survives composition either: the composed configuration always carries a url only.

json
{
  "name": "products-api",
  "transports": {
    "http": {
      "url": "https://products.example.com/graphql",
      "devUrl": "http://localhost:5110/graphql"
    }
  }
}

When such a source schema defines no devUrl, composition falls back to its url and logs the warning SOURCE_SCHEMA_DEV_URL_MISSING, because a deployed URL is often not reachable from a developer machine. When the URL that is selected for such a source schema references a variable that its environment does not define, composition logs the warning SOURCE_SCHEMA_URL_VARIABLE_UNRESOLVED, falls back from an unresolvable devUrl to the url, and composes an unresolvable url as it is. Neither case fails the composition.

`transports.websockets.devUrl`

Type: string (optional)

The same as transports.http.devUrl, for the WebSocket URL of the source schema. A source schema that does not run in the local AppHost and defines no devUrl here keeps its configured transports.websockets.url without a warning.

`transports.http.clientName`

Type: string (optional, defaults to "fusion")

The named HTTP client the gateway uses to communicate with this subgraph. Must match what the gateway configures via #!csharp builder.Services.AddHttpClient("fusion").

`transports.subscriptions.transport`

Type: "sse" | "ws" (optional)

The transport protocol for real-time subscriptions. Defaults to "sse" (Server-Sent Events).

`extensions.nitro.apiId`

Type: string (optional)

The Nitro Cloud API identifier for this subgraph. Required when using Nitro Cloud features like upload or publish.

`environments`

Type: object (optional)

Per-environment variable substitutions. The active environment is selected via the --environment flag during composition or via the ASPNETCORE_ENVIRONMENT environment variable.

json
{
  "environments": {
    "development": {
      "API_URL": "http://localhost:5110/graphql"
    },
    "staging": {
      "API_URL": "https://staging.example.com/graphql"
    },
    "production": {
      "API_URL": "https://api.example.com/graphql"
    }
  }
}

When composing with --environment staging, all {{API_URL}} placeholders resolve to the staging URL.

`preprocessor.inferShareable`

Type: boolean (optional, defaults to false)

When true, composition automatically marks every field on this source schema that is resolvable by multiple source schemas as @shareable, skipping INVALID_FIELD_SHARING validation. Fields that are part of a @key, marked @internal or @inaccessible, or defined on the subscription type are not marked automatically.

Environment Variables

The Nitro CLI reads these environment variables as defaults for their corresponding options.

Variable Corresponding Option Used By
NITRO_API_ID --api-id download, publish, upload, validate
NITRO_API_KEY --api-key download, publish, upload, validate
NITRO_STAGE --stage download, publish, validate
NITRO_TAG --tag publish, upload
NITRO_FUSION_CONFIG_FILE --archive publish
NITRO_OUTPUT_FILE --output-file download
NITRO_REQUEST_ID --request-id publish begin/start/validate/commit/cancel
ASPNETCORE_ENVIRONMENT --environment compose, settings set

Set these in CI/CD pipelines to avoid passing values as command-line arguments.

Exit Codes

  • 0 -- success
  • Non-zero -- failure (error details printed to stderr)

Website Content Docs Fusion Composition (`website/content/docs/fusion/composition.md`)


title: "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.

Each subgraph is a standalone GraphQL service that exposes a source schema. On its own, a source schema cannot tell whether its types are compatible with the rest of your system, whether the fields it declares actually overlap correctly with another team's source schema, or whether every entity it references can be resolved somewhere. Composition is the step that answers these questions before anything is deployed.

The pipeline runs on the developer machine or in CI. It validates each source schema individually, checks that the schemas are compatible with each other, merges them into one unified schema, validates the merged result, and finally checks that every reachable field can be resolved by at least one subgraph. If any of those phases produces errors, composition fails and emits diagnostic log codes that point to the exact rule that was violated.

The output of a successful composition is a Fusion archive: a package built around the execution schema. Lookup, key, and ownership information is embedded directly into the schema as directives, so the gateway has everything it needs to plan and execute distributed queries by reading the schema alone. The gateway loads this archive at startup and never sees raw source schemas, only the composed result.

Composition is the safety net of a Fusion deployment. It catches schema conflicts (mismatched types, broken @key references, unreachable fields, ambiguous ownership) at build time, not at request time. A query that succeeds against the gateway is guaranteed to be answerable by your fleet of subgraphs because composition has already proven it.

If you are still onboarding, start with Getting Started. The pages on Entities and Lookups and Field Ownership cover the directives most often referenced by the log codes below.

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.

mermaid
flowchart TD
    A[Source Schemas] --> B[1. Parse]
    B --> C[2. Preprocess]
    C --> D[3. Enrich]
    D --> E[4. Validate Source Schemas]
    E --> F[5. Pre-Merge Validation]
    F --> G[6. Merge]
    G --> H[7. Post-Merge Validation]
    H --> I[8. Validate Satisfiability]
    I --> J[Execution Schema]
    B -.fail.-> X[Composition Error]
    C -.fail.-> X
    D -.fail.-> X
    E -.fail.-> X
    F -.fail.-> X
    G -.fail.-> X
    H -.fail.-> X
    I -.fail.-> X

1. Parse Source Schemas

Parses each source schema into an internal representation. This phase catches syntactically invalid SDL (INVALID_GRAPHQL).

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

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

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

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

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

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

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 reports UNSATISFIABLE_QUERY_PATH. This is normally an error. When source-schema node resolution has a usable dispatcher but does not cover a composite Node type, Fusion reports a warning because the source resolver can return null or an error at runtime.

Cost Metadata Derivation

Fusion composition records each compatible source usage as an internal @fusion__cost or @fusion__listSize provenance entry, folds the source values, and projects the result as a public directive. A public @cost or @listSize directive is emitted only when at least one source has a compatible usage. The internal entries identify the source schema and preserve its declared values. An unannotated serving source contributes the coordinate default to the public @cost fold without adding an internal provenance entry.

Both folds only consider serving sources: a source schema that resolves the field itself. A source that provides the field only as partial (for example an Apollo Federation @external field returned through @provides) is not a serving source for either fold, so an unannotated partial member contributes neither the coordinate's default weight nor a gap that widens the @listSize bound. Its own @cost or @listSize usage, when declared, still folds in and is still recorded as its own provenance entry.

A usage without a local directive definition receives the canonical definition during composition. A locally declared definition can use a compatible subset of the canonical arguments.

For @cost, the public weight is the maximum effective weight across every serving source. A declared weight is effective as written. An unannotated serving source contributes the default for the coordinate:

Coordinate Default weight
Object type 1
Scalar or enum type 0
Output field returning an object, interface, or union 1
Output field returning a scalar or enum 0
Argument or input field with an input-object value 1
Argument or input field with a scalar, enum, or ID 0

For example, a composite-typed coordinate with weight -7 in one source and no declared weight in another source derives the public weight 1. Given these two source schemas:

graphql
# Schema A
type Query {
  book: Book @cost(weight: "-7")
}

type Book {
  id: ID
}
graphql
# Schema B
type Query {
  book: Book
}

type Book {
  id: ID
}

Composition folds Query.book to the public weight 1 and records schema A's declared value as its own provenance entry; schema B contributes the coordinate default without one:

graphql
type Query @fusion__type(schema: A) @fusion__type(schema: B) {
  book: Book
    @cost(weight: "1")
    @fusion__cost(schema: A, weight: "-7")
    @fusion__field(schema: A)
    @fusion__field(schema: B)
}

The public @listSize directive applies these folds:

Argument Fold
assumedSize Maximum over sources that declare it.
slicingArguments and sizedFields Deterministic union in first-seen order.
requireOneSlicingArgument Apply each source definition's default when omitted, then true if any.
slicingArgumentDefaultValue Maximum over sources that declare it.

The name unions are preserved as declared. A name that does not match an argument or child field in the composite schema contributes no runtime value, and list-size selection continues through its remaining fallbacks.

For assumedSize, given these two source schemas:

graphql
# Schema A
type Query {
  field: [Int] @listSize(assumedSize: 10)
}
graphql
# Schema B
type Query {
  field: [Int] @listSize(assumedSize: 5)
}

Composition folds assumedSize to the maximum of the two, 10, and records both declared values as provenance entries:

graphql
type Query @fusion__type(schema: A) @fusion__type(schema: B) {
  field: [Int]
    @listSize(assumedSize: 10)
    @fusion__field(schema: A)
    @fusion__field(schema: B)
    @fusion__listSize(schema: A, assumedSize: 10)
    @fusion__listSize(schema: B, assumedSize: 5)
}

The slicingArgumentDefaultValue argument is optional. A source can use a spec-only @listSize definition that omits this ChilliCream extension, and it composes without a warning. If every source omits the value, the public directive omits it too.

Composition reports a normal INVALID_GRAPHQL error for invalid locally declared definitions. Applying an argument-less local @cost definition reports The @cost directive must have a 'weight' argument of type String., the source schema name, and the usage coordinate.

Default List Size

The assumed size for a list field that carries no applicable @listSize information is a composition setting, not a gateway runtime option. Set SourceSchemaMergerOptions.DefaultListSize on the composer:

csharp
var options = new SchemaComposerOptions
{
    Merger =
    {
        DefaultListSize = 100
    }
};

When set, composition writes the value onto the execution schema with a schema-level @fusion__cost_options(defaultListSize:) directive, and folds it into the effective assumedSize of a field for which at least one serving source declares a @listSize, so the public @listSize reflects the higher, sound bound. For a field no source annotates, composition emits no @listSize and the gateway applies the value directly from @fusion__cost_options. When absent (the default), no @fusion__cost_options usage is emitted and the gateway treats an unannotated list as unbounded.

See Cost Analysis for gateway enforcement, reporting, and options.

Common Scenarios

A few rules account for most composition failures. Knowing the shape of the error helps you spot the cause quickly.

Two source schemas return incompatible types for the same field. If Product.price is Float in one source schema and Int in another, composition fails with OUTPUT_FIELD_TYPES_NOT_MERGEABLE. Scalar and enum return types must match exactly, while object, interface, and union types merge when one declared type is a supertype of the others (for example, Product and a #!sdl union FeaturedItem = Product merge to FeaturedItem). Align the types when no common supertype exists.

A field is defined in multiple source schemas without @shareable. Defining User.name in both Accounts and Reviews without marking it @shareable produces INVALID_FIELD_SHARING.

graphql
# Accounts source schema
type User @key(fields: "id") {
  id: ID!
  name: String!
}

# Reviews source schema
type User @key(fields: "id") {
  id: ID!
  name: String!
}

Composition emits one diagnostic per non-shareable definition, so both source schemas are reported:

The field 'User.name' in schema 'Accounts' must be shareable.

The field 'User.name' in schema 'Reviews' must be shareable.

Add @shareable to User.name in both source schemas that define it. See Field Ownership for when sharing is appropriate.

If you do not want to mark shared fields manually, you can set the per-source-schema preprocessor.inferShareable option in that schema's schema-settings.json to true. When enabled, the composition automatically marks every field on that source schema that is also exposed by other source schemas as @shareable. Only enable inferShareable, if there are other processes in place that ensure fields are semantically the same across source schemas.

@key references a field that does not exist on the type. A #!sdl @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 for the rules around key selection sets.

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

Code Message How to resolve
CONFLICTING_SOURCE_SCHEMA_NAME '{0}' conflicts with the existing source schema name '{1}'. Either rename '{0}' to '{1}' if they're the same, or rename '{0}' to something else if they're different. Two source schemas were registered under names that the composition treats as the same identity. Rename one of them in your composition configuration so each source schema has a unique name.
DISALLOWED_INACCESSIBLE (Message varies by element kind: built-in scalar, introspection type, introspection field, introspection argument, or built-in directive argument.) Built-in scalars, introspection types and their members, and built-in directive arguments cannot be marked @inaccessible. Remove the directive from the offending element in the named source schema.
EMPTY_MERGED_ENUM_TYPE The merged enum type '{0}' is empty. Every value of the enum was excluded by @inaccessible or tag filters, leaving nothing to merge. Either expose at least one enum value or remove the type entirely.
EMPTY_MERGED_INPUT_OBJECT_TYPE The merged input object type '{0}' is empty. All fields of the input type were excluded after merging. Expose at least one input field or remove the input type from your source schemas.
EMPTY_MERGED_INTERFACE_TYPE The merged interface type '{0}' is empty. All fields of the interface were filtered out by @inaccessible or tag exclusions. Expose at least one field on the interface, remove the filters that hide them, or drop the interface from the schema.
EMPTY_MERGED_OBJECT_TYPE The merged object type '{0}' is empty. Every field on the object type was excluded. Expose at least one field, or remove the type if it is no longer needed.
EMPTY_MERGED_UNION_TYPE The merged union type '{0}' is empty. All members of the union were excluded by @inaccessible or tag exclusions. Expose at least one member type, remove the filters that hide them, or remove the union.
ENUM_TYPE_DEFAULT_VALUE_INACCESSIBLE The default value of '{0}' references the inaccessible enum value '{1}'. A default points to an enum value that is hidden from the composed schema. Either change the default to a value that is accessible or remove @inaccessible from the referenced enum value.
ENUM_VALUES_MISMATCH The enum type '{0}' in schema '{1}' must define the value '{2}'. An enum is defined in more than one source schema and one of them is missing a value the others declare. Add the missing value to the named source schema so all definitions agree. By default composition is strict per the composite schema spec; when at least one Apollo Federation connector source schema is part of the composition, enums used only in output positions merge their values by union and do not raise this error. The enumValuesMergeBehavior merger setting (auto, strict, union) overrides the detection in either direction; enums used in any input position (including through @key or @requires) always require identical value sets.
EXTERNAL_ARGUMENT_DEFAULT_MISMATCH The default value '{0}' of external argument '{1}' in schema '{2}' differs from the default value of '{3}' in schema '{4}'. The same argument has different defaults in the external definition and the owning source schema. Align the default values across both source schemas.
EXTERNAL_ARGUMENT_MISSING The external field '{0}' in schema '{1}' must define the argument '{2}'. An @external field declaration is missing an argument that exists on the canonical definition. Add the argument to the external declaration with a matching type.
EXTERNAL_ARGUMENT_TYPE_MISMATCH The argument '{0}' on external field '{1}' in schema '{2}' has a different type ({3}) than it does in schema '{4}' ({5}). The argument types of an @external declaration and the owning source schema disagree. Change the argument type in one of the source schemas so the signatures match exactly, including nullability.
EXTERNAL_MISSING_ON_BASE The external field '{0}' in schema '{1}' is not defined (non-external) in any other schema. A field marked @external has no non-external definition anywhere. Add the canonical definition in another source schema, or remove @external if this source schema is meant to own the field. See Field Ownership.
EXTERNAL_ON_INTERFACE The interface field '{0}' in schema '{1}' must not be marked as external. @external is not valid on interface fields. Remove @external from the interface field; if you need to mark concrete implementations as external, place the directive on the implementing object types instead.
EXTERNAL_OVERRIDE_COLLISION The external field '{0}' in schema '{1}' must not be annotated with the @override directive. A field cannot be both @external and @override. Decide which source schema owns the field and apply only the appropriate directive.
EXTERNAL_PROVIDES_COLLISION The external field '{0}' in schema '{1}' must not be annotated with the @provides directive. @external declares a field as not owned here, while @provides declares it as supplied here. Remove one of the directives so the ownership story is unambiguous.
EXTERNAL_REQUIRE_COLLISION The external field '{0}' in schema '{1}' must not have arguments that are annotated with the @require directive. An @external field cannot consume @require arguments because it is not actually executed in this subgraph. Remove @require from the arguments, or move the field to a source schema where it is owned.
EXTERNAL_TYPE_MISMATCH The external field '{0}' in schema '{1}' has a different type ({2}) than it does in schema '{3}' ({4}). The return type of the @external declaration differs from the canonical definition. Align the field type and nullability in both source schemas.
EXTERNAL_UNUSED The external field '{0}' in schema '{1}' is not referenced by a @provides directive in the schema. An @external field is declared but no @provides in this schema references it. Remove the unused declaration, or add a @provides that references the field. See Field Ownership.
FEDERATION_DIRECTIVE_NOT_SUPPORTED The @{0} directive is not supported. The schema uses an Apollo Federation directive that Fusion does not support. Remove the directive or replace it with the equivalent Fusion construct.
FEDERATION_V1_NOT_SUPPORTED Federation v1 is not supported. Upgrade the source schema to Federation v2 (or to native Fusion directives) before running composition.
FIELD_ARGUMENT_TYPES_NOT_MERGEABLE The argument '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'. The argument has a fundamentally different type (different named type or different list/non-null structure) across source schemas. Pick one canonical type and update the other source schema to match.
FIELD_WITH_MISSING_REQUIRED_ARGUMENT The argument '{0}' must be defined as required in schema '{1}'. Arguments marked with @require are treated as non-required. The argument is required in some source schemas and optional or @require-driven in another. Mark the argument as required in the named source schema, or align all source schemas on the same nullability.
IMPLEMENTED_BY_INACCESSIBLE The field '{0}' implementing interface field '{1}' is inaccessible in the composed schema. An object type implements an interface field, but its implementation is hidden. Either expose the implementing field or hide the interface field as well.
IMPLEMENT_WITHOUT_DEFAULT The field '{0}' is marked with @implement in schema '{1}', but no applicable default implementation exists for it. Remove @implement, or add an applicable default through an interface-object stand-in. Use @implement only when the declaration intentionally replaces that projected default. See Replace a Projected Default.
INPUT_FIELD_DEFAULT_MISMATCH The default value '{0}' of input field '{1}' in schema '{2}' differs from the default value of '{3}' in schema '{4}'. The same input field has different defaults in two source schemas. Align the default value across all definitions of the input type.
INPUT_FIELD_TYPES_NOT_MERGEABLE The input field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'. An input field has a different type structure (named type, list, or nullability) across source schemas. Decide on the canonical type and update the other source schema.
INPUT_WITH_MISSING_REQUIRED_FIELDS The input type '{0}' in schema '{1}' must define the required field '{2}'. One source schema requires a field on an input type that another source schema omits. Add the field (with the same type) to the named source schema so the input shape is consistent.
INPUT_WITH_MISSING_ONEOF The input type '{0}' in schema '{1}' must be annotated with the '@oneOf' directive, since the same type has been annotated with it in schema '{2}'. The same input type is @oneOf in one source schema but not in another. Add @oneOf to the input type in the named source schema (or remove it from the other) so all definitions agree.
INTERFACE_FIELD_NO_IMPLEMENTATION The merged object type '{0}' must implement the field '{1}' on interface '{2}'. After merging, an object type declares it implements the interface but does not provide every field the interface requires. Add the missing field to the object type, or stop implementing the interface.
INTERFACE_OBJECT_FIELD_REQUIRES_IMPLEMENT The field '{0}' provided by schema '{1}' replaces the default implementation contributed to interface '{2}'. Mark it with @implement to keep this implementation, or remove it to adopt the default. Add @implement when this source intentionally replaces the projected default. Otherwise, remove the field declaration so the type inherits the default. See Replace a Projected Default.
INTERFACE_OBJECT_KEY_MISMATCH The @interfaceObject stand-in '{0}' in schema '{1}' declares the key '{2}', which does not match any key declared on the interface it stands in for. Change the stand-in key to one declared or inferred on the same-named interface. The key fields and selection shape must match. See Interface Objects.
INTERFACE_OBJECT_KEY_MISSING The @interfaceObject stand-in '{0}' in schema '{1}' must declare at least one @key directive. Add at least one @key that matches a key on the same-named interface and can populate the stand-in lookup. See Interface Objects.
INTERFACE_OBJECT_NO_INTERFACE The type '{0}' is declared as an @interfaceObject stand-in, but no source schema defines '{0}' as an interface. Define the same-named interface in a concrete-aware source schema, or remove @interfaceObject if the type is not a stand-in. See Interface Objects.
INVALID_ENUM_VALUES_MERGE_BEHAVIOR The enum values merge behavior '{0}' is invalid. Set the enumValuesMergeBehavior merger setting in the composition settings to Auto, Strict, or Union.
INVALID_FIELD_SHARING The field '{0}' in schema '{1}' must be shareable. The same non-key field is owned by more than one source schema without an explicit sharing contract. Add @shareable to the field (or to the enclosing type, which applies to all of its fields) in every source schema that defines it, or move ownership to a single source schema. See Field Ownership.
INVALID_GRAPHQL Invalid GraphQL in source schema. Exception message: {0}. The source schema does not parse as valid GraphQL. Read the included parser exception, fix the SDL in the offending source schema, and re-export. See Getting Started for how to export a source schema.
INVALID_NODE_RESOLUTION Source-schema node resolution requires global object identification to be enabled. Or: The node resolution mode '{0}' is invalid. Enable Global Object Identification when you select source-schema resolution. For programmatic composition, use NodeResolution.Gateway or NodeResolution.SourceSchema. See Configure node resolution.
INVALID_PROJECTED_FIELD_SHARING The default field '{0}' on type '{1}' is contributed by unrelated interfaces {2}; every contributing @interfaceObject declaration must be marked @shareable. Mark every contributing stand-in field @shareable only when its type and semantics match, or remove the conflicting default from all but one interface. See Share Defaults from Unrelated Interfaces.
INVALID_SHAREABLE_FIELD_RUNTIME_TYPE_ROUTING The shareable field runtime type routing mode '{0}' is invalid. Set ShareableFieldRuntimeTypeRouting to SourceLocal or CommonRuntimeTypes. See Composition Settings for the Aspire setting.
INVALID_SHAREABLE_USAGE The field '{0}' in schema '{1}' must not be marked as shareable. @shareable was applied where it is not allowed (for example, on a field that is already governed by another ownership directive). Remove @shareable from the field. See Field Ownership.
IS_INVALID_FIELDS The @is directive on argument '{0}' in schema '{1}' specifies an invalid field selection against the composed schema. The field selection map in @is(field: ...) does not resolve against the entity's fields. Update it to reference fields that actually exist on the parent type. See Entities and Lookups.
IS_INVALID_FIELD_TYPE The @is directive on argument '{0}' in schema '{1}' must specify a string value for the 'field' argument. The field argument was given as a non-string literal. Pass it as a quoted string, for example #!sdl @is(field: "id").
IS_INVALID_SYNTAX The @is directive on argument '{0}' in schema '{1}' contains invalid syntax in the 'field' argument. The string value for field is not a valid field selection map. Rewrite it as a syntactically valid field selection map.
IS_INVALID_USAGE The @is directive on argument '{0}' in schema '{1}' is invalid because the declaring field is not a lookup field. @is is only valid on arguments of lookup fields. Move @is to a lookup, or annotate the declaring field with @lookup. See Entities and Lookups.
KEY_DIRECTIVE_IN_FIELDS_ARGUMENT A @key directive on type '{0}' in schema '{1}' references field '{2}', which must not include directive applications. The fields selection of @key cannot apply directives to the selected fields. Remove the directive applications from the selection set. See Entities and Lookups.
KEY_FIELDS_SELECT_INVALID_TYPE A @key directive on type '{0}' in schema '{1}' references field '{2}', which must not be a list, interface, or union type. Key fields must resolve to scalar, enum, or object types, not lists, interfaces, or unions. Pick a different field for the key, or model the key as a scalar identifier.
KEY_INVALID_ARGUMENTS A @key directive on type '{0}' in schema '{1}' specifies invalid arguments. {2} Key fields support constant arguments. Remove or correct any argument that is unknown, has an incompatible value type, or leaves a required argument unsupplied.
KEY_INVALID_FIELDS A @key directive on type '{0}' in schema '{1}' specifies an invalid field selection against the composed schema. The fields selection refers to fields that do not exist on the type. Add the missing fields to the type or correct the selection set. See Entities and Lookups.
KEY_INVALID_FIELDS_TYPE A @key directive on type '{0}' in schema '{1}' must specify a string value for the 'fields' argument. The fields argument was given as a non-string literal. Pass it as a quoted string, for example #!sdl @key(fields: "id").
KEY_INVALID_SYNTAX A @key directive on type '{0}' in schema '{1}' contains invalid syntax in the 'fields' argument. The string value for fields is not a valid GraphQL selection set. Rewrite it as a syntactically valid selection.
LOOKUP_RETURNS_LIST The lookup field '{0}' in schema '{1}' must not return a list. A @lookup field returned a list, which prevents the gateway from mapping a key to a single entity. Change the return type to a single nullable entity. See Entities and Lookups.
NON_NULL_INPUT_FIELD_IS_INACCESSIBLE The non-null input field '{0}' in schema '{1}' must be accessible in the composed schema. A required input field was hidden by @inaccessible, which would make the input impossible to construct from the gateway. Either make the field nullable or expose it.
NO_QUERIES The merged query type has no accessible fields. After merging and applying accessibility filters, no query fields remain. Expose at least one query field, or remove @inaccessible from the queries you intend to publish.
OPT_IN_FEATURE_STABILITY_MISMATCH The opt-in feature '{0}' has a different stability in schema '{1}' ({2}) than it does in schema '{3}' ({4}). Two or more source schemas declare the same opt-in feature with different stability values. Align the stability for that feature across all subgraphs that declare it using OptInFeatureStability("featureName", "stability") so every declaration uses the same string.
OUTPUT_FIELD_TYPES_NOT_MERGEABLE The output field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'. The field's return type cannot be unified across source schemas: leaf types (scalar or enum) differ in name or kind, the list shape differs, or composite types (object, interface, or union) share no common supertype. Nullability differences and composite types with a common supertype merge automatically; otherwise align the return types.
OVERRIDE_FROM_SELF The @override directive on field '{0}' in schema '{1}' must not reference the same schema. @override(from: "...") cannot point at the source schema that owns the directive. Set from to the name of the source schema the field is being taken from. See Field Ownership.
OVERRIDE_ON_INTERFACE The interface field '{0}' in schema '{1}' must not be annotated with the @override directive. @override cannot be applied to interface fields. Move the directive to the implementing object types if you need to take ownership of a specific implementation. See Field Ownership.
PROVIDES_DIRECTIVE_IN_FIELDS_ARGUMENT The @provides directive on field '{0}' in schema '{1}' references field '{2}', which must not include directive applications. The selection in @provides(fields: ...) cannot apply directives. Remove the directive applications from the selection. See Field Ownership.
PROVIDES_FIELDS_HAS_ARGUMENTS The @provides directive on field '{0}' in schema '{1}' references field '{2}', which must not have arguments. A field selected by @provides is being called with arguments, which is not allowed. Pick an argument-less field or restructure the schema so the provided field needs no arguments.
PROVIDES_FIELDS_MISSING_EXTERNAL The @provides directive on field '{0}' in schema '{1}' references field '{2}', which must be marked as external. @provides only makes sense when the referenced field is @external here. Add @external to the referenced field in the same source schema. See Field Ownership.
PROVIDES_INVALID_FIELDS The @provides directive on field '{0}' in schema '{1}' specifies an invalid field selection. The selection points to fields that do not exist on the returned type. Update the selection so it matches fields that the returned type actually defines.
PROVIDES_INVALID_FIELDS_TYPE The @provides directive on field '{0}' in schema '{1}' must specify a string value for the 'fields' argument. The fields argument was given as a non-string literal. Pass it as a quoted string.
PROVIDES_INVALID_SYNTAX The @provides directive on field '{0}' in schema '{1}' contains invalid syntax in the 'fields' argument. The selection set syntax in fields is invalid. Rewrite it as a valid GraphQL selection set.
PROVIDES_ON_NON_COMPOSITE_FIELD The field '{0}' in schema '{1}' includes a @provides directive, but does not return a composite type. @provides only applies when a field returns an object, interface, or union, because the directive describes which sub-fields are also delivered. Remove @provides, or change the field to return a composite type.
QUERY_ROOT_TYPE_INACCESSIBLE The root query type in schema '{0}' must be accessible. The query root cannot be marked @inaccessible. Remove the directive from the root query type.
REFERENCE_TO_DEPRECATED_TYPE The merged field '{0}' in type '{1}' cannot reference the deprecated type '{2}'. Either deprecate the field or change its return type. A field that is not deprecated returns a deprecated object type. Merging propagates an object type's deprecation from every source schema that declares it, so the composed schema can pair a deprecated type with a field that no source schema deprecated. Either deprecate the field as well, or change its return type.
REFERENCE_TO_INACCESSIBLE_TYPE (Message varies by reference site: field argument, input field, or output field.) A publicly accessible position (a field argument, input field, or output field) references a type that has been hidden from the composed schema. Either expose the referenced type or change the position to use a publicly accessible type.
REFERENCE_TO_INTERNAL_TYPE The merged field '{0}' in type '{1}' cannot reference the internal type '{2}'. A public field returns an internal type, which would leak the internal type into the composed schema. Either mark the field as internal as well, or change the return type to a non-internal one.
REQUIRE_INVALID_FIELDS The @require directive on argument '{0}' in schema '{1}' specifies an invalid field selection against the composed schema. The field selection map in @require(field: ...) references fields that the parent type does not expose. Update it to reference fields that exist on the parent. See Entities and Lookups.
REQUIRE_INVALID_FIELD_TYPE The @require directive on argument '{0}' in schema '{1}' must specify a string value for the 'field' argument. The field argument must be a string literal. Pass it as a quoted string.
REQUIRE_INVALID_SYNTAX The @require directive on argument '{0}' in schema '{1}' contains invalid syntax in the 'field' argument. The field value is not a valid field selection map. Rewrite it as a syntactically valid field selection map.
ROOT_MUTATION_USED The root mutation type in schema '{0}' must be named 'Mutation'. Fusion requires the root mutation type to use the canonical name Mutation. Rename the type in the named source schema. A type named Mutation must not exist unless it is the root mutation type.
ROOT_QUERY_USED The root query type in schema '{0}' must be named 'Query'. Fusion requires the root query type to use the canonical name Query. Rename the type in the named source schema. A type named Query must not exist unless it is the root query type. See Getting Started for the expected schema layout.
ROOT_SUBSCRIPTION_USED The root subscription type in schema '{0}' must be named 'Subscription'. Fusion requires the root subscription type to use the canonical name Subscription. Rename the type in the named source schema. A type named Subscription must not exist unless it is the root subscription type.
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 (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. This is an error unless the source-schema node-resolution exception described under Warnings applies. See Diagnosing UNSATISFIABLE_QUERY_PATH Diagnostics for how to read the message and fix the underlying gap.

Warnings

Code Message How to resolve
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.
SOURCE_SCHEMA_DEV_URL_MISSING The source schema '{0}' does not specify a 'devUrl' for its HTTP transport. The composed configuration uses its 'url', which might not be reachable from the local development environment. Emitted only when composing for a local Aspire run, never by nitro fusion compose or publish. A source schema that is not running in the local AppHost defines no devUrl, so the deployed url is used. See What Happens When a Gateway Starts.
SOURCE_SCHEMA_URL_VARIABLE_UNRESOLVED The '{0}' of the source schema '{1}' contains variables that are not defined for the environment '{2}'. {3} Emitted only when composing for a local Aspire run, never by nitro fusion compose or publish. A \{\{VARIABLE\}\} in the source schema's url or devUrl could not be resolved against the selected environment, so composition fell back leniently instead of failing. See What Happens When a Gateway Starts.
SPECIFIED_BY_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.
UNSATISFIABLE_QUERY_PATH A source-schema Node dispatcher does not cover a composite Node type. In source-schema node-resolution mode, Fusion allows the source resolver to report the uncovered type as null or an error at runtime. Confirm that this compatibility behavior is intended.

Diagnosing UNSATISFIABLE_QUERY_PATH Diagnostics

Most composition diagnostics 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. It is an error by default. Source-schema node resolution can downgrade uncovered composite Node types to warnings when a usable source dispatcher exists.

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

The top-level message names the unreachable field:

text
Unable to access the field 'Order.shippingAddress'.

Underneath it, indented with two spaces per level, are the nested errors that the validator collected for each option it tried. Common nested messages and what they signal:

Nested message shape What it means
Unable to transition between schemas '<from>' and '<to>' for access to field '<coordinate>'. The validator wanted to hop from one source schema to another to fetch the field, but transitioning failed. The reason for the failure appears as a further nested child under this message.
No lookups found for type '<type>' in schema '<schema>'. A specific reason a transition failed: the target source schema has no @lookup for the entity at all.
Unable to satisfy the requirement '<selection>' for lookup '<lookup>' in schema '<schema>'. A @lookup on the target source schema needs key fields that the parent type does not expose along this path.
Unable to satisfy the requirement '<selection>' on field '<coordinate>'. A field's @require argument depends on fields the parent type does not expose along this path.
Type '<type>' implements the 'Node' interface, but no source schema provides a non-internal 'Query.node<Node>' lookup field for this type. A type implements Node but no source schema exposes a #!sdl node(id: ID!) lookup that returns it.

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

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:

text
Unable to access the field 'Order.shippingAddress' on path 'accounts:Query.viewer<User> -> accounts:User.orders<Order>'.

Each path segment has the shape <schemaName>:<TypeName>.<fieldName><<FieldTypeName>>, and segments are joined with ->. The form is verbose, but unambiguous: it tells you which source schema served each hop and which field type the validator ended up holding.

Set the option via the CLI flag during composition:

shell
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

  • 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 <entity>(id: ID!): <Entity>) annotated with @lookup. See Entities and Lookups.
  • 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.
  • @require reaches for a field that is not on the path. A @require(field: "...") references parent fields that the validator cannot resolve given the path it took. Either expose the required field on the parent type along that path, or rewrite the requirement.
  • Node interface without a node lookup. A type implements Node but no source schema exposes a #!sdl node(id: ID!): Node lookup that returns it. Add one in any source schema that owns the entity.

When you fix the root cause, both the top-level UNSATISFIABLE_QUERY_PATH diagnostic and its nested children disappear together.

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 <schema>-settings.json file:

json
{
  "satisfiability": {
    "ignoredNonAccessibleFields": {
      "Order.shippingAddress": [
        "accounts:Query.viewer<User> -> accounts:User.orders<Order>"
      ]
    }
  }
}

The key is <TypeName>.<fieldName>. Each value is a list of paths (matching exactly what IncludeSatisfiabilityPaths would print for that field) where the validator should skip the check. The field name and at least one of its paths must match for the error to be silenced.

This setting lives in the per-source-schema settings file (e.g. accounts-settings.json), not in the archive-level composition settings.

[!WARNING]
Use this sparingly. It silences a real diagnostic without fixing the underlying gap, so leaving entries in place lets unsatisfiable shapes ship.


Website Content Docs Fusion Cost Analysis (`website/content/docs/fusion/cost-analysis.md`)


title: Cost Analysis

Fusion estimates an operation's field cost and type cost before operation planning. When maximum response size is enabled, it also estimates the response-field count. The analyzer compiles and caches a cost plan, then evaluates that plan against the request's coerced variables.

Cost metadata comes from the public @cost and @listSize directives in the execution schema. Fusion composition derives these directives from source-schema metadata as described in Cost Metadata Derivation.

How Cost Is Calculated

The analyzer produces these metrics:

  • Field cost represents resolver and input-processing work.
  • Type cost represents the weighted objects in the response.
  • Maximum response size represents the maximum number of response-object fields when MaxResponseSize is enabled.

Weights come from the public @cost directive that composition derives for each coordinate. An unannotated coordinate falls back to its default weight; see Cost Metadata Derivation for the default per coordinate kind.

Field Cost Example

Walk a query for a book field weighted 10, returning a title and an author:

graphql
{
  book {
    # 10 (weight: "10")
    title # 0 (scalar, unannotated)
    author {
      # 1 (object field, unannotated)
      name # 0 (scalar, unannotated)
    }
  }
}
# Field cost: 10 + 0 + 1 + 0 = 11

Add pagination and the weight of every field below a sized field multiplies by the list size. This books field is weighted 10 and carries #!sdl @listSize(assumedSize: 50, slicingArguments: ["first", "last"], slicingArgumentDefaultValue: 10, sizedFields: ["edges", "nodes"]), evaluated with first: 50:

graphql
{
  books(first: 50) {
    # 10 (weight: "10")
    edges {
      # 1 (object field, paid once)
      node {
        # 1 x 50 (object field, once per edge)
        title # 0 x 50 (scalar)
        author {
          # 1 x 50 (object field, once per node)
          name # 0 x 50 (scalar)
        }
      }
    }
  }
}
# Field cost: 10 + 1 + 1 x 50 + 0 x 50 + 1 x 50 + 0 x 50 = 111

Type Cost Example

Type cost counts the weighted objects the response instantiates. The same paginated query instantiates one BooksConnection, fifty BooksEdge objects, fifty Book objects, and fifty Author objects, on top of the root Query object:

graphql
{
  # 1 Query
  books(first: 50) {
    # 1 BooksConnection
    edges {
      # 50 BooksEdges
      node {
        # 50 Books
        title
        author {
          # 50 Authors
          name
        }
      }
    }
  }
}
# Type cost: 1 + 1 + 50 + 50 + 50 = 152

The examples use this source schema:

graphql
directive @cost(
  weight: String!
) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR
directive @listSize(
  assumedSize: Int
  slicingArguments: [String!]
  sizedFields: [String!]
  requireOneSlicingArgument: Boolean = true
  slicingArgumentDefaultValue: Int
) on FIELD_DEFINITION

type Query {
  book: Book @cost(weight: "10")
  books(first: Int, last: Int): BooksConnection
    @cost(weight: "10")
    @listSize(
      assumedSize: 50
      slicingArguments: ["first", "last"]
      slicingArgumentDefaultValue: 10
      sizedFields: ["edges", "nodes"]
    )
}

type Book {
  title: String
  author: Author
}

type Author {
  name: String
}

type BooksConnection {
  edges: [BooksEdge]
  nodes: [Book]
}

type BooksEdge {
  node: Book
}

Pricing Rules

  • Complementary @include and @skip branches are mutually exclusive.
  • Fields are collected by response name before signed weights are applied, and clamping to zero happens after the field-call sum and the per-instance type sum are calculated.
  • An interface or union return weight is the signed maximum of its member object-type weights.
  • A field selected through an interface is priced through each possible object type's field metadata.
  • Costs on arguments of directives used in the operation contribute to field cost.
  • Negative slicing-argument values clamp to 0. A slicing value of 0 remains 0, and the field-call cost is still paid once.

List Size

The analyzer selects the assumed size for a list field in this order:

  1. An inherited size from a parent @listSize(sizedFields:) annotation. The inherited size takes priority over the child field's own @listSize.
  2. The maximum slicing-argument value present after coercion, including a schema argument default.
  3. slicingArgumentDefaultValue, only when no slicing argument is present.
  4. assumedSize.
  5. The default list size configured at composition time. Without a composed default, the list is unbounded.

An explicit null slicing argument is not a slicing value and suppresses that argument's schema default. An undefined slicing variable behaves as an absent argument, so a schema default can apply before the remaining fallbacks. See Default List Size for configuring the composed default.

Default Enforcement

AddGraphQLGatewayServer() enables cost enforcement in every hosting environment with these limits:

  • Maximum field cost: 1,000
  • Maximum type cost: 10,000

This environment-independent behavior differs from the default introspection rule, which permits introspection in Development.

Passing disableDefaultSecurity: true disables cost enforcement. Cost analysis and the GraphQL-Cost reporting modes remain available.

csharp
builder.Services.AddGraphQLGatewayServer(
    disableDefaultSecurity: true);

Set EnforceCostLimits to false when only cost enforcement should be disabled:

csharp
builder.Services
    .AddGraphQLGatewayServer()
    .ModifyCostOptions(options => options.EnforceCostLimits = false);

Set SkipAnalyzer to true to bypass analysis, enforcement, and reporting.

Reporting and Validation

Send the GraphQL-Cost HTTP request header to obtain cost metrics:

Header value Execution Reported value
report Yes Evaluated cost for the supplied variables.
validate No Evaluated cost for the supplied variables.

validate requires the variables the operation declares, exactly like execute and report. Without them, the request fails with the ordinary variable coercion error; the assumed bound is not exposed through the request pipeline. With variables, validate returns an extensions-only response with HTTP status 200, including when the reported values exceed configured limits. A variable batch returns one extensions-only result per variable set, with that set's operationCost. A successful variable batch in report mode also includes one operationCost per result.

json
{
  "extensions": {
    "operationCost": {
      "fieldCost": 11,
      "typeCost": 5
    }
  }
}

When MaxResponseSize is enabled, operationCost also contains maxResponseSize. Positive infinite values are serialized as the string "Infinity".

Rejections and HTTP Status

Cost-limit failures use error code HC0047. The error extensions identify the failed limit:

Limit Error extension keys
Field cost fieldCost, maxFieldCost
Type cost typeCost, maxTypeCost
Maximum response size maxResponseSize, maxAllowedResponseSize
json
{
  "errors": [
    {
      "message": "The maximum allowed type cost was exceeded.",
      "extensions": {
        "code": "HC0047",
        "typeCost": 2,
        "maxTypeCost": 1
      }
    }
  ]
}

The HTTP status depends on the accepted response media type:

Accept media type Rejection status
application/graphql-response+json 400
application/json 200

In report mode, a rejected response also contains extensions.operationCost. For a rejected variable batch, the single rejection result contains one operationCost: its fieldCost and typeCost are the sums across all variable sets, and it contains maxResponseSize only for a response-size rejection, using the first violating set's value.

One request is one invocation of the request pipeline, and a variable batch is one request. Cost enforcement sums the field cost and type cost across all variable sets and compares those sums with MaxFieldCost and MaxTypeCost. If either sum exceeds its limit, the whole request is rejected before any variable set executes, with one HC0047 result and the HTTP status shown above. Maximum response size remains enforced per variable set. MaxResponseSize is checked per variable set, not summed. If any set exceeds MaxResponseSize, the whole request is likewise rejected before any set executes, with one HC0047 result. When multiple sets violate this limit, the first violating set determines the reported maxResponseSize.

Request batching is an array of independent requests in one HTTP request. Cost limits currently apply separately to each independent request in a request batch. Summing costs across an entire request batch is planned, with no target version.

Pipeline Placement

The predefined Fusion pipelines run these stages in order:

  1. Document Cache
  2. Document Parser
  3. Document Validation
  4. Operation Variable Coercion
  5. Cost Analysis
  6. Operation Plan Cache
  7. Operation Plan
  8. Skip Warmup Execution
  9. Concurrency Gate
  10. Operation Execution

Cost enforcement runs before the operation-plan cache and before operation planning. A rejected request never enters the planner's single-flight coalescing and does not create an operation-plan cache entry or an in-flight planning entry. Document normalization, i.e. expanding fragments and removing statically excluded selections, is not a pipeline stage; it is resolved once per request by the first stage that needs it, which is variable coercion, cached by operation id and rewritten only on a cache miss, and cost analysis and operation planning then read it from the operation document info instead of re-normalizing it. Cost enforcement is per request and reflects the coerced variable values; limits on operation structure, such as maximum depth, node count, and parser limits, remain the protection against operations that are expensive to plan regardless of their variables.

A custom pipeline must place UseOperationVariableCoercion() before UseCostAnalysis(), and place UseCostAnalysis() before UseOperationPlanCache().

Options Reference

Configure FusionCostOptions with ModifyCostOptions:

Option Type Default Contract
MaxFieldCost double 1,000 Maximum allowed field cost. Valid range: non-negative finite values or Infinity.
MaxTypeCost double 10,000 Maximum allowed type cost. Valid range: non-negative finite values or Infinity.
EnforceCostLimits bool true Enforces the field, type, and response-size limits.
SkipAnalyzer bool false Skips analysis, enforcement, and reporting.
MaxResponseSize double? null Maximum estimated response-field count. null disables this check and metric. Valid range: null, non-negative finite values, or Infinity.
CostPlanCacheSize int 256 Maximum compiled cost plans cached per schema.
CaseBudget int? null Exact cases compiled per operation before CaseBudgetExceededBehavior decides the fallback. null uses the default (510).
CaseBudgetExceededBehavior CaseBudgetExceededBehavior? null Behavior once compiling one operation exhausts CaseBudget. null uses the default (EvaluatePerRequest).
csharp
builder.Services
    .AddGraphQLGatewayServer()
    .ModifyCostOptions(options =>
    {
        options.MaxFieldCost = 5_000;
        options.MaxTypeCost = 5_000;
        options.MaxResponseSize = 10_000;
        options.CostPlanCacheSize = 512;
    });

An operation whose exact compile would exceed CaseBudget is, by default (CaseBudgetExceededBehavior.EvaluatePerRequest), evaluated exactly per request instead of from a compiled plan: slower for that operation, but never over-estimated. CaseBudgetExceededBehavior.Overestimate restores the compiled, sound over-estimate for the unaffordable remainder instead.

Note: The default list size from List Size step 5 is not a gateway runtime option. It is a composition setting (SourceSchemaMergerOptions.DefaultListSize) carried into the execution schema by the @fusion__cost_options(defaultListSize:) directive, and the gateway reads it from there. See Composition for details.

Per-Request Cost Options

FusionCostOptions apply to every request by default, but a gateway can loosen or tighten those limits for a particular request or user by attaching a FusionRequestCostOptions to the request. A value set on the request replaces the corresponding gateway value for that request, in either direction: a request can raise a limit above the gateway default or lower it below the gateway default. Attached request options replace all of the gateway's limits for that request, and a null MaxResponseSize on the request means that request has no response-size limit.

The typical place to do this is an IHttpRequestInterceptor. Its OnCreateAsync method runs for every HTTP request and has access to the authenticated user, letting it pick request options based on group membership before the operation executes:

csharp
public class CostOptionsHttpRequestInterceptor : DefaultHttpRequestInterceptor
{
    public override ValueTask OnCreateAsync(HttpContext context,
        IRequestExecutor requestExecutor, OperationRequestBuilder requestBuilder,
        CancellationToken cancellationToken)
    {
        if (context.User.IsInRole("developer"))
        {
            requestBuilder.SetCostOptions(
                new FusionRequestCostOptions(
                    maxFieldCost: 5_000,
                    maxTypeCost: 5_000,
                    enforceCostLimits: true,
                    skipAnalyzer: false,
                    maxResponseSize: 50_000));
        }

        return base.OnCreateAsync(context, requestExecutor, requestBuilder,
            cancellationToken);
    }
}
csharp
builder.Services
    .AddGraphQLGatewayServer()
    .ModifyCostOptions(options => options.MaxResponseSize = 10_000)
    .AddHttpRequestInterceptor<CostOptionsHttpRequestInterceptor>();

Here, requests from the developer role get a higher MaxResponseSize (and higher field/type cost limits) than the gateway's configured limits, while every other request keeps enforcing the gateway's configured limits.

Response-size analysis itself is an opt-in that is only ever enabled per gateway, by setting FusionCostOptions.MaxResponseSize. A request cannot turn the analysis on: if the gateway leaves MaxResponseSize unset (null) and a request nonetheless sets FusionRequestCostOptions.MaxResponseSize, the request fails fast with error code HC0062 and the message:

The request cost options set MaxResponseSize, but the schema does not enable the response-size analysis.

To enable the check, set FusionCostOptions.MaxResponseSize on the gateway first; requests may then raise or lower it as needed.

Setting FusionRequestCostOptions.SkipAnalyzer for a request bypasses the cost analyzer entirely for that request. A MaxResponseSize set on the same request is then ignored.

Accessing the Analysis Result

RequestContext.TryGetCostAnalysisResult provides the compiled CostPlan and every estimate for the request. Read the result after the cost middleware has completed:

csharp
builder.Services
    .AddGraphQLGatewayServer()
    .UseRequest(
        next => async context =>
        {
            await next(context);

            if (context.TryGetCostAnalysisResult(out var result))
            {
                CostPlan plan = result.Plan;
                IReadOnlyList<CostEstimate> estimates = result.Estimates;
            }
        },
        key: "ReadCostAnalysisResult",
        before: WellKnownRequestMiddleware.CostAnalyzerMiddleware);

Cost analysis and reporting return HC0048 when required operation or document state is missing, when a request reaches the analyzer with zero coerced variable sets (an empty variable batch executed through IRequestExecutor; over HTTP, the transport refuses variables: [] before execution), or when metrics cannot be attached to the execution-result state. This applies to execute, report, and validate mode alike.

Next Steps


Website Content Docs Fusion Data Requirements And Mapping (`website/content/docs/fusion/data-requirements-and-mapping.md`)


title: "Data Requirements"

In traditional distributed systems, dependencies between services hide beneath the surface. A service assumes another service provides certain fields, responds in a certain shape, or is available at a certain time. These assumptions are invisible: they live in code, not in contracts. You discover them when something breaks in production. A field gets renamed, a service changes its response format, or a new team removes data another team depended on without knowing.

Fusion makes these dependencies declarative and validated at build time. When a resolver in one subgraph needs data from another subgraph, it declares that dependency explicitly in the schema using the @require directive. The composition step validates that every declared dependency is satisfiable before any code reaches production: the required fields must exist, be reachable, and have compatible types. If a dependency cannot be satisfied, composition fails and tells you exactly what is missing and where.

This shifts cross-service data dependencies from hidden runtime failures to visible, validated build-time contracts.

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

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.

Here is the simplest case: a resolver in the Shipping subgraph needs the product's weight to calculate a shipping estimate. The Products subgraph owns weight, not the Shipping subgraph. With @require, the Shipping subgraph declares this dependency directly in its schema:

GraphQL schema

graphql
# Shipping subgraph
type Product {
  id: ID!
  shippingEstimate(zip: String!, weight: Float! @require(field: "weight")): Int!
}

type Query {
  productById(id: ID!): Product @lookup @internal
}

The #!sdl @require(field: "weight") directive on the weight argument tells the gateway: "Before calling this resolver, fetch weight from whichever subgraph can provide it and pass it in as the weight argument."

C# resolver

csharp
[ObjectType<Product>]
public static partial class ProductNode
{
    public static int GetShippingEstimate(
        [Parent] Product product,
        string zip,
        [Require] float weight)
        => ShippingCalculator.Estimate(zip, weight);
}

The [Require] attribute maps to #!sdl @require(field: "weight") in the exported schema. Because the C# argument name weight matches the entity field name, the field path is inferred automatically.

When the names differ, provide the field path explicitly:

csharp
public static int GetShippingEstimate(
    [Parent] Product product,
    string zip,
    [Require("weight")] float packageWeight)
    => ShippingCalculator.Estimate(zip, packageWeight);

Public facing composite schema (what clients see)

graphql
type Product {
  shippingEstimate(zip: String!): Int!
}

The weight argument is gone. Clients pass only zip. The gateway handles the resolution of weight transparently.

How the Gateway Resolves Requirements

When a resolver declares a data requirement with @require, three things happen during composition and execution:

  1. Composition reads the @require directive and removes the annotated argument from the composite schema. Clients never see it.
  2. Query planning detects the dependency. The gateway plans an additional fetch to retrieve the required fields from whichever subgraph can provide it.
  3. Execution fetches the required data first, then passes it as a resolver argument when invoking the downstream subgraph.

The resolver receives the data as if it were a normal argument. It does not know or care where the data came from. Services are not coupled to each other. The contract is between a resolver and the data it needs, not between one service and another.

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.

GraphQL schema

graphql
# Shipping subgraph
type Product {
  id: ID!
  deliveryEstimate(
    zip: String!
    dimension: ProductDimensionInput!
      @require(
        field: """
        {
          weight,
          length: dimensions.length,
          width: dimensions.width,
          height: dimensions.height
        }
        """
      )
  ): Int!
}

input ProductDimensionInput {
  weight: Float!
  length: Float!
  width: Float!
  height: Float!
}

The FieldSelectionMap inside @require tells the gateway how to populate the ProductDimensionInput from fields on the Product entity:

  • weight maps directly because the input field name matches the entity field name.
  • length: dimensions.length maps the input field length from the nested entity path dimensions.length. The same applies to width and height.

The fields referenced in the map do not all have to come from the same subgraph. The gateway resolves each field from whichever subgraph owns it. Your resolver declares what data it needs, not which services to call.

C# resolver

csharp
[ObjectType<Product>]
public static partial class ProductNode
{
    public static int GetDeliveryEstimate(
        [Parent] Product product,
        string zip,
        [Require(
            """
            {
              weight,
              length: dimensions.length,
              width: dimensions.width,
              height: dimensions.height
            }
            """)]
        ProductDimensionInput dimension)
        => ShippingCalculator.Estimate(zip, dimension);
}
csharp
public sealed class ProductDimensionInput
{
    public float Weight { get; init; }
    public float Length { get; init; }
    public float Width { get; init; }
    public float Height { get; init; }
}

Public facing composite schema (what clients see)

graphql
type Product {
  deliveryEstimate(zip: String!): Int!
}

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

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.

GraphQL schema

graphql
# Inventory subgraph
type Product {
  id: ID!
  shippingEstimate(
    zip: String!
    weight: Float! @require(field: "weight")
    price: Float! @require(field: "price")
  ): Int!
}

C# resolver

csharp
[ObjectType<Product>]
public static partial class ProductNode
{
    public static int GetShippingEstimate(
        [Parent] Product product,
        string zip,
        [Require] float weight,
        [Require] float price)
        => weight > 500 || price > 1000
            ? ExpressShipping.Calculate(weight)
            : StandardShipping.Calculate(weight);
}

Nested Field Paths

@require can reach into nested objects using dot notation.

GraphQL schema

graphql
type Product {
  id: ID!
  taxEstimate(
    countryCode: String! @require(field: "seller.address.countryCode")
    price: Float! @require(field: "price")
  ): Float!
}

The gateway traverses seller.address.countryCode on the entity and passes the resolved value as the countryCode argument.

Constant Arguments in Field Selections

Fields referenced in a @require or @is field selection map may carry constant arguments to parameterize or disambiguate the selected field. This is useful when a field offers multiple representations via an argument, and the resolver always needs one specific form.

GraphQL schema

graphql
# Shipping subgraph
type Product {
  shippingCost(size: Int! @require(field: "dimension(unit: METRIC)")): Float
  dimension(unit: Unit!): Int
}

Here the @require path selects dimension with the constant argument unit: METRIC. The gateway evaluates this selection against the Products subgraph and passes the resulting value to shippingCost.

Argument values in a field selection map must be constant literals (no variables). They must match the field's declared argument definitions, and all required arguments must be supplied. Violations surface as IS_INVALID_FIELDS for @is and REQUIRE_INVALID_FIELDS for @require. Note that @provides field selections must not include arguments at all; any argument usage there is reported as PROVIDES_FIELDS_HAS_ARGUMENTS.

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.

GraphQL schema

graphql
type Product {
  id: ID!
  taxEstimate(
    countryCodes: [String!]! @require(field: "seller.addresses[countryCode]")
    price: Float! @require(field: "price")
  ): Float!
}

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 #!json ["US", "DE", "US"] as the countryCodes argument.

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

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 #!sdl @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."

This is different from @shareable, which declares that a subgraph can always resolve a field. @provides is conditional: the data is only available when coming through a specific field path.

GraphQL schema

graphql
# Reviews subgraph
type Review {
  id: ID!
  body: String!
  author: User @provides(fields: "username")
}

type User {
  id: ID!
  username: String! @external
}

type Query {
  reviewById(id: ID!): Review @lookup
}

The #!sdl @provides(fields: "username") on author tells the gateway that when it resolves author from the Reviews subgraph, it can also get username without a separate call to the Accounts subgraph.

The @external on username declares that this field is owned by another subgraph (Accounts), but the Reviews subgraph can provide it in the context of Review.author.

C# resolver

csharp
[ObjectType<Review>]
public static partial class ReviewNode
{
    [Provides("username")]
    public static User GetAuthor(
        [Parent(requires: nameof(Review.AuthorId))] Review review)
        => new User(review.AuthorId, review.AuthorUsername);
}

Providing Multiple and Nested Fields

@provides accepts a field selection set, so you can declare multiple fields or nested fields:

graphql
type Review {
  product: Product @provides(fields: "name price")
}
graphql
type Review {
  product: Product @provides(fields: "sku variation { size color }")
}

When to Provide Fields Contextually

Use @provides when:

  • Your subgraph stores denormalized data from another subgraph (e.g., a cached username)
  • Avoiding the extra round-trip measurably improves performance
  • The locally stored data is kept in sync with the owning subgraph

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

@require uses the FieldSelectionMap scalar for its field argument. This is a mini-language for describing how to map entity fields to argument shapes.

Syntax Example Meaning
Simple path "weight" Maps the weight field directly
Nested path "dimensions.weight" Traverses into dimensions, then selects weight
Object selection "{ weight, height }" Selects multiple fields into an object shape
Mapped selection "{ w: dimensions.weight }" Renames: maps entity's dimensions.weight to argument field w
Mixed selection "{ weight, len: dimensions.length }" Combines direct and renamed mappings
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

Simple path. Use when @require maps one argument to one field.

graphql
weight: Float! @require(field: "weight")

Object selection. Use when mapping multiple entity fields into a single input object argument.

graphql
dimension: ProductDimensionInput! @require(field: "{ weight, length, width, height }")

Mapped selection. Use when the input field names differ from the entity field names, or when you need to reach into nested fields.

graphql
dimension: ProductDimensionInput! @require(field: "{ w: weight, l: dimensions.length }")

List aggregation. Use when you need to collect a single field from each element of a list.

graphql
countryCodes: [String!]! @require(field: "seller.addresses[countryCode]")

List projection. Use when you need multiple fields from each element but want to drop the rest.

graphql
dims: [ProductDimensionInput!]! @require(field: "dimensions[{ weight, height }]")

For the full FieldSelectionMap grammar, see the Composite Schemas specification.

Troubleshooting

`REQUIRE_INVALID_FIELDS`: Referenced field does not exist

text
Error: The @require directive on argument "weight" references field "weight"
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`

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`

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

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

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


Website Content Docs Fusion Deployment And Ci Cd (`website/content/docs/fusion/deployment-and-ci-cd.md`)


title: "Deployment and CI/CD"

This page covers the CI/CD pipelines that ship a Fusion gateway in production: uploading source schemas to Nitro from each subgraph repository, publishing a Fusion configuration to a stage after a deployment, and validating composition on pull requests before merge. Composition runs in CI, the gateway pulls the latest archive from Nitro at runtime, and no archive file is ever shipped by hand.

The gateway closes the loop with AddNitro().AddDefaults(), which subscribes to the latest archive on the configured stage and hot-swaps it without a restart.

Prerequisites

Before wiring up your pipelines you need:

  • A Nitro account. If you don't have one yet, see the Nitro documentation to sign up and create an organization.
  • An API ID for the Fusion gateway. Each gateway has its own API in Nitro. The ID looks like QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==. See the Nitro documentation for how to create an API and copy the ID.
  • An API key with permission to upload source schemas, publish configurations, and validate against the target stage. Store it as a CI secret (NITRO_API_KEY).
  • A stage to deploy to (for example dev, staging, production). Stages are created in Nitro and addressed by name.

[!NOTE]
All nitro fusion subcommands fall back to the environment variables NITRO_API_ID, NITRO_API_KEY, NITRO_STAGE, and NITRO_TAG when the corresponding command-line option is omitted. Setting these at the job level keeps the individual commands compact. See the CLI reference for the full list.

Deployment workflow

The deployment pipeline is split across two jobs. The build job compiles each subgraph, exports its source schema, and uploads that schema to Nitro tagged with the current commit. The deploy job ships the subgraph application itself and then asks Nitro to publish a new Fusion configuration that references the uploaded source schema by tag. Nitro composes the configuration server-side from the registered source schemas (see Composition for what composition does) and makes the resulting archive available to all gateways subscribed to that stage.

The runtime side is configured once on the gateway:

csharp
builder.Services.AddNitro().AddDefaults();

builder.Services
    .AddGraphQLGatewayServer()
    .ModifyNitroOptions(o =>
    {
        o.Service.ApiId = "...";
        o.Service.ApiKey = "...";
        o.Service.Stage = "production";
    });

[!NOTE]
AddDefaults() is generated by a source generator in the ChilliCream.Nitro package. It requires the ChilliCream.Nitro.Fusion package to be referenced in your project. If AddDefaults() is not available, verify your package references or use the explicit .AddFusion() method instead. See the Analyzers page for details.

With the runtime in place, every successful nitro fusion publish becomes the new configuration the gateway serves traffic against. You don't ship the archive yourself.

Upload the source schema

The build job exports the subgraph's GraphQL SDL and uploads it to Nitro. The export step uses the standard Hot Chocolate schema export command:

shell
dotnet run --project ./src/SubgraphA -- schema export --output schema.graphql

After the export, upload the file. The tag you pass here is the handle the publish step uses later to find this exact upload, so use a value that's unique per build (the commit SHA works well):

yaml
- uses: ChilliCream/nitro-fusion-upload@v16
  with:
    tag: ${{ github.sha }}
    api-id: ${{ secrets.NITRO_API_ID }}
    api-key: ${{ secrets.NITRO_API_KEY }}
    source-schema-files: |
      ./src/SubgraphA/schema.graphql
yaml
- task: NitroFusionUpload@16
  inputs:
    # Authentication depends on the method you configured.
    # This example uses a Nitro service connection.
    nitroServiceConnection: nitro-prod
    apiId: $(NITRO_API_ID)
    tag: $(Build.SourceVersion)
    sourceSchemaFiles: |
      ./src/SubgraphA/schema.graphql
shell
nitro fusion upload \
  --tag "${{ github.sha }}" \
  --api-id "${{ secrets.NITRO_API_ID }}" \
  --api-key "${{ secrets.NITRO_API_KEY }}" \
  --source-schema-file "./src/SubgraphA/schema.graphql"

[!NOTE]
The tag you pass to upload must match the tag you reference from publish. If they drift, the publish step won't find the source schema and the deployment fails before any traffic is rerouted.

A complete build-and-deploy workflow looks like this:

yaml
name: Deploy SubgraphA

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: "10.0.x"

      - name: Export source schema
        run: dotnet run --project ./src/SubgraphA -- schema export --output ./src/SubgraphA/schema.graphql

      - name: Upload source schema to Nitro
        uses: ChilliCream/nitro-fusion-upload@v16
        with:
          tag: ${{ github.sha }}
          api-id: ${{ secrets.NITRO_API_ID }}
          api-key: ${{ secrets.NITRO_API_KEY }}
          source-schema-files: |
            ./src/SubgraphA/schema.graphql

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy subgraph
        run: |
          # deploy your subgraph application here, e.g. push container image, kubectl apply, etc.

      - name: Publish Fusion configuration
        uses: ChilliCream/nitro-fusion-publish@v16
        with:
          tag: ${{ github.sha }}
          stage: production
          api-id: ${{ secrets.NITRO_API_ID }}
          api-key: ${{ secrets.NITRO_API_KEY }}
          source-schemas: |
            subgraph-a@${{ github.sha }}

Publish the Fusion configuration

The deploy job publishes a new Fusion configuration that references the source schema uploaded in the build job. Nitro re-composes from the registered source schemas, runs validation, and makes the new archive available on the target stage.

yaml
- uses: ChilliCream/nitro-fusion-publish@v16
  with:
    tag: ${{ github.sha }}
    stage: production
    api-id: ${{ secrets.NITRO_API_ID }}
    api-key: ${{ secrets.NITRO_API_KEY }}
    source-schemas: |
      subgraph-a@${{ github.sha }}
yaml
- task: NitroFusionPublish@16
  inputs:
    # Authentication depends on the method you configured.
    # This example uses a Nitro service connection.
    nitroServiceConnection: nitro-prod
    apiId: $(NITRO_API_ID)
    tag: $(Build.SourceVersion)
    stage: production
    sourceSchemas: |
      subgraph-a@$(Build.SourceVersion)
shell
nitro fusion publish \
  --tag "${{ github.sha }}" \
  --stage "production" \
  --api-id "${{ secrets.NITRO_API_ID }}" \
  --api-key "${{ secrets.NITRO_API_KEY }}" \
  --source-schema "subgraph-a@${{ github.sha }}"

The --source-schema value uses the name@version syntax. The name is the source schema name from the subgraph's schema-settings.json (the name field), and the version is the tag you passed to nitro fusion upload in the build job. To publish a configuration that combines this subgraph with sibling subgraphs at fixed versions, repeat the option:

shell
nitro fusion publish \
  --tag "${{ github.sha }}" \
  --stage "production" \
  --source-schema "subgraph-a@${{ github.sha }}" \
  --source-schema "[email protected]" \
  --source-schema "subgraph-c@latest"

[!NOTE]
nitro fusion publish must run after the subgraph application has been deployed and is reachable at its production URL. Once publish succeeds, gateways subscribed to that stage start routing traffic against the new schema, and any subgraph endpoint referenced by it must already accept requests. Publishing first and deploying second produces a window where the gateway sends traffic to a URL that isn't live yet.

Pull request validation

Before merging a change to a subgraph schema, the PR validation workflow asks Nitro to compose the proposed source schema together with the currently published versions of the other subgraphs and reports any composition errors or breaking changes against the target stage. This catches incompatibilities before they reach main.

yaml
- uses: ChilliCream/nitro-fusion-validate@v16
  with:
    stage: production
    api-id: ${{ secrets.NITRO_API_ID }}
    api-key: ${{ secrets.NITRO_API_KEY }}
    source-schema-files: |
      ./src/SubgraphA/schema.graphql
yaml
- task: NitroFusionValidate@16
  inputs:
    # Authentication depends on the method you configured.
    # This example uses a Nitro service connection.
    nitroServiceConnection: nitro-prod
    apiId: $(NITRO_API_ID)
    stage: production
    sourceSchemaFiles: |
      ./src/SubgraphA/schema.graphql
shell
nitro fusion validate \
  --stage "production" \
  --api-id "${{ secrets.NITRO_API_ID }}" \
  --api-key "${{ secrets.NITRO_API_KEY }}" \
  --source-schema-file "./src/SubgraphA/schema.graphql"

A complete PR validation workflow:

yaml
name: Validate SubgraphA

on:
  pull_request:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: "10.0.x"

      - name: Export source schema
        run: dotnet run --project ./src/SubgraphA -- schema export --output ./src/SubgraphA/schema.graphql

      - name: Validate against production stage
        uses: ChilliCream/nitro-fusion-validate@v16
        with:
          stage: production
          api-id: ${{ secrets.NITRO_API_ID }}
          api-key: ${{ secrets.NITRO_API_KEY }}
          source-schema-files: |
            ./src/SubgraphA/schema.graphql

[!NOTE]
Validate against the same stage you'll publish to. Validating against dev while you're about to publish to production defeats the purpose, since the source schemas registered on the two stages can differ.

Without Nitro

Nitro is the supported path. It handles the schema registry, server-side composition, breaking-change detection, persisted operation tracking, and atomic rollout to subscribed gateways. If you can use Nitro, use it.

If you can't, you compose locally, ship the resulting archive to durable storage yourself, and load it on the gateway. The local compose step produces a gateway.far:

shell
nitro fusion compose \
  --source-schema-file ./src/SubgraphA/schema.graphqls \
  --source-schema-file ./src/SubgraphB/schema.graphqls \
  --archive ./gateway.far

The gateway loads the archive from disk:

csharp
builder.Services
    .AddGraphQLGatewayServer()
    .AddFileSystemConfiguration("gateway.far");

If the archive lives in object storage, a container registry, or a custom source, implement IFusionConfigurationProvider and register it through AddConfigurationProvider:

csharp
builder.Services
    .AddGraphQLGatewayServer()
    .AddConfigurationProvider(sp => new CustomConfigurationProvider());

See the configuration provider API section in the migration guide for the full surface (IFusionConfigurationProvider, AddFileSystemConfiguration, AddInMemoryConfiguration, AddConfigurationProvider).

Going down this path means you own the production concerns Nitro otherwise handles for you:

  • Exclusive write access. Pushes from multiple subgraph pipelines must serialize so that two pipelines don't compose against stale state and overwrite each other's archive.
  • Pre-publish validation. Before publishing a new archive, you have to download the previously deployed one, re-run composition with the proposed change, and check for breaking changes. Otherwise a bad subgraph change reaches the gateway unfiltered.
  • Persisted operation safety. The previously deployed archive must keep working for in-flight clients. Removing a field that's still referenced by persisted operations breaks live traffic.
  • Atomic rollout. The gateway must pick up the new archive without a request seeing a half-written file. Write to a temporary path and rename, or use a storage layer with atomic swaps.

For any meaningful production setup (multiple subgraph pipelines, schema registry, breaking-change detection, persisted operation tracking, atomic rollout), Nitro handles all of this and is the recommended option.

Next steps


Website Content Docs Fusion Directives Reference (`website/content/docs/fusion/directives-reference.md`)


title: "Directive Reference"

Fusion implements the GraphQL Composite Schemas Specification. The directives defined in this specification are applied to source schemas (subgraph schemas) to control how they compose into a unified composite schema. Each directive entry below shows its SDL definition, what it does, and a practical example with the resulting composed output.

In Hot Chocolate, many of these directives are expressed using C# attributes. See the individual guide pages for supported C# usage and tutorials.

Quick Reference

Directive Locations Repeatable Purpose
@key OBJECT, INTERFACE Yes Define entity identity
@lookup FIELD_DEFINITION No Mark entity lookup resolvers
@is ARGUMENT_DEFINITION No Map lookup arguments to entity fields
@interfaceObject OBJECT No Contribute fields to an interface
@implement FIELD_DEFINITION No Replace a projected interface-object default
@require ARGUMENT_DEFINITION No Declare cross-subgraph data dependencies
@shareable OBJECT, FIELD_DEFINITION Yes Allow multiple subgraphs to define the same field
@provides FIELD_DEFINITION No Declare locally-resolvable subfields
@external FIELD_DEFINITION No Mark field as owned by another subgraph
@override FIELD_DEFINITION No Migrate field ownership between subgraphs
@internal OBJECT, FIELD_DEFINITION No Hide from composite schema and merge process
@inaccessible 10 locations No Hide from client-facing composite schema
@eventStream FIELD_DEFINITION No Back a subscription field with a message broker
@eventCursor ARGUMENT_DEFINITION, FIELD_DEFINITION No Mark the resume cursor for resumable streams

Entity Identity and Resolution

`@key`

Designates an entity's unique key, which identifies how to uniquely reference an instance of an entity across different source schemas.

graphql
directive @key(fields: FieldSelectionSet!) repeatable on OBJECT | INTERFACE
Argument Type Description
fields FieldSelectionSet! A field selection set that forms the unique key (e.g. "id" or "tenantId id")

Each @key directive on a type specifies one distinct unique key for that entity. Apply multiple @key directives to define alternative keys that the gateway can use to resolve the entity. Fields referenced in a key are implicitly shareable across subgraphs -- you do not need to add @shareable to key fields.

Key fields may supply constant arguments to select a specific variant of a field (for example, #!sdl @key(fields: "id(scope: LOCAL)")). Argument values must be constant literals (no variables), must match the field's declared argument definitions, and all required arguments must be supplied. Composition reports unknown, incompatible, or missing-required arguments as KEY_INVALID_ARGUMENTS.

Example -- single key:

graphql
# Source schema
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
}
graphql
# Composed schema
type Product {
  id: ID!
  name: String!
  price: Float!
}

Example -- multiple keys:

graphql
# Source schema
type Product @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: String!
  name: String!
}

Example -- composite key:

graphql
# Source schema (both fields required together)
type Product @key(fields: "id sku") {
  id: ID!
  sku: String!
  name: String!
}

In C#: #!csharp [EntityKey("id")] attribute. See Entities and Lookups.


`@lookup`

Marks a field as an entity lookup resolver that the gateway uses to resolve an entity by a stable key.

graphql
directive @lookup on FIELD_DEFINITION

Lookup fields provide the gateway with entry points into a subgraph for entity resolution. A source schema can define multiple lookup fields for the same entity to support resolution by different keys. Lookup fields must return a nullable type and must not return a list.

Example:

graphql
# Source schema
type Query {
  productById(id: ID!): Product @lookup
  productByName(name: String!): Product @lookup
}

type Product @key(fields: "id") @key(fields: "name") {
  id: ID!
  name: String!
}
graphql
# Composed schema
type Query {
  productById(id: ID!): Product
  productByName(name: String!): Product
}

type Product {
  id: ID!
  name: String!
}

In C#: [Lookup] attribute. See Entities and Lookups.


`@is`

Maps a lookup argument to a field on the entity type when the argument name does not match the field name.

graphql
directive @is(field: FieldSelectionMap!) on ARGUMENT_DEFINITION
Argument Type Description
field FieldSelectionMap! A selection map that describes the mapping from entity fields to the argument

When a lookup argument name matches the corresponding field on the return type, you can omit @is. Use @is when the names differ or when the mapping involves nested fields. Fields in the selection map may carry constant arguments (for example, #!sdl @is(field: "id(scope: LOCAL)")); argument values must be constant literals (no variables), must match the field's argument definitions, and all required arguments must be supplied. Argument errors surface as IS_INVALID_FIELDS.

Example -- argument name differs from field name:

graphql
# Source schema
type Query {
  personById(personId: ID! @is(field: "id")): Person @lookup
}

type Person @key(fields: "id") {
  id: ID!
  name: String!
}
graphql
# Composed schema
type Query {
  personById(personId: ID!): Person
}

type Person {
  id: ID!
  name: String!
}

Example -- nested field reference:

graphql
# Source schema
type Query {
  personByAddressId(id: ID! @is(field: "address.id")): Person @lookup
}

In C#: The @is mapping is inferred automatically from the argument name. When it does not match, use the field parameter convention described in Entities and Lookups.


Interface Objects

`@interfaceObject`

Declares an object type as a stand-in for an interface with the same name in another source schema. The stand-in can contribute fields to the interface without declaring its concrete implementations.

graphql
directive @interfaceObject on OBJECT

The stand-in must declare at least one @key, and each key must match a key on the interface entity. A source that contributes non-key fields through the stand-in must also provide a lookup returning the stand-in.

Example:

graphql
# Catalog source schema
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

type Book implements Media @key(fields: "id") {
  id: ID!
  title: String!
}
graphql
# Analytics source schema
type Query {
  mediaByKey(id: ID!): Media @lookup @internal
}

type Media @interfaceObject @key(fields: "id") {
  id: ID!
  views: Int!
}
graphql
# Composed schema
interface Media {
  id: ID!
  title: String!
  views: Int!
}

type Book implements Media {
  id: ID!
  title: String!
  views: Int!
}

See Interface Objects for lookup requirements, projected fields, and concrete type recovery.


`@implement`

Marks a field as an explicit implementation that replaces a default projected from an interface object.

graphql
directive @implement on FIELD_DEFINITION

Use @implement only when a concrete object or a more-specific interface stand-in declares a field that would otherwise adopt a projected default. Composition rejects an unmarked replacement and rejects @implement when there is no applicable default.

Example:

graphql
# Catalog source schema
type Book implements Media @key(fields: "id") {
  id: ID!
  title: String!
  views: Int! @implement
}
graphql
# Analytics source schema
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  views: Int!
}

In the composed schema, Catalog owns Book.views. Other Media implementations continue to use the Analytics default.

See Replace a Projected Default for the complete pattern.


Data Requirements

`@require`

Declares that a resolver argument needs data from fields owned by other subgraphs. The gateway resolves the required data first, then passes it to the resolver. Arguments annotated with @require are removed from the composed client-facing schema.

graphql
directive @require(field: FieldSelectionMap!) on ARGUMENT_DEFINITION
Argument Type Description
field FieldSelectionMap! A selection map describing which fields from the entity type are needed

Use @require when a resolver in one subgraph needs data that another subgraph owns. The gateway handles the data fetching automatically. This shifts cross-service data dependencies from hidden runtime failures to validated build-time contracts. Fields in the selection map may carry constant arguments to select a specific variant (for example, #!sdl @require(field: "dimension(unit: METRIC)")); argument values must be constant literals (no variables), must match the field's argument definitions, and all required arguments must be supplied. Argument errors surface as REQUIRE_INVALID_FIELDS.

Example -- scalar requirement:

graphql
# Source schema (Shipping subgraph)
type Product {
  shippingEstimate(zip: String!, weight: Float! @require(field: "weight")): Int!
}
graphql
# Composed schema (weight argument removed)
type Product {
  shippingEstimate(zip: String!): Int!
}

Example -- structured requirement with input type:

graphql
# Source schema
type Product {
  delivery(
    zip: String!
    dimension: ProductDimensionInput!
      @require(field: "{ size: dimension.size, weight: dimension.weight }")
  ): DeliveryEstimates
}

In C#: [Require] attribute on method parameters. See Data Requirements.


Field Ownership and Sharing

`@shareable`

Allows multiple subgraphs to define the same field. Without @shareable, defining the same non-key field in two subgraphs causes a composition error.

graphql
directive @shareable repeatable on OBJECT | FIELD_DEFINITION

When multiple subgraphs mark the same field as @shareable, they declare that the field is semantically equivalent across all definitions. The gateway is free to resolve the field from any subgraph that defines it. Apply @shareable to an object type to make all its fields shareable.

Example:

graphql
# Source schema A (Products subgraph)
type Product @key(fields: "id") {
  id: ID!
  name: String! @shareable
  description: String!
}
graphql
# Source schema B (Inventory subgraph)
type Product @key(fields: "id") {
  id: ID!
  name: String! @shareable
  inStock: Boolean!
}
graphql
# Composed schema
type Product {
  id: ID!
  name: String!
  description: String!
  inStock: Boolean!
}

In C#: [Shareable] attribute. See Field Ownership.


`@provides`

Declares that a field returning an entity can resolve specific subfields of that entity locally, without requiring an additional call to another subgraph.

graphql
directive @provides(fields: FieldSelectionSet!) on FIELD_DEFINITION
Argument Type Description
fields FieldSelectionSet! A field selection set describing the subfields of the returned type that this subgraph can resolve

This is a query-planning optimization. When a client requests provided subfields through this particular field path, the gateway resolves them from the current subgraph instead of making a separate call. Fields referenced in @provides must be marked @external on the return type. Fields in a @provides selection must not have arguments; composition reports any argument usage as PROVIDES_FIELDS_HAS_ARGUMENTS.

Example:

graphql
# Source schema (Reviews subgraph)
type Review {
  id: ID!
  body: String!
  author: User @provides(fields: "email")
}

type User @key(fields: "id") {
  id: ID!
  email: String! @external
}
graphql
# Composed schema
type Review {
  id: ID!
  body: String!
  author: User
}

type User {
  id: ID!
  email: String!
}

In C#: #!csharp [Provides("email")] attribute. See Field Ownership.


`@external`

Marks a field as owned by another subgraph. The current subgraph references the field for entity identification (via @key) or to provide it locally through @provides.

graphql
directive @external on FIELD_DEFINITION

Every @external field must be referenced by at least one @provides directive or used in a @key. An unused @external field causes a composition error. External fields cannot be combined with @override or @provides on the same field.

Example -- external field used with @provides:

graphql
# Source schema (Reviews subgraph)
type Review {
  id: ID!
  author: User @provides(fields: "email")
}

type User @key(fields: "id") {
  id: ID!
  email: String! @external
}

Example -- external field used as entity key:

graphql
# Source schema (Payments subgraph)
type Product @key(fields: "sku") {
  sku: String! @external
  price: Float!
}

type Query {
  productBySku(sku: String!): Product @lookup
}

In C#: [External] attribute (from HotChocolate.ApolloFederation.Types). See Field Ownership.


`@override`

Migrates field ownership from one subgraph to another. The current subgraph takes responsibility for resolving the field, and the original subgraph stops serving it. The original subgraph does not need to be modified.

graphql
directive @override(from: String!) on FIELD_DEFINITION
Argument Type Description
from String! The name of the source schema that originally provided this field

Use @override to move a field to a new subgraph during schema evolution. The overriding subgraph typically marks the entity's key fields as @external. Cyclic overrides cause a composition error.

Example:

graphql
# Source schema: original Catalog subgraph (unchanged)
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
}
graphql
# Source schema: new Payments subgraph (takes over price)
type Product @key(fields: "id") {
  id: ID! @external
  price: Float! @override(from: "Catalog")
  tax: Float!
}
graphql
# Composed schema
type Product {
  id: ID!
  name: String!
  price: Float!
  tax: Float!
}

In C#: #!csharp [Override("Catalog")] attribute (from HotChocolate.ApolloFederation.Types). See Schema Exposure and Evolution.


Visibility

`@internal`

Hides a type or field from the composite schema and excludes it from the standard schema-merging process. The gateway can still use internal fields as lookup entry points for entity resolution.

graphql
directive @internal on OBJECT | FIELD_DEFINITION

Internal types and fields do not collide with similarly named fields or types on other source schemas, because they bypass merge rules entirely. Use @internal to create resolution-only entry points that clients cannot query directly.

Example:

graphql
# Source schema A
type Query {
  productById(id: ID!): Product @lookup
  productBySku(sku: ID!): Product @lookup @internal
}

type Product @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: ID!
  name: String!
}
graphql
# Composed schema (internal lookup removed)
type Query {
  productById(id: ID!): Product
}

type Product {
  id: ID!
  sku: ID!
  name: String!
}

In C#: [Internal] attribute. See Schema Exposure and Evolution.


`@inaccessible`

Prevents a type system member from appearing in the client-facing composite schema, even if it is accessible in the underlying source schemas.

graphql
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION

Unlike @internal, inaccessible elements still participate in composition merging and can be referenced by @require dependencies in other subgraphs. If any source schema marks a type system member as @inaccessible, it is hidden from the composite schema -- even if other schemas expose the same member without @inaccessible.

Example:

graphql
# Source schema A
type Product @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: String! @inaccessible
  name: String!
}
graphql
# Source schema B
type Product @key(fields: "sku") {
  sku: String!
  price: Float!
}
graphql
# Composed schema (sku hidden by schema A's @inaccessible)
type Product {
  id: ID!
  name: String!
  price: Float!
}

In C#: [Inaccessible] attribute. See Schema Exposure and Evolution.


`@internal` vs `@inaccessible`

These two directives both hide elements from the composite schema, but they behave differently during composition:

| Aspect | @internal | `@inaccessi