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.
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.
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.
Green Donut
Green Donut is a lightweight, yet powerful DataLoader that simplifies batching, caching, and solves the N+1 problem.
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.
Sponsor
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
tenantIdon 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 thatuser.tenantreturns aTenantidentified 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
Tenantis an entity with anidfield. - Adding new subgraphs is safe. If a new Billing subgraph wants to contribute a
billingPlanfield to theTenanttype, 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.
type Query {
productById(id: ID!): Product @lookup
}- An internal lookup (marked with the
@internaldirective) is excluded from the composite schema, cannot be called by clients, and is used only by the gateway for cross-subgraph entity resolution.
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.
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:
dotnet --versionYou should see 10.0.100 or higher.
Nitro CLI
The Nitro CLI is a .NET tool that handles schema composition. Install it globally:
dotnet tool install -g ChilliCream.Nitro.CommandLineVerify the installation:
nitro versionHot Chocolate Templates
Install the Hot Chocolate templates:
dotnet new install HotChocolate.TemplatesCreate 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:
mkdir fusion-getting-started
cd fusion-getting-started
dotnet new graphql -n ProductsThe 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:
<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:
"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:
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:
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:
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 theQueryroot type. The source generator (fromHotChocolate.Types.Analyzers) automatically registers these fields.[Lookup]marksGetProductByIdas a lookup resolver. This is how the gateway resolves aProductwhen 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 returnnulland avoid cascading failures when one or more subgraphs cannot provide requested fields for an entity.
Configure the Server
Set Program.cs to:
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:
cd Products
dotnet runOpen your browser to http://localhost:5001/graphql/ to access the Nitro GraphQL IDE. Try this query:
query {
products {
id
name
price
}
}You should see:
{
"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:
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:
dotnet run --project ./Products -- schema exportThis 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:
{
"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
productsquery that returns all products - Provides a
productByIdlookup 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:
dotnet new graphql -n ReviewsThe 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:
"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:
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:
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:
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, andpricefrom the Products subgraphreviewsfrom the Reviews subgraph
All fields appear on a single unified Product type in the composite schema.
Add Query Resolvers
Create ReviewQueries.cs:
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:
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 theProductentity. The gateway calls this when it needs to resolve a Product reference.[Internal]hides this field from the composite schema. Clients cannot callproductByIdon 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:
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:
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 theReviewtype.[BindMember(nameof(Review.ProductId))]replaces the rawProductIdinteger onReviewwith a resolvedProductobject. In the exported schema, clients seereview.product(returning a fullProduct) instead ofreview.productId(returning a raw integer).[Parent]tells Hot Chocolate to inject the parent object (theReview) into the resolver. This is howGetProduct()accesses theProductIdfrom 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:
cd Reviews
dotnet runOpen http://localhost:5002/graphql/ and try:
query {
reviews {
id
body
stars
}
}You should see:
{
"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]:
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:
dotnet run --project ./Reviews -- schema exportThis 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:
{
"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
reviewsquery and areviewByIdlookup - Adds a
reviewsfield to theProducttype using an entity stub, without duplicating any Product data - Replaces the raw
productIdon reviews with a resolvedProductreference via[BindMember] - Provides an internal
productByIdlookup 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:
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 settingsEach 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:
mkdir Gateway
nitro fusion compose -f Products/schema.graphqls -f Reviews/schema.graphqls -a Gateway/gateway.farIf composition succeeds, you will see output similar to:
Validating source schemas...
Merging schemas...
Fusion archive created: gateway.farThe 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:
- Validate source schemas: check GraphQL validity and Fusion metadata.
- Merge source schemas: combine matching types and enforce field ownership rules.
- Check satisfiability: verify that cross-subgraph selections can actually be fulfilled with the available keys and lookups.
- 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:
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:
<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:
"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:
ls Gateway/gateway.farConfigure 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:
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 explicitclientNameis specified in the subgraph'sschema-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
cd fusion-getting-started/Products
dotnet runTerminal 2: Reviews subgraph
cd fusion-getting-started/Reviews
dotnet runTerminal 3: Gateway
cd fusion-getting-started/Gateway
dotnet runWait for all three services to start. You should see output like this in each terminal:
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:
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.farfile - 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:
query {
products {
id
name
price
reviews {
body
stars
}
}
}Example in Nitro:
You should see:
{
"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:
- Fetched the products from the Products subgraph. This returned
id,name, andpricefor each product. - Resolved the reviews from the Reviews subgraph. Using each product's
id, the gateway called the Reviews subgraph's internalproductByIdlookup to get aProductstub, then resolved thereviewsfield on each stub. - 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.
query {
reviews {
body
stars
product {
name
price
}
}
}You should see:
{
"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:
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:
public class Product
{
public int Id { get; set; }
[Shareable]
public required string Name { get; set; }
public double Price { get; set; }
}Reviews subgraph (entity stub):
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:
- I want to add another subgraph to this project: Adding a Subgraph
- I want to understand entities more deeply: Entities and Lookups
- I need cross-subgraph field dependencies (
[Require]): Entities and Lookups - I need to deploy this: Deployment & CI/CD
- I need to secure this: Authentication and Authorization
- I'm coming from Apollo: Coming from Apollo Federation
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.
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#.
[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.
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.
- Trusted documents covers the full workflow: extraction, registration, and enforcement.
- Strawberry Shake and Relay both support build-time operation extraction.
When in the future you want to change or phase out parts of your schema you know the impact this change will have to your system before you apply it. This is a super power for API evolution.
Third-party GraphQL
A third-party API is consumed by external developers or clients outside your organization. GitHub’s GraphQL API is a canonical example. You publish a schema, and external teams build applications against it. Because you do not control the clients, they can send any operation they want.
Hot Chocolate provides cost analysis for this scenario. You assign weights to fields and connections, and the server rejects operations that exceed the performance budget before execution begins.
- Cost analysis explains field weights, type costs, and budget configuration.
- Controlling introspection lets you restrict schema visibility in production.
These two approaches complement each other. A common setup is to host both a public and an internal GraphQL API. The internal API uses trusted documents to strictly control operations, while the public API relies on cost analysis and other safeguards to manage external traffic and protect against abuse.
Key Terminology
| Term | Definition |
|---|---|
| Schema | The contract that describes what data clients can query. Hot Chocolate generates it from your C# code. |
| Query type | The root type for read operations. Clients enter the graph through fields on this type. |
| Mutation type | The root type for write operations. Mutations execute serially and are expected to cause side effects. |
| Subscription type | The root type for real-time operations. Clients subscribe to events and receive updates as they occur. |
| Resolver | A function that fetches data for a single field. In implementation-first, each public method on a [QueryType] class is a resolver for third-party or first-party APIs. |
| Batch resolver | A resolver that fetches data for multiple parent objects in a single call, improving performance by reducing the number of backend requests. Useful for solving the N+1 problem and optimizing data access patterns. |
| DataLoader | A batching and caching layer that groups multiple individual data requests into a single batch call, eliminating the N+1 problem. |
| Source generator | A Roslyn source generator that inspects your C# code at build time and generates the schema registration, resolver pipelines, and DataLoader infrastructure. |
| Cost analysis | 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"
// 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.
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
PublishAsyncandIEventHandler<T>. - Send (fire-and-forget): Who should act? Deliver to one endpoint without waiting for a result. Use
SendAsyncandIEventRequestHandler. - Request/reply: What is the result? Send and await a typed response. Use
RequestAsyncandIEventRequestHandler.
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:
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.
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.
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:
// 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());
}
}// 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:
- Request Pane: This is where you will input your queries.
- Response Pane: Here, you will see the output or response of your queries.
- 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:
{
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
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.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.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.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.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.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.
dnx skills add anthropics/skills --agent claude-codeOn the first run, dnx prompts you to confirm the package download before it proceeds:
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:
dnx --yes skills add anthropics/skills --agent claude-codeYou 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.
dotnet tool install -g skillsYou 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:
dotnet new tool-manifest
dotnet tool install skillsLocal 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.
dnx skills add anthropics/skills --agent claude-codeAgent names are case-sensitive. Use claude-code, not claude or Claude. For GitHub Copilot the name is github-copilot.
The skills CLI fetches the source, discovers the skills, installs them, and prints a summary:
Source: https://github.com/anthropics/skills.git
Found 2 skill(s)
┌─Installation Summary─────────────────────────────────────────────────────────┐
│ Canonical: /your-project/.agents/skills │
│ Symlinked: Claude Code │
└──────────────────────────────────────────────────────────────────────────────┘
┌─Installed 2 skill(s)─────────────────────────────────────────────────────────┐
│ ✓ pdf │
│ → /your-project/.claude/skills/pdf │
│ ✓ docx │
│ → /your-project/.claude/skills/docx │
└──────────────────────────────────────────────────────────────────────────────┘
Done! Review skills before use; they run with full agent permissions.The trailing line is a deliberate safety nudge. A skill can include scripts that the agent runs with your agent's full permissions. Install skills only from sources you trust, and read a new skill before you use it.
If you omit --agent, the CLI runs interactively and lets you pick which agents to target from the ones it detects on your machine. To install only specific skills from a multi-skill source, add --skill <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:
dnx skills listProject Skills
Skill Path Agents
pdf ./.claude/skills/pdf Claude Code
docx ./.claude/skills/docx Claude CodeThe 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:
dotnet tool update -g skillsTool 'skills' was successfully updated from version '0.3.0' to version '<new>'.To uninstall it:
dotnet tool uninstall -g skillsTool '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.mdwithdnx 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:
dnx skills add anthropics/skills --agent claude-codeSource: 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:
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 filesThe folder name should match the name in the frontmatter. Here is a minimal, valid SKILL.md with the two required fields:
---
name: roll-dice
description: Rolls one or more dice and returns the results. Use when the user asks to roll dice, flip for a decision, or pick a random number in a range.
---
# Roll dice
When the user asks to roll dice, parse the count and number of sides (default to one six-sided die),
then return each roll and the total.The dnx skills init command scaffolds this layout for you; Authoring Skills 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
skillsand 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:
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:
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 fieldspartialsource-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.
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.");
}
}Navigating the Type System Map
The following map helps you select the next detailed page without needing to learn every rule here.
Root Types
GraphQL defines a single root type for each operation kind. In C#, you can organize root fields across multiple semantic classes, and Hot Chocolate will merge them into the final root type.
| Element | Purpose | Authoring cue | Learn more |
|---|---|---|---|
| Query | Read entry point. Query fields should be side-effect-free and may execute in parallel. | [QueryType] or AddQueryType. |
Queries |
| 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
- Build a read entry point with Queries.
- Learn how returned shapes are inferred and configured in Object Types.
- Add field inputs with Arguments and Input Object Types.
- Strengthen the contract with Lists and Non-Null.
- Move to runtime behavior with Resolvers and DataLoader after the schema shape is clear.
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.
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.
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.
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.
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:
- Entity Framework for EF Core DbContext patterns and pooling.
- MongoDB for the MongoDB driver integration.
- Marten for Marten document database support.
- Extending Filtering for building custom filter providers.
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:
query {
me {
name
company {
id
name
}
}
}This produces the following resolver tree:
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.
[QueryType]
public partial class Query
{
public static string Foo() => "Bar";
}This generates the following schema:
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.
[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.
csharppublic 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.
csharppublic 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.
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.
csharpdescriptor
.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.
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.
csharpdescriptor
.Field("foo")
.Argument("id", a => a.Type<NonNullType<IntType>>())
.Resolve(context =>
{
var id = context.ArgumentValue<int>("id");
// Omitted code for brevity
});
Injecting Services
Hot Chocolate automatically recognizes types registered in the DI container and injects them into resolver parameters.
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.
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:
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.
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:
[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:
[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.
csharppublic 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
GraphQLExceptionfor 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:
builder
.AddGraphQL()
.AddSha256DocumentHashProvider();Learn more about hashing providers
Next Steps
- Building a public API? Start with Cost Analysis.
- Building a private API? Start with Trusted Documents.
- Need authentication? See Authentication.
- Need authorization? See Authorization.
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.
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.
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.
Global State
Global State lets you define properties on a per-request basis and makes them available to all resolvers and middleware.
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.
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
builder.Services
.AddMediator()
.AddCatalog(); // source-generated from your assembly nameThat 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.
// 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.
// 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.
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.
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
// 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
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.
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.
// 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:
// 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
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:
- If you apply
#!csharp [assembly: MediatorModule("Billing")], the method isAddBilling() - Otherwise, it uses the last segment of the assembly name:
Demo.CatalogproducesAddCatalog()
To set an explicit module name, add the attribute to any file in your project:
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>():
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:
builder.Services
.AddMediator()
.AddCatalog() // source-generated handlers
.AddHandler<ExternalPaymentHandler>(); // additional handler from another assemblyPrefer 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:
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.
// Register a named mediator
builder.Services
.AddMediator("billing")
.AddBilling();
// Register the default (unnamed) mediator
builder.Services
.AddMediator()
.AddCatalog();Resolve a named mediator from DI:
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:
/* Detailed source-code truncated for AI context efficiency. */If everything worked, dotnet run starts the server and you can:
POST /orderswith a JSON body to place an orderGET /productsto list products#!http POST /orders/{id}/shipto 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.Analyzersas 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
nextdelegate - a middleware that forgets to callnextsilently 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.Analyzerspackage is referenced withOutputItemType="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:
// InMemory - zero configuration
builder.Services
.AddMessageBus()
.AddEventHandler<OrderPlacedEventHandler>()
.AddInMemory();// RabbitMQ - production-ready
builder.Services
.AddMessageBus()
.AddEventHandler<OrderPlacedEventHandler>()
.AddRabbitMQ();// PostgreSQL - database-backed transport
builder.Services
.AddMessageBus()
.AddEventHandler<OrderPlacedEventHandler>()
.AddPostgres("Host=localhost;Database=mocha_messaging;Username=postgres;Password=postgres");// 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.
Bus scope (global defaults)
└── Transport scope (transport-specific overrides)
└── Endpoint scope (endpoint-specific overrides)To add middleware at the transport level:
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.
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:
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:
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():
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>():
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:
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:
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
- InMemory Transport - Set up the InMemory transport for development and testing.
- PostgreSQL Transport - Configure database-backed messaging with PostgreSQL.
- RabbitMQ Transport - Configure the RabbitMQ transport for production deployments.
- Azure Service Bus Transport - Configure managed cloud messaging on Azure.
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:
- Click on the
New APIbutton located at the top of the document explorer toolbar. - Right-click within the document explorer and select
New APIfrom 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. - Choose the API Type. Options include
API Collection,API Service, orAPI 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:
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.
Environments: This section is for managing environments.
History: This section displays the list of recently executed queries.
Settings: To modify application preferences, you can navigate to the Settings section. Learn more about it in our Settings Guide.
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.
Document Tree: Here you can manage files, folders, documents, and APIs. Read more about it in the Explorer.
Main Area: The main area showcases the content related to the selected section from the sidebar. In this instance, it displays the Documents section.
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
The count of members and the number of seats available in your subscription are displayed in the management overview section. (members/total seats)
This list show all the members of the organization and their role.
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.
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:
- By clicking on the join link they received via email.
- 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.
Press the 'Edit' button on the right side of the member list to open the edit dialog.
Select the desired role for the member.
Press the 'Save' button to save the changes.
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:
By signing in from the new origin. This will trigger a prompt to add the URL to the organization.
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
- Configure the allowed types of origins.
- Add redirect Urls to the organization.
- 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.
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 resultStrategies
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.
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.
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.
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.
- Create a dotnet tool-manifest.
dotnet new tool-manifest- Install the Strawberry Shake tools.
dotnet tool install StrawberryShake.Tools --localStep 2: Create a Blazor WebAssembly project
Next, we will create our Blazor project so that we have a little playground.
- First, a new solution called
Demo.sln.
dotnet new sln --name Demo- Create a new Blazor for WebAssembly application.
dotnet new blazorwasm --name Demo- Add the project to the solution
Demo.sln.
dotnet sln add ./DemoStep 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.
- Add the
StrawberryShake.Blazorpackage to your project.
dotnet add Demo package StrawberryShake.BlazorStep 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.
- Add the crypto client to your Blazor application.
dotnet graphql init https://demo.chillicream.com/graphql/ --clientName CryptoClient --Path ./Demo- Customize the namespace of the generated client to be
Demo.GraphQL. For this head over to the.graphqlrc.jsonand insert a namespace property in theStrawberryShakesection.
{
"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.
- Choose your favorite IDE and the solution. If your are using VSCode do the following:
code ./Demo- Create new query document
GetSessions.graphqlwith the following content:
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.
- Compile your project.
dotnet buildWith the project compiled the strawberry shake generator produced a client but also components that we can use in Blazor.
- Head over to the
Program.csand add the newCryptoClientto 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.
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();- Go to
_Imports.razorand addDemo.GraphQLto the common imports
@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 StrawberryShakeStep 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.
Head over to
Pages/Index.razor(from .NET 8,Home.razor).Remove everything from your page but the
@page "/"
@page "/"- Add the
UseGetAssetscomponent to your page.
@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.
- With that done let's render the actual content.
@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>- Start the Blazor application with
#!shell dotnet watch --project ./Demoand 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.
{
"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.
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
{
products(first: 5) {
nodes {
name
brand {
name
}
}
}
}The products resolver runs once and returns five products. Then the brand resolver runs once per product. A naive brand resolver that queries the database directly issues five queries for five products, and fifty queries for fifty products:
1 query: products(first: 5)
5 queries: brand for product 1..5 ← N+1The 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.
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
- Loading data by key? Start with DataLoader.
- Resolving one field for many parents? See Batch Resolvers.
- New to resolvers? See Resolvers for the resolver tree mental model.
- Using Entity Framework? See Entity Framework for integration patterns.
- Looking for HTTP request batching? See Server Batching.
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.
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();
}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
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
ValueTask<IList> ToListAsync(CancellationToken cancellationToken);Returns a list of items.
FirstOrDefaultAsync
ValueTask<object?> FirstOrDefaultAsync(CancellationToken cancellationToken);Returns the first element of the sequence, or a default value if the sequence contains no elements.
SingleOrDefaultAsync
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.
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:
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
- Entity Framework integration for EF Core executables
- MongoDB integration for MongoDB executables
- Filtering for applying filters to executables
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.
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
dotnet-trace psUse 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
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 --inclusiveThis 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
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
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
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.Httpmonitor 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:
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:
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:
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:
dotmemory get-snapshot <pid> --save-to-dir="$OUT/dotmemory"Attach and take a snapshot every 30 seconds, at most three, then detach:
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 inGC.RunFinalizers,PollGCWorkerand a blocked file watcher inInterop+Sys.Readdominate the exclusive time. Ignore them and read the frames below them. The speedscope export puts a syntheticCPU_TIMEorUNMANAGED_CODE_TIMEleaf under every sample, and?!?stands for native frames without symbols, mostly GC threads. For the CPU a process really uses, readdotnet.process.cpu.timefromdotnet-counters. - Take the dotMemory snapshots last. In two sessions the profiled Fusion gateway crashed 45 seconds and two minutes after
dotmemory attachhad disconnected, under load, with a native fault that the runtime turned into acreatedumpminidump. SetDOTNET_DbgEnableMiniDump=1on the application if you want that dump, and restart the application after a dotMemory session before measuring anything else. .dtpand.dmwsnapshots are for Rider on the host. Agents should use thedotnet-traceanddotnet-gcdumpoutputs, 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:
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.csApply 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.
MultipartBodyLengthLimitapplies to every section (HC0135), andmaxAllowedRequestSizealso applies to theoperationsfield (HC0010); whichever of the two is smaller limitsoperationsand gives its code.BufferBodyLengthLimit, whenBufferBodyis set, applies to the whole body (HC0136).FormFeaturedoes not applymaxAllowedRequestSize; it throwsInvalidDataExceptionforMultipartBodyLengthLimitandIOExceptionforBufferBodyLengthLimit. - With
BufferBodyset, it buffers the body even when an earlier component already made the body seekable, soBufferBodyLengthLimitapplies in that case too. - It refuses a request whose
IAntiforgeryValidationFeaturehasIsValidset tofalse, without theHttpContext.Itemskeys thatFormFeaturechecks first. - It does not read URL-encoded forms, which never reach the multipart middleware.
- It does not apply endpoint
IFormOptionsMetadata, which the publicFormFeatureconstructor 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:
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:
dotnet new graphql -n ShippingThe 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:
<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:
"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:
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:
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:
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:
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);
}GetShipmentByIdis a public lookup. Clients can call it directly, and the gateway uses it for entity resolution.GetProductByIdis an internal lookup. It is hidden from the composite schema and exists only for the gateway to enter the Shipping subgraph'sProducttype 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:
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:
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:
dotnet run --project ./Shipping -- schema exportThis generates two files in the Shipping directory:
schema.graphqlscontains the subgraph's GraphQL schema.schema-settings.jsoncontains 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:
{
"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:
nitro fusion compose \
-s Products/schema.graphqls \
-s Reviews/schema.graphqls \
-s Shipping/schema.graphqls \
-a gateway.farIf composition succeeds, copy the updated gateway.far to your gateway project directory:
cp gateway.far Gateway/gateway.farIf you already have a composed gateway.far with the Products and Reviews subgraphs, you can add the new subgraph to the existing archive:
nitro fusion compose \
-s Shipping/schema.graphqls \
-a gateway.farTest Cross-Subgraph Queries
Start all services and the gateway. With the Shipping subgraph added, you can now query shipment data that crosses subgraph boundaries:
query {
productById(id: 1) {
name
price
shipments {
trackingNumber
status
}
}
}This query touches two subgraphs:
- The gateway calls the Products subgraph to fetch
nameandprice. - The gateway uses the internal lookup to enter the Shipping subgraph and resolve
shipmentsfor 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:
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
@requirepatterns 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:
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:
app.MapGraphQL().RequireAuthorization();Or apply a named policy that requires a specific scope:
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:
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:
{
"transports": {
"http": {
"url": "http://products:5001/graphql",
"clientName": "fusion"
}
}
}Register header propagation for that client:
builder.Services
.AddHttpClient("fusion")
.AddHeaderPropagation();Add the UseHeaderPropagation middleware before the GraphQL middleware in your pipeline so headers are forwarded:
// 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.
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:
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:
{
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:
{
"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:
[
{
"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:
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
}{
"_0___fusion_1_id": "1",
"_1___fusion_1_id": "2"
}The subgraph answers with one response, and the gateway splits it by alias:
{
"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:
{
"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": falseas 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.variableBatchingandrequestBatchingmirror the batching the subgraph's server options allow, whichAddSourceSchemaDefaults()turns on, andaliasBatchingis alwaystrue.
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:
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:
{
"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:
- A deterministic GET route so caches get a stable cache key.
- 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.
GET /graphql?query=query GetProducts{products{nodes{name}}}The same applies when variables are included.
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.
GET /graphql/persisted/GetProducts/123456789Variables can then be sent as query parameters.
GET /graphql/persisted/GetProducts/123456789?variables={"first":5}In order to use persisted-operation routes you need to add the middleware MapGraphQLPersistedOperations.
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.
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.
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.
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:
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.
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.
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
npm (recommended)
npm install @chillicream/nitroThe 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)
brew install ChilliCream/tools/nitro-cli.NET tool
Install globally:
dotnet tool install --global ChilliCream.Nitro.CommandLineOr install per-project:
dotnet new tool-manifest
dotnet tool install ChilliCream.Nitro.CommandLineVerify installation
nitro --versionCommands 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
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:
nitro fusion compose \
--source-schema-file ./Products/schema.graphqls \
--source-schema-file ./Reviews/schema.graphqls \
--archive gateway.far \
--environment Development \
--enable-global-object-identificationCompose two remote schemas and one local schema. Each remote settings option corresponds to the URL occurrence above it:
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.farAfter a successful composition, Nitro prints:
✅ Composite schema written to '/absolute/path/to/gateway.far'.Auto-discover and compose all schemas in the current directory:
nitro fusion compose --archive gateway.farWatch mode for local development:
nitro fusion compose --watchWatch 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:
nitro fusion compose \
--exclude-by-tag experimental \
--exclude-by-tag internal-only \
--archive gateway.farnitro fusion download
Downloads gateway configuration from Nitro Cloud.
Syntax
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
nitro fusion download \
--api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg== \
--stage production \
--output-file gateway.farnitro fusion migrate
Migrates configuration files from v1 to v2 format.
Syntax
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:
nitro fusion migrate subgraph-configMigrate configs in a specific directory:
nitro fusion migrate subgraph-config --working-directory ./subgraphsnitro fusion publish
Publishes a Fusion configuration to a stage on Nitro Cloud. Three input modes are available (mutually exclusive).
Syntax
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.
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.
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.
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 | -- |
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 |
nitro fusion publish startnitro 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 | -- |
nitro fusion publish validate --archive gateway.farnitro 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 | -- |
nitro fusion publish commit --archive gateway.farnitro fusion publish cancel
Cancel the deployment.
| Option | Description | Default |
|---|---|---|
--request-id |
Deployment request ID | Uses cached value from begin |
nitro fusion publish cancelOrchestrated Workflow
A typical advanced deployment follows this sequence:
nitro fusion publish begin-- reserve a deployment slotnitro fusion publish start-- start compositionnitro fusion publish validate-- validate the configurationnitro 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
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:
nitro fusion run gateway.far --port 5000Start a gateway on a random port:
nitro fusion run gateway.farnitro fusion settings set
Modifies composition settings in a Fusion archive.
Syntax
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:
nitro fusion settings set global-object-identification true --archive gateway.far
nitro fusion settings set node-resolution source-schema --archive gateway.farUse common-runtime-types routing on an existing archive:
nitro fusion settings set shareable-field-runtime-type-routing common-runtime-types \
--archive gateway.farFor the behavior of each routing value, see Shareable Abstract Field Routing.
Configure cache control merging:
nitro fusion settings set cache-control-merge-behavior include --archive gateway.farExclude tagged fields:
nitro fusion settings set exclude-by-tag experimental,internal-only --archive gateway.farnitro fusion upload
Uploads a source schema to Nitro Cloud for later composition.
Syntax
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:
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:
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
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:
nitro fusion validate \
--source-schema-file ./Products/schema.graphqls \
--stage production \
--api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==Validate from an archive:
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.
{
"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.
{
"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.
{
"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.
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.-> X1. 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:
# Schema A
type Query {
book: Book @cost(weight: "-7")
}
type Book {
id: ID
}# 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:
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:
# Schema A
type Query {
field: [Int] @listSize(assumedSize: 10)
}# 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:
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:
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.
# 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:
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:
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:
nitro fusion compose --include-satisfiability-pathsPaths 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
@lookupfor 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
@keyselections across source schemas. The shared entity has different@keyselection sets in different source schemas, so the validator cannot find a key both sides recognize. Align the key selection sets, or add additional@keydirectives so every key the producer uses is also a key the consumer recognizes. @requirereaches 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.Nodeinterface without a node lookup. A type implementsNodebut no source schema exposes a#!sdl node(id: ID!): Nodelookup 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:
{
"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
MaxResponseSizeis 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:
{
book {
# 10 (weight: "10")
title # 0 (scalar, unannotated)
author {
# 1 (object field, unannotated)
name # 0 (scalar, unannotated)
}
}
}
# Field cost: 10 + 0 + 1 + 0 = 11Add 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:
{
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 = 111Type 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:
{
# 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 = 152The examples use this source schema:
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
@includeand@skipbranches 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 of0remains0, and the field-call cost is still paid once.
List Size
The analyzer selects the assumed size for a list field in this order:
- An inherited size from a parent
@listSize(sizedFields:)annotation. The inherited size takes priority over the child field's own@listSize. - The maximum slicing-argument value present after coercion, including a schema argument default.
slicingArgumentDefaultValue, only when no slicing argument is present.assumedSize.- 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.
builder.Services.AddGraphQLGatewayServer(
disableDefaultSecurity: true);Set EnforceCostLimits to false when only cost enforcement should be disabled:
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.
{
"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 |
{
"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:
- Document Cache
- Document Parser
- Document Validation
- Operation Variable Coercion
- Cost Analysis
- Operation Plan Cache
- Operation Plan
- Skip Warmup Execution
- Concurrency Gate
- 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). |
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:
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);
}
}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:
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
# 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
[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:
public static int GetShippingEstimate(
[Parent] Product product,
string zip,
[Require("weight")] float packageWeight)
=> ShippingCalculator.Estimate(zip, packageWeight);Public facing composite schema (what clients see)
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:
- Composition reads the
@requiredirective and removes the annotated argument from the composite schema. Clients never see it. - Query planning detects the dependency. The gateway plans an additional fetch to retrieve the required fields from whichever subgraph can provide it.
- 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
# 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:
weightmaps directly because the input field name matches the entity field name.length: dimensions.lengthmaps the input fieldlengthfrom the nested entity pathdimensions.length. The same applies towidthandheight.
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
[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);
}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)
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
# Inventory subgraph
type Product {
id: ID!
shippingEstimate(
zip: String!
weight: Float! @require(field: "weight")
price: Float! @require(field: "price")
): Int!
}C# resolver
[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
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
# 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
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
# 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
[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:
type Review {
product: Product @provides(fields: "name price")
}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.
weight: Float! @require(field: "weight")Object selection. Use when mapping multiple entity fields into a single input object argument.
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.
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.
countryCodes: [String!]! @require(field: "seller.addresses[countryCode]")List projection. Use when you need multiple fields from each element but want to drop the rest.
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
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
@requiredirective is on the argument, not on the field - The
fieldvalue is a valid FieldSelectionMap (invalid syntax triggers aREQUIRE_INVALID_SYNTAXcomposition error)
Next Steps
- Need entity identity and lookup patterns? See Entities and Lookups for the full guide to keys, public vs. internal lookups, and composite keys.
- Need field ownership contracts? See Field Ownership.
- Need the directive and attribute quick reference? See the Attribute and Directive Reference.
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]
Allnitro fusionsubcommands fall back to the environment variablesNITRO_API_ID,NITRO_API_KEY,NITRO_STAGE, andNITRO_TAGwhen 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:
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 theChilliCream.Nitropackage. It requires theChilliCream.Nitro.Fusionpackage to be referenced in your project. IfAddDefaults()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:
dotnet run --project ./src/SubgraphA -- schema export --output schema.graphqlAfter 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):
- 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- 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.graphqlnitro 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 touploadmust match the tag you reference frompublish. 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:
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.
- 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 }}- 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)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:
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 publishmust 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.
- 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- 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.graphqlnitro 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:
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 againstdevwhile you're about to publish toproductiondefeats 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:
nitro fusion compose \
--source-schema-file ./src/SubgraphA/schema.graphqls \
--source-schema-file ./src/SubgraphB/schema.graphqls \
--archive ./gateway.farThe gateway loads the archive from disk:
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:
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
- CLI reference for every
nitro fusioncommand and its options. - Composition for what runs server-side when you publish.
- Migrating from v15 to v16 if you're moving an existing gateway over.
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.
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:
# Source schema
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
}# Composed schema
type Product {
id: ID!
name: String!
price: Float!
}Example -- multiple keys:
# Source schema
type Product @key(fields: "id") @key(fields: "sku") {
id: ID!
sku: String!
name: String!
}Example -- composite key:
# 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.
directive @lookup on FIELD_DEFINITIONLookup 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:
# 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!
}# 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.
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:
# Source schema
type Query {
personById(personId: ID! @is(field: "id")): Person @lookup
}
type Person @key(fields: "id") {
id: ID!
name: String!
}# Composed schema
type Query {
personById(personId: ID!): Person
}
type Person {
id: ID!
name: String!
}Example -- nested field reference:
# Source schema
type Query {
personByAddressId(id: ID! @is(field: "address.id")): Person @lookup
}In C#: The
@ismapping 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.
directive @interfaceObject on OBJECTThe 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:
# Catalog source schema
interface Media @key(fields: "id") {
id: ID!
title: String!
}
type Book implements Media @key(fields: "id") {
id: ID!
title: String!
}# Analytics source schema
type Query {
mediaByKey(id: ID!): Media @lookup @internal
}
type Media @interfaceObject @key(fields: "id") {
id: ID!
views: Int!
}# 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.
directive @implement on FIELD_DEFINITIONUse @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:
# Catalog source schema
type Book implements Media @key(fields: "id") {
id: ID!
title: String!
views: Int! @implement
}# 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.
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:
# Source schema (Shipping subgraph)
type Product {
shippingEstimate(zip: String!, weight: Float! @require(field: "weight")): Int!
}# Composed schema (weight argument removed)
type Product {
shippingEstimate(zip: String!): Int!
}Example -- structured requirement with input type:
# 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.
directive @shareable repeatable on OBJECT | FIELD_DEFINITIONWhen 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:
# Source schema A (Products subgraph)
type Product @key(fields: "id") {
id: ID!
name: String! @shareable
description: String!
}# Source schema B (Inventory subgraph)
type Product @key(fields: "id") {
id: ID!
name: String! @shareable
inStock: Boolean!
}# 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.
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:
# 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
}# 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.
directive @external on FIELD_DEFINITIONEvery @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:
# 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:
# 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 (fromHotChocolate.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.
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:
# Source schema: original Catalog subgraph (unchanged)
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
}# Source schema: new Payments subgraph (takes over price)
type Product @key(fields: "id") {
id: ID! @external
price: Float! @override(from: "Catalog")
tax: Float!
}# Composed schema
type Product {
id: ID!
name: String!
price: Float!
tax: Float!
}In C#:
#!csharp [Override("Catalog")]attribute (fromHotChocolate.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.
directive @internal on OBJECT | FIELD_DEFINITIONInternal 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:
# 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!
}# 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.
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITIONUnlike @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:
# Source schema A
type Product @key(fields: "id") @key(fields: "sku") {
id: ID!
sku: String! @inaccessible
name: String!
}# Source schema B
type Product @key(fields: "sku") {
sku: String!
price: Float!
}# 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